diff --git a/weblate/templates/trans/alert/duplicatefilemask.html b/weblate/templates/trans/alert/duplicatefilemask.html index 0d436c4aae67..212a2f4e66f7 100644 --- a/weblate/templates/trans/alert/duplicatefilemask.html +++ b/weblate/templates/trans/alert/duplicatefilemask.html @@ -1,16 +1,17 @@ {% load i18n %}

- {% trans "Some linked components have the same file mask." %} + {% trans "Some linked components have the same file mask or share some of the files." %} {% trans "Please fix this by removing one of them." %}

-

{% trans "The following file masks were found multiple times:" %}

+

{% trans "The following files were found multiple times:" %}

diff --git a/weblate/trans/models/__init__.py b/weblate/trans/models/__init__.py index 63124e463bef..3f08834eb80d 100644 --- a/weblate/trans/models/__init__.py +++ b/weblate/trans/models/__init__.py @@ -184,7 +184,7 @@ def post_delete_linked(sender, instance, **kwargs) -> None: # When removing project, the linked component might be already deleted now try: if instance.linked_component: - instance.linked_component.update_link_alerts(noupdate=True) + instance.linked_component.update_alerts() except Component.DoesNotExist: pass diff --git a/weblate/trans/models/alert.py b/weblate/trans/models/alert.py index 254f6081d69f..129b965a9b80 100644 --- a/weblate/trans/models/alert.py +++ b/weblate/trans/models/alert.py @@ -12,7 +12,7 @@ import sentry_sdk from django.conf import settings from django.db import models -from django.db.models import Count +from django.db.models import Count, Q from django.template.loader import render_to_string from django.utils import timezone from django.utils.functional import cached_property @@ -29,7 +29,9 @@ from django_stubs_ext import StrOrPromise from weblate.auth.models import User - from weblate.trans.models import Component, Translation + from weblate.trans.models.component import Component, ComponentQuerySet + from weblate.trans.models.translation import Translation, TranslationQuerySet + ALERTS: dict[str, type[BaseAlert]] = {} ALERTS_IMPORT: set[str] = set() @@ -228,6 +230,48 @@ def __init__(self, instance, duplicates) -> None: super().__init__(instance) self.duplicates = duplicates + @staticmethod + def get_translations(component: Component) -> TranslationQuerySet: + from weblate.trans.models import Translation + + return Translation.objects.filter( + Q(component=component) | Q(component__linked_component=component) + ) + + @classmethod + def check_component(cls, component: Component) -> bool | dict | None: + if component.is_repo_link: + return False + + translations = set( + cls.get_translations(component) + .values_list("filename") + .annotate(count=Count("id")) + .filter(count__gt=1) + .values_list("filename", flat=True) + ) + translations.discard("") + if translations: + return {"duplicates": sorted(translations)} + return False + + def resolve_filename( + self, filename: str + ) -> ComponentQuerySet | TranslationQuerySet: + if "*" in filename: + # Legacy path for old alerts + # TODO: Remove in Weblate 6.0 + return self.instance.component.component_set.filter(filemask=filename) + return self.get_translations(self.instance.component).filter(filename=filename) + + def get_analysis(self): + return { + "duplicates_resolved": [ + (filename, self.resolve_filename(filename)) + for filename in self.duplicates + ] + } + @register class MergeFailure(ErrorAlert): diff --git a/weblate/trans/models/component.py b/weblate/trans/models/component.py index ab00082c7d23..4fd062f0b0d2 100644 --- a/weblate/trans/models/component.py +++ b/weblate/trans/models/component.py @@ -7,7 +7,7 @@ import os import re import time -from collections import Counter, defaultdict +from collections import defaultdict from copy import copy from glob import glob from itertools import chain @@ -1962,7 +1962,7 @@ def get_repo_link_url(self): return "weblate://{}".format("/".join(self.get_url_path())) @cached_property - def linked_childs(self): + def linked_childs(self) -> ComponentQuerySet: """Return list of components which links repository to us.""" children = self.component_set.prefetch() for child in children: @@ -3218,6 +3218,8 @@ def after_save( # Update alerts after stats update self.update_alerts() + if self.linked_component: + self.linked_component.update_alerts() # Make sure we create glossary if create and settings.CREATE_GLOSSARIES: @@ -3293,18 +3295,6 @@ def update_variants(self, updated_units=None) -> None: variant_regex="", unit_count=0 ).delete() - def update_link_alerts(self, noupdate: bool = False) -> None: - base = self.linked_component if self.is_repo_link else self - masks = [base.filemask] - masks.extend(base.linked_childs.values_list("filemask", flat=True)) - duplicates = [item for item, count in Counter(masks).items() if count > 1] - if duplicates: - self.add_alert( - "DuplicateFilemask", duplicates=duplicates, noupdate=noupdate - ) - else: - self.delete_alert("DuplicateFilemask") - def _update_alerts(self): self._alerts_scheduled = False # Flush alerts case, mostly needed for tests @@ -3312,8 +3302,6 @@ def _update_alerts(self): update_alerts(self) - self.update_link_alerts() - # Update libre checklist upon save on all components in a project if ( settings.OFFER_HOSTING diff --git a/weblate/trans/tests/test_alert.py b/weblate/trans/tests/test_alert.py index 30a285ecf633..d84a2cbe159a 100644 --- a/weblate/trans/tests/test_alert.py +++ b/weblate/trans/tests/test_alert.py @@ -112,6 +112,24 @@ def test_monolingual(self) -> None: component.alert_set.filter(name="MonolingualTranslation").exists() ) + def test_duplicate_mask(self): + component = self.component + self.assertFalse(component.alert_set.filter(name="DuplicateFilemask").exists()) + response = self.client.get(component.get_absolute_url()) + self.assertNotContains( + response, "The following files were found multiple times" + ) + + other = self.create_link_existing() + + self.assertTrue(component.alert_set.filter(name="DuplicateFilemask").exists()) + response = self.client.get(component.get_absolute_url()) + self.assertContains(response, "The following files were found multiple times") + + other.delete() + + self.assertFalse(component.alert_set.filter(name="DuplicateFilemask").exists()) + class LanguageAlertTest(ViewTestCase): def create_component(self): @@ -120,10 +138,8 @@ def create_component(self): def test_ambiguous_language(self) -> None: component = self.component self.assertFalse(component.alert_set.filter(name="AmbiguousLanguage").exists()) - self.component.add_new_language( - Language.objects.get(code="ku"), self.get_request() - ) - self.component.update_alerts() + component.add_new_language(Language.objects.get(code="ku"), self.get_request()) + component.update_alerts() self.assertTrue(component.alert_set.filter(name="AmbiguousLanguage").exists())