Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pontoon/administration/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
26 changes: 26 additions & 0 deletions pontoon/administration/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="tlh", 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"] == "tlh"


@pytest.mark.django_db
def test_project_add_locale(client_superuser):
locale_kl = LocaleFactory.create(code="kl", name="Klingon")
Expand Down
6 changes: 3 additions & 3 deletions pontoon/administration/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions pontoon/base/models/locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,21 @@ 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):
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(
Expand Down Expand Up @@ -115,7 +119,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,
Expand Down
2 changes: 1 addition & 1 deletion pontoon/base/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions pontoon/base/models/project_locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand Down Expand Up @@ -74,7 +76,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
)

Expand Down
20 changes: 20 additions & 0 deletions pontoon/base/models/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
from django.utils import timezone


class ResourceQuerySet(models.QuerySet):
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).update(
obsolete=True,
date_obsoleted=now,
section=None,
)

def current(self):
return self.filter(obsolete=False)


class Resource(models.Model):
project = models.ForeignKey("Project", models.CASCADE, related_name="resources")
path = models.TextField() # Path to localization file
Expand Down Expand Up @@ -42,6 +60,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,
Expand Down
3 changes: 3 additions & 0 deletions pontoon/base/models/translated_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions pontoon/base/models/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
6 changes: 3 additions & 3 deletions pontoon/base/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
66 changes: 66 additions & 0 deletions pontoon/base/tests/managers/test_user.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -322,3 +325,66 @@ 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 = (
[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
18 changes: 12 additions & 6 deletions pontoon/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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"),
Expand Down Expand Up @@ -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
),
},
Expand Down Expand Up @@ -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
),
},
Expand Down
2 changes: 1 addition & 1 deletion pontoon/batch/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion pontoon/insights/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions pontoon/localizations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading