From 1102ff570a3143db84dafce4ca54d5e8e90ce5ee Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:13:45 -0500 Subject: [PATCH 01/16] obsolete resources and corresponding entities Does not delete resources/entities/translated_resources - filters for obsolete strings will be added later --- pontoon/base/models/entity.py | 8 ++++++++ pontoon/base/models/resource.py | 10 ++++++++++ pontoon/sync/core/entities.py | 10 ++++++---- pontoon/sync/tests/test_entities.py | 19 +++++++++++++++---- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/pontoon/base/models/entity.py b/pontoon/base/models/entity.py index 53323dc16e..1de0e6db26 100644 --- a/pontoon/base/models/entity.py +++ b/pontoon/base/models/entity.py @@ -251,6 +251,14 @@ def prefetch_entities_data(self, locale: Locale, preferred_source_locale: str): return entities + def obsolete(self, now): + entities = list(self) + for entity in entities: + entity.obsolete = True + entity.date_obsoleted = now + entity.section = None + Entity.objects.bulk_update(entities, ["obsolete", "date_obsoleted", "section"]) + class Entity(DirtyFieldsMixin, models.Model): resource = models.ForeignKey(Resource, models.CASCADE, related_name="entities") diff --git a/pontoon/base/models/resource.py b/pontoon/base/models/resource.py index 593c9cc2bc..a794cf705c 100644 --- a/pontoon/base/models/resource.py +++ b/pontoon/base/models/resource.py @@ -3,6 +3,14 @@ from django.utils import timezone +class ResourceQuerySet(models.QuerySet): + def obsolete(self, now): + from pontoon.base.models.entity import Entity + + self.update(obsolete=True, date_obsoleted=now) + Entity.objects.filter(resource__in=self).obsolete(now) + + class Resource(models.Model): project = models.ForeignKey("Project", models.CASCADE, related_name="resources") path = models.TextField() # Path to localization file @@ -42,6 +50,8 @@ class Format(models.TextChoices): deadline = models.DateField(blank=True, null=True) + objects = ResourceQuerySet.as_manager() + # Formats that allow empty translations EMPTY_TRANSLATION_FORMATS = { Format.DTD, diff --git a/pontoon/sync/core/entities.py b/pontoon/sync/core/entities.py index 01ba6b1b06..8c21a878bf 100644 --- a/pontoon/sync/core/entities.py +++ b/pontoon/sync/core/entities.py @@ -70,7 +70,7 @@ def sync_resources_from_repo( with transaction.atomic(): renamed_paths = rename_resources(project, paths, checkout) - removed_paths = remove_resources(project, paths, checkout) + removed_paths = remove_resources(project, paths, checkout, now) old_res_added_ent_count, changed_paths = update_resources(project, updates, now) new_res_added_ent_count, _ = add_resources(project, updates, changed_paths, now) update_translated_resources(project, locale_map, paths) @@ -103,7 +103,10 @@ def rename_resources( def remove_resources( - project: Project, paths: L10nConfigPaths | L10nDiscoverPaths, checkout: Checkout + project: Project, + paths: L10nConfigPaths | L10nDiscoverPaths, + checkout: Checkout, + now: datetime, ) -> set[str]: if not checkout.removed: return set() @@ -115,8 +118,7 @@ def remove_resources( ) removed_db_paths = {res.path for res in removed_resources} if removed_db_paths: - # FIXME: https://github.com/mozilla/pontoon/issues/2133 - removed_resources.delete() + removed_resources.obsolete(now) rm_count = len(removed_db_paths) str_source_files = "source file" if rm_count == 1 else "source files" log.info( diff --git a/pontoon/sync/tests/test_entities.py b/pontoon/sync/tests/test_entities.py index 08d01075c0..33974daf01 100644 --- a/pontoon/sync/tests/test_entities.py +++ b/pontoon/sync/tests/test_entities.py @@ -12,6 +12,7 @@ from django.utils import timezone from pontoon.base.models import Entity, Project, TranslatedResource +from pontoon.base.models.translation import Translation from pontoon.base.tests import ( EntityFactory, LocaleFactory, @@ -54,13 +55,17 @@ def test_remove_resource(): ResourceFactory.create(project=project, path="a.ftl", format="fluent") ResourceFactory.create(project=project, path="b.po", format="gettext") res_c = ResourceFactory.create(project=project, path="c.ftl", format="fluent") + entity_c = EntityFactory.create(resource=res_c, string="Hello") + translation_c = TranslationFactory.create( + entity=entity_c, locale=locale, string="Bonjour" + ) # Filesystem setup makedirs(repo.checkout_path) build_file_tree( repo.checkout_path, { - "en-US": {"a.ftl": "", "b.pot": ""}, + "en-US": {"a.ftl": "", "b.po": ""}, "fr-Test": {"a.ftl": "", "b.po": "", "c.ftl": ""}, }, ) @@ -79,9 +84,15 @@ def test_remove_resource(): assert sync_resources_from_repo( project, locale_map, mock_checkout, paths, now ) == (0, set(), {"c.ftl"}) - assert {res.path for res in project.resources.all()} == {"a.ftl", "b.po"} - with pytest.raises(TranslatedResource.DoesNotExist): - TranslatedResource.objects.get(resource=res_c) + assert {res.path for res in project.resources.all()} == { + "a.ftl", + "b.po", + "c.ftl", + } + assert [res.obsolete for res in project.resources.all()] == [False, False, True] + assert TranslatedResource.objects.filter(resource=res_c).exists() + assert Entity.objects.filter(pk=entity_c.pk).exists() + assert Translation.objects.filter(pk=translation_c.pk).exists() @pytest.mark.django_db From efacbf2c3f5c0c99b788dc71617a43dd3b5c4f91 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:42:56 -0500 Subject: [PATCH 02/16] fix tests --- pontoon/sync/tests/test_entities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pontoon/sync/tests/test_entities.py b/pontoon/sync/tests/test_entities.py index 33974daf01..82bcf7ed9c 100644 --- a/pontoon/sync/tests/test_entities.py +++ b/pontoon/sync/tests/test_entities.py @@ -89,7 +89,10 @@ def test_remove_resource(): "b.po", "c.ftl", } - assert [res.obsolete for res in project.resources.all()] == [False, False, True] + assert project.resources.count() == 3 + assert project.resources.get(path="c.ftl").obsolete is True + assert project.resources.get(path="a.ftl").obsolete is False + assert project.resources.get(path="b.po").obsolete is False assert TranslatedResource.objects.filter(resource=res_c).exists() assert Entity.objects.filter(pk=entity_c.pk).exists() assert Translation.objects.filter(pk=translation_c.pk).exists() From 0de52d55421840f503c03008b7684313943a5557 Mon Sep 17 00:00:00 2001 From: Jamie <164675620+functionzz@users.noreply.github.com> Date: Fri, 13 Feb 2026 18:59:05 -0500 Subject: [PATCH 03/16] Update pontoon/base/models/entity.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matjaž Horvat --- pontoon/base/models/entity.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pontoon/base/models/entity.py b/pontoon/base/models/entity.py index 1de0e6db26..f72443e765 100644 --- a/pontoon/base/models/entity.py +++ b/pontoon/base/models/entity.py @@ -252,12 +252,11 @@ def prefetch_entities_data(self, locale: Locale, preferred_source_locale: str): return entities def obsolete(self, now): - entities = list(self) - for entity in entities: - entity.obsolete = True - entity.date_obsoleted = now - entity.section = None - Entity.objects.bulk_update(entities, ["obsolete", "date_obsoleted", "section"]) + self.update( + obsolete=True, + date_obsoleted=now, + section=None, + ) class Entity(DirtyFieldsMixin, models.Model): From c36236f409f3068fcd6b8a6f2a0e67bf123464de Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:57:32 -0500 Subject: [PATCH 04/16] add .current to TranslatedResourceQueryset + ResourceQueryset --- pontoon/administration/forms.py | 6 +++--- pontoon/administration/views.py | 6 +++--- pontoon/base/models/locale.py | 4 ++-- pontoon/base/models/project.py | 2 +- pontoon/base/models/project_locale.py | 2 +- pontoon/base/models/resource.py | 3 +++ pontoon/base/models/translated_resource.py | 3 +++ pontoon/base/models/translation.py | 14 +++++++++----- pontoon/base/signals.py | 6 +++--- pontoon/base/views.py | 18 ++++++++++++------ pontoon/batch/views.py | 2 +- pontoon/insights/tasks.py | 3 ++- pontoon/localizations/views.py | 5 +++-- .../commands/send_deadline_notifications.py | 12 ++++++++---- pontoon/pretranslation/tasks.py | 13 +++++++++---- pontoon/projects/views.py | 4 ++-- pontoon/sync/core/entities.py | 7 ++++--- pontoon/sync/core/translations_from_repo.py | 12 ++++++------ pontoon/teams/views.py | 8 +++++--- pontoon/terminology/models.py | 6 +++--- pontoon/translations/views.py | 2 +- 21 files changed, 84 insertions(+), 54 deletions(-) diff --git a/pontoon/administration/forms.py b/pontoon/administration/forms.py index f8440e615f..b5878bcdea 100644 --- a/pontoon/administration/forms.py +++ b/pontoon/administration/forms.py @@ -123,9 +123,9 @@ def __init__(self, *args, **kwargs): # If the project instance is available, filter resources for this project if kwargs.get("instance") and kwargs["instance"].project: project = kwargs["instance"].project - self.fields["resources"].queryset = Resource.objects.filter( - project=project - ).select_related() + self.fields["resources"].queryset = ( + Resource.objects.current().filter(project=project).select_related() + ) TagInlineFormSet = inlineformset_factory(Project, Tag, form=TagInlineForm, extra=1) diff --git a/pontoon/administration/views.py b/pontoon/administration/views.py index e321a79436..6bc96d27e8 100644 --- a/pontoon/administration/views.py +++ b/pontoon/administration/views.py @@ -306,7 +306,7 @@ def manage_project(request, slug=None, template="admin_project.html"): } # Set locale in Translate link - if Resource.objects.filter(project=project).exists() and locales_selected: + if Resource.objects.current().filter(project=project).exists() and locales_selected: locale = ( utils.get_project_locale_from_request(request, project.locales) or locales_selected[0].code @@ -373,7 +373,7 @@ def _get_resource_for_database_project(project): """ try: - return Resource.objects.get( + return Resource.objects.current().get( project=project, ) except Resource.DoesNotExist: @@ -492,7 +492,7 @@ def manage_project_strings(request, slug=None): # Get all strings, find the ones that changed, update them in the database. formset = EntityFormSet(request.POST, queryset=entities) if formset.is_valid(): - resource = Resource.objects.filter(project=project).first() + resource = Resource.objects.current().filter(project=project).first() entity_max_order = entities.aggregate(Max("order"))["order__max"] try: # This line can purposefully cause an exception, and that diff --git a/pontoon/base/models/locale.py b/pontoon/base/models/locale.py index d2cd5cc12c..c2c98b9acf 100644 --- a/pontoon/base/models/locale.py +++ b/pontoon/base/models/locale.py @@ -58,7 +58,7 @@ def available(self): from pontoon.base.models.translated_resource import TranslatedResource return self.filter( - pk__in=TranslatedResource.objects.values_list("locale", flat=True) + pk__in=TranslatedResource.objects.current().values_list("locale", flat=True) ) def stats_data(self, project=None): @@ -115,7 +115,7 @@ class Locale(models.Model, AggregatedStats): def aggregated_stats_query(self): from pontoon.base.models.translated_resource import TranslatedResource - return TranslatedResource.objects.filter( + return TranslatedResource.objects.current().filter( locale=self, resource__project__disabled=False, resource__project__system_project=False, diff --git a/pontoon/base/models/project.py b/pontoon/base/models/project.py index 22b6d99272..1816a848dc 100644 --- a/pontoon/base/models/project.py +++ b/pontoon/base/models/project.py @@ -119,7 +119,7 @@ class Project(models.Model, AggregatedStats): def aggregated_stats_query(self): from pontoon.base.models.translated_resource import TranslatedResource - return TranslatedResource.objects.filter(resource__project=self) + return TranslatedResource.objects.current().filter(resource__project=self) name = models.CharField(max_length=128, unique=True) slug = models.SlugField(unique=True) diff --git a/pontoon/base/models/project_locale.py b/pontoon/base/models/project_locale.py index 586899f5d6..1742ed582f 100644 --- a/pontoon/base/models/project_locale.py +++ b/pontoon/base/models/project_locale.py @@ -74,7 +74,7 @@ class ProjectLocale(models.Model, AggregatedStats): def aggregated_stats_query(self): from pontoon.base.models.translated_resource import TranslatedResource - return TranslatedResource.objects.filter( + return TranslatedResource.objects.current().filter( locale=self.locale, resource__project=self.project ) diff --git a/pontoon/base/models/resource.py b/pontoon/base/models/resource.py index a794cf705c..8ec17a62d2 100644 --- a/pontoon/base/models/resource.py +++ b/pontoon/base/models/resource.py @@ -10,6 +10,9 @@ def obsolete(self, now): self.update(obsolete=True, date_obsoleted=now) Entity.objects.filter(resource__in=self).obsolete(now) + def current(self): + return self.filter(obsolete=False) + class Resource(models.Model): project = models.ForeignKey("Project", models.CASCADE, related_name="resources") diff --git a/pontoon/base/models/translated_resource.py b/pontoon/base/models/translated_resource.py index 6da5999280..ad1e8a0683 100644 --- a/pontoon/base/models/translated_resource.py +++ b/pontoon/base/models/translated_resource.py @@ -14,6 +14,9 @@ class TranslatedResourceQuerySet(models.QuerySet): + def current(self): + return self.filter(resource__obsolete=False) + def string_stats( self, user: User | None = None, diff --git a/pontoon/base/models/translation.py b/pontoon/base/models/translation.py index 453ecc4f4c..7a971969f7 100644 --- a/pontoon/base/models/translation.py +++ b/pontoon/base/models/translation.py @@ -23,9 +23,11 @@ class TranslationQuerySet(models.QuerySet): def translated_resources(self, locale): from pontoon.base.models.translated_resource import TranslatedResource - return TranslatedResource.objects.filter( - resource__entities__translation__in=self, locale=locale - ).distinct() + return ( + TranslatedResource.objects.current() + .filter(resource__entities__translation__in=self, locale=locale) + .distinct() + ) def authors(self): """ @@ -310,8 +312,10 @@ def save(self, failed_checks=None, *args, **kwargs): self.entity.reset_term_translation(self.locale) # We use get_or_create() instead of just get() to make it easier to test. - translatedresource, created = TranslatedResource.objects.get_or_create( - resource=self.entity.resource, locale=self.locale + translatedresource, created = ( + TranslatedResource.objects.current().get_or_create( + resource=self.entity.resource, locale=self.locale + ) ) # Update latest translation where necessary diff --git a/pontoon/base/signals.py b/pontoon/base/signals.py index d32b47ac8b..eff2c61ae0 100644 --- a/pontoon/base/signals.py +++ b/pontoon/base/signals.py @@ -28,7 +28,7 @@ def project_locale_removed(sender, **kwargs): project_locale = kwargs.get("instance", None) if project_locale is not None: - TranslatedResource.objects.filter( + TranslatedResource.objects.current().filter( resource__project=project_locale.project, locale=project_locale.locale ).delete() @@ -173,7 +173,7 @@ def add_locale_to_system_projects(sender, instance, created, **kwargs): projects = Project.objects.filter(system_project=True) for project in projects: ProjectLocale.objects.create(project=project, locale=instance) - for resource in project.resources.all(): + for resource in project.resources.current(): translated_resource = TranslatedResource.objects.create( resource=resource, locale=instance, @@ -189,7 +189,7 @@ def add_locale_to_terminology_project(sender, instance, created, **kwargs): if created: project = Project.objects.get(slug="terminology") ProjectLocale.objects.create(project=project, locale=instance) - for resource in project.resources.all(): + for resource in project.resources.current(): translated_resource = TranslatedResource.objects.create( resource=resource, locale=instance, diff --git a/pontoon/base/views.py b/pontoon/base/views.py index 7001e6abb0..f5efac0444 100755 --- a/pontoon/base/views.py +++ b/pontoon/base/views.py @@ -104,7 +104,11 @@ def locale_projects(request, locale): def locale_stats(request, locale): """Get locale stats used in All Resources part.""" locale = get_object_or_404(Locale, code=locale) - stats = TranslatedResource.objects.filter(locale=locale).string_stats(request.user) + stats = ( + TranslatedResource.objects.current() + .filter(locale=locale) + .string_stats(request.user) + ) stats["title"] = "all-resources" return JsonResponse([stats], safe=False) @@ -116,9 +120,11 @@ def locale_project_parts(request, locale, slug): try: locale = Locale.objects.get(code=locale) project = Project.objects.visible_for(request.user).get(slug=slug) - tr = TranslatedResource.objects.filter( - locale=locale, resource__project=project - ).distinct() + tr = ( + TranslatedResource.objects.current() + .filter(locale=locale, resource__project=project) + .distinct() + ) details = list( tr.annotate( title=F("resource__path"), @@ -192,7 +198,7 @@ def _get_entities_list(locale, preferred_source_locale, project, form): return JsonResponse( { "entities": Entity.map_entities(locale, preferred_source_locale, entities), - "stats": TranslatedResource.objects.query_stats( + "stats": TranslatedResource.objects.current().query_stats( project, form.cleaned_data["paths"], locale ), }, @@ -227,7 +233,7 @@ def _get_paginated_entities(locale, preferred_source_locale, project, form, enti requested_entity=requested_entity, ), "has_next": entities_page.has_next(), - "stats": TranslatedResource.objects.query_stats( + "stats": TranslatedResource.objects.current().query_stats( project, form.cleaned_data["paths"], locale ), }, diff --git a/pontoon/batch/views.py b/pontoon/batch/views.py index 04bbd697eb..0b0c3903a4 100644 --- a/pontoon/batch/views.py +++ b/pontoon/batch/views.py @@ -121,7 +121,7 @@ def batch_edit_translations(request): ) tr_pks = [tr.pk for tr in action_status["translated_resources"]] - TranslatedResource.objects.filter(pk__in=tr_pks).calculate_stats() + TranslatedResource.objects.current().filter(pk__in=tr_pks).calculate_stats() # Mark translations as changed active_translations.bulk_mark_changed() diff --git a/pontoon/insights/tasks.py b/pontoon/insights/tasks.py index fe493d048e..2cf02d7628 100644 --- a/pontoon/insights/tasks.py +++ b/pontoon/insights/tasks.py @@ -272,7 +272,8 @@ def count_projectlocale_stats() -> Iterable[dict[str, int]]: which may need a local copy if the behaviour here is modified. """ return ( - TranslatedResource.objects.filter( + TranslatedResource.objects.current() + .filter( resource__project__disabled=False, resource__project__system_project=False, resource__project__visibility="public", diff --git a/pontoon/localizations/views.py b/pontoon/localizations/views.py index 00b47130ed..0b76f70bcd 100644 --- a/pontoon/localizations/views.py +++ b/pontoon/localizations/views.py @@ -38,7 +38,7 @@ def localization(request, code, slug): get_object_or_404(ProjectLocale, locale=locale, project=project) - trans_res = TranslatedResource.objects.filter( + trans_res = TranslatedResource.objects.current().filter( locale=locale, resource__project=project ) @@ -75,7 +75,8 @@ def ajax_resources(request, code, slug): # Prefetch data needed for the latest activity column translatedresources = ( - TranslatedResource.objects.filter( + TranslatedResource.objects.current() + .filter( resource__project=project, locale=locale, resource__entities__obsolete=False, diff --git a/pontoon/messaging/management/commands/send_deadline_notifications.py b/pontoon/messaging/management/commands/send_deadline_notifications.py index c6c85b9356..a60d58757f 100644 --- a/pontoon/messaging/management/commands/send_deadline_notifications.py +++ b/pontoon/messaging/management/commands/send_deadline_notifications.py @@ -37,10 +37,14 @@ def handle(self, *args, **options): locales = [] for project_locale in project.project_locale.all(): - pl_stats = TranslatedResource.objects.filter( - locale=project_locale.locale, - resource__project=project_locale.project, - ).string_stats() + pl_stats = ( + TranslatedResource.objects.current() + .filter( + locale=project_locale.locale, + resource__project=project_locale.project, + ) + .string_stats() + ) if pl_stats["approved"] < pl_stats["total"]: locales.append(project_locale.locale) diff --git a/pontoon/pretranslation/tasks.py b/pontoon/pretranslation/tasks.py index 118a2af203..838b396247 100644 --- a/pontoon/pretranslation/tasks.py +++ b/pontoon/pretranslation/tasks.py @@ -65,7 +65,8 @@ def pretranslate(project: Project, paths: set[str] | None): # Fetch all available locale-resource pairs (TranslatedResource objects) tr_pairs = ( - TranslatedResource.objects.filter( + TranslatedResource.objects.current() + .filter( resource__project=project, locale__in=locales, ) @@ -196,9 +197,13 @@ def pretranslate(project: Project, paths: set[str] | None): # `operator.ior` is the '|' Python operator, which turns into a logical OR # when used between django ORM query objects. tr_query = reduce(operator.ior, tr_filter) - translatedresources = TranslatedResource.objects.filter(tr_query).annotate( - locale_resource=Concat( - "locale_id", V("-"), "resource_id", output_field=CharField() + translatedresources = ( + TranslatedResource.objects.current() + .filter(tr_query) + .annotate( + locale_resource=Concat( + "locale_id", V("-"), "resource_id", output_field=CharField() + ) ) ) translatedresources.calculate_stats() diff --git a/pontoon/projects/views.py b/pontoon/projects/views.py index c9ec64d885..987accf990 100644 --- a/pontoon/projects/views.py +++ b/pontoon/projects/views.py @@ -39,7 +39,7 @@ def projects(request): "projects/projects.html", { "projects": projects, - "all_projects_stats": TranslatedResource.objects.all().string_stats( + "all_projects_stats": TranslatedResource.objects.current().string_stats( request.user ), "project_stats": project_stats, @@ -57,7 +57,7 @@ def project(request, slug): return project project_locales = project.project_locale - project_tr = TranslatedResource.objects.filter(resource__project=project) + project_tr = TranslatedResource.objects.current().filter(resource__project=project) # Only include filtered teams if provided teams = request.GET.get("teams", "").split(",") diff --git a/pontoon/sync/core/entities.py b/pontoon/sync/core/entities.py index 8c21a878bf..63e650d2c8 100644 --- a/pontoon/sync/core/entities.py +++ b/pontoon/sync/core/entities.py @@ -342,12 +342,13 @@ def update_translated_resources( ) -> None: prev_tr_keys: set[tuple[int, int]] = set( (tr["resource_id"], tr["locale_id"]) - for tr in TranslatedResource.objects.filter(resource__project=project) + for tr in TranslatedResource.objects.current() + .filter(resource__project=project) .values("resource_id", "locale_id") .iterator() ) add_tr: list[TranslatedResource] = [] - for resource in Resource.objects.filter(project=project).iterator(): + for resource in Resource.objects.current().filter(project=project).iterator(): _, locales = paths.target(resource.path) for lc in locales: locale = locale_map.get(lc, None) @@ -372,7 +373,7 @@ def update_translated_resources( del_tr_q = Q() for resource_id, locale_id in prev_tr_keys: del_tr_q |= Q(resource_id=resource_id, locale_id=locale_id) - _, del_dict = TranslatedResource.objects.filter(del_tr_q).delete() + _, del_dict = TranslatedResource.objects.current().filter(del_tr_q).delete() del_count = del_dict.get("base.translatedresource", 0) str_tr = "translated resource" if del_count == 1 else "translated resources" log.info(f"[{project.slug}] Removed {del_count} {str_tr}") diff --git a/pontoon/sync/core/translations_from_repo.py b/pontoon/sync/core/translations_from_repo.py index 2abd2b5c46..a633837df4 100644 --- a/pontoon/sync/core/translations_from_repo.py +++ b/pontoon/sync/core/translations_from_repo.py @@ -115,9 +115,9 @@ def delete_removed_gettext_resources( Translation.objects.filter(entity__resource__project=project).filter( rm_t ).delete() - TranslatedResource.objects.filter(resource__project=project).filter( - rm_tr - ).delete() + TranslatedResource.objects.current().filter( + resource__project=project + ).filter(rm_tr).delete() return count @@ -178,9 +178,9 @@ def find_db_updates( resources: dict[str, Resource] = { res.path: res - for res in Resource.objects.filter( - project=project, path__in=resource_paths - ).iterator() + for res in Resource.objects.current() + .filter(project=project, path__in=resource_paths) + .iterator() } # Exclude translations for which DB & repo already match diff --git a/pontoon/teams/views.py b/pontoon/teams/views.py index dc55355e57..a0db5271e7 100644 --- a/pontoon/teams/views.py +++ b/pontoon/teams/views.py @@ -74,7 +74,7 @@ def teams(request): "teams/teams.html", { "locales": locales, - "all_locales_stats": TranslatedResource.objects.all().string_stats(), + "all_locales_stats": TranslatedResource.objects.current().string_stats(), "locale_stats": locale_stats, "form": form, "top_instances": get_top_instances(locales, locale_stats), @@ -92,8 +92,10 @@ def team(request, locale): if not visible_count: raise Http404 - locale_stats = TranslatedResource.objects.filter(locale=locale).string_stats( - request.user + locale_stats = ( + TranslatedResource.objects.current() + .filter(locale=locale) + .string_stats(request.user) ) return render( diff --git a/pontoon/terminology/models.py b/pontoon/terminology/models.py index 80c1371084..0301400fb0 100644 --- a/pontoon/terminology/models.py +++ b/pontoon/terminology/models.py @@ -6,12 +6,12 @@ def update_terminology_project_stats(): - resource = Resource.objects.get(project__slug="terminology") + resource = Resource.objects.current().get(project__slug="terminology") resource.total_strings = Entity.objects.filter( resource=resource, obsolete=False ).count() resource.save(update_fields=["total_strings"]) - TranslatedResource.objects.filter(resource=resource).calculate_stats() + TranslatedResource.objects.current().filter(resource=resource).calculate_stats() class TermQuerySet(models.QuerySet): @@ -126,7 +126,7 @@ def create_entity(self): - Entity.comment contains joint content of several fields: Term.part_of_speech. Term.definition. E.g.: Term.usage. """ - resource = Resource.objects.get(project__slug="terminology") + resource = Resource.objects.current().get(project__slug="terminology") entity, created = Entity.objects.get_or_create( string=self.text, diff --git a/pontoon/translations/views.py b/pontoon/translations/views.py index 8a0c5fecd3..aa3a282ddf 100644 --- a/pontoon/translations/views.py +++ b/pontoon/translations/views.py @@ -26,7 +26,7 @@ def _add_stats(response_data, resource, locale, stats): if stats: paths = [resource.path] if stats == "resource" else [] - response_data["stats"] = TranslatedResource.objects.query_stats( + response_data["stats"] = TranslatedResource.objects.current().query_stats( resource.project, paths, locale ) From 7e072ddccc6f469340dd1cd500ed77420f0cb09d Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:38:50 -0500 Subject: [PATCH 05/16] remove useless .current() calls + rename obsolete() to mark_as_obsolete() --- pontoon/base/models/resource.py | 2 +- pontoon/base/models/translation.py | 2 +- pontoon/sync/core/entities.py | 2 +- pontoon/terminology/models.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pontoon/base/models/resource.py b/pontoon/base/models/resource.py index 8ec17a62d2..fc138ad4c8 100644 --- a/pontoon/base/models/resource.py +++ b/pontoon/base/models/resource.py @@ -4,7 +4,7 @@ class ResourceQuerySet(models.QuerySet): - def obsolete(self, now): + def mark_as_obsolete(self, now): from pontoon.base.models.entity import Entity self.update(obsolete=True, date_obsoleted=now) diff --git a/pontoon/base/models/translation.py b/pontoon/base/models/translation.py index 7a971969f7..26f8fea799 100644 --- a/pontoon/base/models/translation.py +++ b/pontoon/base/models/translation.py @@ -313,7 +313,7 @@ def save(self, failed_checks=None, *args, **kwargs): # We use get_or_create() instead of just get() to make it easier to test. translatedresource, created = ( - TranslatedResource.objects.current().get_or_create( + TranslatedResource.objects.get_or_create( resource=self.entity.resource, locale=self.locale ) ) diff --git a/pontoon/sync/core/entities.py b/pontoon/sync/core/entities.py index 63e650d2c8..e70da4de53 100644 --- a/pontoon/sync/core/entities.py +++ b/pontoon/sync/core/entities.py @@ -118,7 +118,7 @@ def remove_resources( ) removed_db_paths = {res.path for res in removed_resources} if removed_db_paths: - removed_resources.obsolete(now) + removed_resources.mark_as_obsolete(now) rm_count = len(removed_db_paths) str_source_files = "source file" if rm_count == 1 else "source files" log.info( diff --git a/pontoon/terminology/models.py b/pontoon/terminology/models.py index 0301400fb0..26c6cf5815 100644 --- a/pontoon/terminology/models.py +++ b/pontoon/terminology/models.py @@ -126,7 +126,7 @@ def create_entity(self): - Entity.comment contains joint content of several fields: Term.part_of_speech. Term.definition. E.g.: Term.usage. """ - resource = Resource.objects.current().get(project__slug="terminology") + resource = Resource.objects.get(project__slug="terminology") entity, created = Entity.objects.get_or_create( string=self.text, From 62971b2957734c8caf4b1354b243b11bd99510b0 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:39:11 -0500 Subject: [PATCH 06/16] run make format --- pontoon/base/models/translation.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pontoon/base/models/translation.py b/pontoon/base/models/translation.py index 26f8fea799..d42c3b8789 100644 --- a/pontoon/base/models/translation.py +++ b/pontoon/base/models/translation.py @@ -312,10 +312,8 @@ def save(self, failed_checks=None, *args, **kwargs): self.entity.reset_term_translation(self.locale) # We use get_or_create() instead of just get() to make it easier to test. - translatedresource, created = ( - TranslatedResource.objects.get_or_create( - resource=self.entity.resource, locale=self.locale - ) + translatedresource, created = TranslatedResource.objects.get_or_create( + resource=self.entity.resource, locale=self.locale ) # Update latest translation where necessary From 79a85a523c41ea196ee83a9a1e877e185b8e10f6 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:50:34 -0500 Subject: [PATCH 07/16] add tests for contributor data + translate link --- pontoon/administration/tests/test_views.py | 26 ++++++++ pontoon/base/tests/managers/test_user.py | 69 ++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/pontoon/administration/tests/test_views.py b/pontoon/administration/tests/test_views.py index d846be4b24..6c7b4dd28b 100644 --- a/pontoon/administration/tests/test_views.py +++ b/pontoon/administration/tests/test_views.py @@ -380,6 +380,32 @@ def test_manage_project_strings_download_csv(client_superuser): assert "Mächt’ge".encode() in response.content +@pytest.mark.django_db +def test_manage_project_translate_link_excludes_obsolete_resources(client_superuser): + """Test that translate_locale is only set when non-obsolete resources exist.""" + locale_kl = LocaleFactory.create(code="kl", name="Klingon") + project = ProjectFactory.create( + data_source=Project.DataSource.DATABASE, + locales=[locale_kl], + repositories=[], + ) + + # add obsolete resource + ResourceFactory.create(project=project, obsolete=True) + + url = reverse("pontoon.admin.project", args=(project.slug,)) + response = client_superuser.get(url) + assert response.status_code == 200 + assert "translate_locale" not in response.context + + # add non-obsolete resource + ResourceFactory.create(project=project, obsolete=False) + + response = client_superuser.get(url) + assert response.status_code == 200 + assert response.context["translate_locale"] == "kl" + + @pytest.mark.django_db def test_project_add_locale(client_superuser): locale_kl = LocaleFactory.create(code="kl", name="Klingon") diff --git a/pontoon/base/tests/managers/test_user.py b/pontoon/base/tests/managers/test_user.py index 612ce1bab6..1f6766c72d 100644 --- a/pontoon/base/tests/managers/test_user.py +++ b/pontoon/base/tests/managers/test_user.py @@ -1,6 +1,9 @@ +from unittest.mock import MagicMock + import pytest from django.db.models import Q +from django.utils import timezone from pontoon.base.utils import aware_datetime from pontoon.contributors.utils import users_with_translations_counts @@ -322,3 +325,69 @@ def test_mgr_user_query_args_filtering( assert top_contribs[0].translations_approved_count == 11 assert top_contribs[0].translations_rejected_count == 0 assert top_contribs[0].translations_unapproved_count == 3 + + +@pytest.mark.django_db +def test_mgr_user_translation_counts_after_resource_removed( + resource_a, + locale_a, +): + """ + Tests that contributor translation counts remain unchanged after + a resource is removed via remove_resources. + + Translation counts should include translations from obsolete resources + since they represent the contributor's historical work. + """ + from pontoon.sync.core.entities import remove_resources + + contributor = UserFactory.create() + entities = EntityFactory.create_batch(size=12, resource=resource_a) + + batch_kwargs = sum( + [ + [dict(approved=True)] * 7, + [dict(approved=False, fuzzy=False, rejected=True)] * 3, + [dict(fuzzy=True)] * 2, + ], + [], + ) + + for i, kwa in enumerate(batch_kwargs): + TranslationFactory.create( + locale=locale_a, + user=contributor, + entity=entities[i], + approved=kwa.get("approved", False), + rejected=kwa.get("rejected", False), + fuzzy=kwa.get("fuzzy", False), + ) + + top_contribs = users_with_translations_counts() + assert len(top_contribs) == 1 + assert top_contribs[0] == contributor + assert top_contribs[0].translations_count == 12 + assert top_contribs[0].translations_approved_count == 7 + assert top_contribs[0].translations_rejected_count == 3 + assert top_contribs[0].translations_unapproved_count == 2 + + # Remove resource using remove_resources (simulates sync removing source file) + checkout = MagicMock() + checkout.path = "/path_1" + checkout.removed = [resource_a.path] + + paths = MagicMock() + paths.ref_root = "/path_1" + + remove_resources(resource_a.project, paths, checkout, timezone.now()) + + resource_a.refresh_from_db() + assert resource_a.obsolete is True + + top_contribs = users_with_translations_counts() + assert len(top_contribs) == 1 + assert top_contribs[0] == contributor + assert top_contribs[0].translations_count == 12 + assert top_contribs[0].translations_approved_count == 7 + assert top_contribs[0].translations_rejected_count == 3 + assert top_contribs[0].translations_unapproved_count == 2 From 1a52cd7f14991862e91d4cd7427387fdb1a30dc1 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:26:37 -0500 Subject: [PATCH 08/16] switch kl to tlh --- pontoon/administration/tests/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pontoon/administration/tests/test_views.py b/pontoon/administration/tests/test_views.py index 6c7b4dd28b..c8d61c4d28 100644 --- a/pontoon/administration/tests/test_views.py +++ b/pontoon/administration/tests/test_views.py @@ -383,7 +383,7 @@ def test_manage_project_strings_download_csv(client_superuser): @pytest.mark.django_db def test_manage_project_translate_link_excludes_obsolete_resources(client_superuser): """Test that translate_locale is only set when non-obsolete resources exist.""" - locale_kl = LocaleFactory.create(code="kl", name="Klingon") + locale_kl = LocaleFactory.create(code="tlh", name="Klingon") project = ProjectFactory.create( data_source=Project.DataSource.DATABASE, locales=[locale_kl], @@ -403,7 +403,7 @@ def test_manage_project_translate_link_excludes_obsolete_resources(client_superu response = client_superuser.get(url) assert response.status_code == 200 - assert response.context["translate_locale"] == "kl" + assert response.context["translate_locale"] == "tlh" @pytest.mark.django_db From 0321a490d3278511006ffd7e5c6ae5acd30ddf33 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:37:20 -0500 Subject: [PATCH 09/16] remove EntityQueryset.obsolete, replace with inline .update --- pontoon/base/models/entity.py | 7 ------- pontoon/base/models/resource.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pontoon/base/models/entity.py b/pontoon/base/models/entity.py index f72443e765..53323dc16e 100644 --- a/pontoon/base/models/entity.py +++ b/pontoon/base/models/entity.py @@ -251,13 +251,6 @@ def prefetch_entities_data(self, locale: Locale, preferred_source_locale: str): return entities - def obsolete(self, now): - self.update( - obsolete=True, - date_obsoleted=now, - section=None, - ) - class Entity(DirtyFieldsMixin, models.Model): resource = models.ForeignKey(Resource, models.CASCADE, related_name="entities") diff --git a/pontoon/base/models/resource.py b/pontoon/base/models/resource.py index fc138ad4c8..f9a80fdaec 100644 --- a/pontoon/base/models/resource.py +++ b/pontoon/base/models/resource.py @@ -4,11 +4,18 @@ class ResourceQuerySet(models.QuerySet): - def mark_as_obsolete(self, now): + def mark_as_obsolete(self, now=None): from pontoon.base.models.entity import Entity + if now is None: + now = timezone.now() + self.update(obsolete=True, date_obsoleted=now) - Entity.objects.filter(resource__in=self).obsolete(now) + Entity.objects.filter(resource__in=self).update( + obsolete=True, + date_obsoleted=now, + section=None, + ) def current(self): return self.filter(obsolete=False) From 4bbcca6b8268b20a8a7b3ae87bb09f79c4906f87 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:40:56 -0500 Subject: [PATCH 10/16] revert .po change --- pontoon/sync/tests/test_entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pontoon/sync/tests/test_entities.py b/pontoon/sync/tests/test_entities.py index 82bcf7ed9c..aaf5943576 100644 --- a/pontoon/sync/tests/test_entities.py +++ b/pontoon/sync/tests/test_entities.py @@ -65,7 +65,7 @@ def test_remove_resource(): build_file_tree( repo.checkout_path, { - "en-US": {"a.ftl": "", "b.po": ""}, + "en-US": {"a.ftl": "", "b.pot": ""}, "fr-Test": {"a.ftl": "", "b.po": "", "c.ftl": ""}, }, ) From 18cfadc5b009bf33697fa283c5ba628b1a9efa1b Mon Sep 17 00:00:00 2001 From: Jamie <164675620+functionzz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:37:45 -0500 Subject: [PATCH 11/16] Update pontoon/base/tests/managers/test_user.py Co-authored-by: Eemeli Aro --- pontoon/base/tests/managers/test_user.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pontoon/base/tests/managers/test_user.py b/pontoon/base/tests/managers/test_user.py index 1f6766c72d..d62630543c 100644 --- a/pontoon/base/tests/managers/test_user.py +++ b/pontoon/base/tests/managers/test_user.py @@ -344,13 +344,10 @@ def test_mgr_user_translation_counts_after_resource_removed( contributor = UserFactory.create() entities = EntityFactory.create_batch(size=12, resource=resource_a) - batch_kwargs = sum( - [ - [dict(approved=True)] * 7, - [dict(approved=False, fuzzy=False, rejected=True)] * 3, - [dict(fuzzy=True)] * 2, - ], - [], + batch_kwargs = ( + [dict(approved=True)] * 7 + + [dict(approved=False, fuzzy=False, rejected=True)] * 3 + + [dict(fuzzy=True)] * 2 ) for i, kwa in enumerate(batch_kwargs): From 7930f41e8340693abdbbe9be7636f3b705712e56 Mon Sep 17 00:00:00 2001 From: Jamie <164675620+functionzz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:43:50 -0500 Subject: [PATCH 12/16] Update pontoon/sync/tests/test_entities.py Co-authored-by: Eemeli Aro --- pontoon/sync/tests/test_entities.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pontoon/sync/tests/test_entities.py b/pontoon/sync/tests/test_entities.py index aaf5943576..28be5a3b67 100644 --- a/pontoon/sync/tests/test_entities.py +++ b/pontoon/sync/tests/test_entities.py @@ -84,15 +84,15 @@ def test_remove_resource(): assert sync_resources_from_repo( project, locale_map, mock_checkout, paths, now ) == (0, set(), {"c.ftl"}) - assert {res.path for res in project.resources.all()} == { + assert {res.path: res.obsolete for res in project.resources.all()} == { + "a.ftl": False, + "b.po": False, + "c.ftl": True, + } + assert {res.path for res in project.resources.current()} == { "a.ftl", "b.po", - "c.ftl", } - assert project.resources.count() == 3 - assert project.resources.get(path="c.ftl").obsolete is True - assert project.resources.get(path="a.ftl").obsolete is False - assert project.resources.get(path="b.po").obsolete is False assert TranslatedResource.objects.filter(resource=res_c).exists() assert Entity.objects.filter(pk=entity_c.pk).exists() assert Translation.objects.filter(pk=translation_c.pk).exists() From 31906de95007f080ac8a164522e3077b6a6b790d Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:44:17 -0500 Subject: [PATCH 13/16] run make format --- pontoon/base/tests/managers/test_user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pontoon/base/tests/managers/test_user.py b/pontoon/base/tests/managers/test_user.py index d62630543c..481d27fa2d 100644 --- a/pontoon/base/tests/managers/test_user.py +++ b/pontoon/base/tests/managers/test_user.py @@ -345,9 +345,9 @@ def test_mgr_user_translation_counts_after_resource_removed( entities = EntityFactory.create_batch(size=12, resource=resource_a) batch_kwargs = ( - [dict(approved=True)] * 7 + - [dict(approved=False, fuzzy=False, rejected=True)] * 3 + - [dict(fuzzy=True)] * 2 + [dict(approved=True)] * 7 + + [dict(approved=False, fuzzy=False, rejected=True)] * 3 + + [dict(fuzzy=True)] * 2 ) for i, kwa in enumerate(batch_kwargs): From 6bdbf6d43a9d4efa429e328e842b06450cca91c2 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:42:41 -0500 Subject: [PATCH 14/16] add TranslatedResource checks in test --- pontoon/sync/tests/test_entities.py | 34 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/pontoon/sync/tests/test_entities.py b/pontoon/sync/tests/test_entities.py index 28be5a3b67..67b7caa47b 100644 --- a/pontoon/sync/tests/test_entities.py +++ b/pontoon/sync/tests/test_entities.py @@ -42,7 +42,7 @@ def test_no_changes(): @pytest.mark.django_db -def test_remove_resource(): +def test_resource_obsoletion(): with TemporaryDirectory() as root: # Database setup settings.MEDIA_ROOT = root @@ -52,10 +52,18 @@ def test_remove_resource(): project = ProjectFactory.create( name="test-rm", locales=[locale], repositories=[repo] ) - ResourceFactory.create(project=project, path="a.ftl", format="fluent") - ResourceFactory.create(project=project, path="b.po", format="gettext") + res_a = ResourceFactory.create(project=project, path="a.ftl", format="fluent") + res_b = ResourceFactory.create(project=project, path="b.po", format="gettext") res_c = ResourceFactory.create(project=project, path="c.ftl", format="fluent") + entity_a = EntityFactory.create(resource=res_a, string="Window") + entity_b = EntityFactory.create(resource=res_b, string="Close") entity_c = EntityFactory.create(resource=res_c, string="Hello") + translation_a = TranslationFactory.create( + entity=entity_a, locale=locale, string="Fenetre" + ) + translation_b = TranslationFactory.create( + entity=entity_b, locale=locale, string="Ferme" + ) translation_c = TranslationFactory.create( entity=entity_c, locale=locale, string="Bonjour" ) @@ -70,6 +78,12 @@ def test_remove_resource(): }, ) + # check TranslatedResource objects before resource obsoletion + assert { + translated.resource.path + for translated in TranslatedResource.objects.current() + } == {"a.ftl", "b.po", "c.ftl", "common", "playground"} + # Paths setup mock_checkout = Mock( Checkout, @@ -80,7 +94,7 @@ def test_remove_resource(): ) paths = find_paths(project, Checkouts(mock_checkout, mock_checkout)) - # Test + # Test sync_resources_from_repo assert sync_resources_from_repo( project, locale_map, mock_checkout, paths, now ) == (0, set(), {"c.ftl"}) @@ -94,8 +108,18 @@ def test_remove_resource(): "b.po", } assert TranslatedResource.objects.filter(resource=res_c).exists() + assert { + translated.resource.path + for translated in TranslatedResource.objects.current() + } == {"a.ftl", "b.po", "common", "playground"} assert Entity.objects.filter(pk=entity_c.pk).exists() - assert Translation.objects.filter(pk=translation_c.pk).exists() + + assert ( + Translation.objects.filter( + pk__in=[translation_a.pk, translation_b.pk, translation_c.pk] + ).count() + == 3 + ) @pytest.mark.django_db From 39d460d9d94b5502a70dc0e9a5044e7239c07914 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:00:58 -0400 Subject: [PATCH 15/16] add obsoletion filter for stats_data --- pontoon/base/models/locale.py | 6 +++++- pontoon/base/models/project_locale.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pontoon/base/models/locale.py b/pontoon/base/models/locale.py index c2c98b9acf..9eff3c806a 100644 --- a/pontoon/base/models/locale.py +++ b/pontoon/base/models/locale.py @@ -63,12 +63,16 @@ def available(self): def stats_data(self, project=None): if project is not None: - query = self.filter(translatedresources__resource__project=project) + query = self.filter( + translatedresources__resource__project=project, + translatedresources__resource__obsolete=False, + ) else: query = self.filter( translatedresources__resource__project__disabled=False, translatedresources__resource__project__system_project=False, translatedresources__resource__project__visibility="public", + translatedresources__resource__obsolete=False, ) return query.annotate( diff --git a/pontoon/base/models/project_locale.py b/pontoon/base/models/project_locale.py index 1742ed582f..1d06e4d34f 100644 --- a/pontoon/base/models/project_locale.py +++ b/pontoon/base/models/project_locale.py @@ -32,7 +32,8 @@ def visible(self): def stats_data(self, project=None, locale=None): if project: query = self.filter( - locale__translatedresources__resource__project=project + locale__translatedresources__resource__project=project, + locale__translatedresources__resource__obsolete=False, ).prefetch_related("locale") tr = "locale__translatedresources" elif locale: @@ -41,6 +42,7 @@ def stats_data(self, project=None, locale=None): project__disabled=False, project__system_project=False, project__visibility="public", + project__resources__obsolete=False, ).prefetch_related("project") tr = "project__resources__translatedresources" return query.annotate( From 72668e160a1f0fcb96c6f5e9ca867969619ec0d7 Mon Sep 17 00:00:00 2001 From: functionzz <164675620+functionzz@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:28:16 -0400 Subject: [PATCH 16/16] obsolete entity cannot be translated --- pontoon/translations/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pontoon/translations/views.py b/pontoon/translations/views.py index aa3a282ddf..d9d9fa2251 100644 --- a/pontoon/translations/views.py +++ b/pontoon/translations/views.py @@ -75,6 +75,12 @@ def create_translation(request): resource = entity.resource project = resource.project + if entity.obsolete: + return JsonResponse( + {"status": False, "message": "Forbidden: This string is obsolete."}, + status=403, + ) + # Read-only translations cannot saved if utils.readonly_exists(project, locale): return JsonResponse(