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:" %}
- {% for mask in duplicates %}
+ {% for mask, translations in analysis.duplicates_resolved %}
-
-
{{ mask }}
+ {{ mask }}
:
+ {% for translation in translations %}{{ translation }}{% endfor %}
{% endfor %}
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())