Repository: mozilla/pontoon

Issue:
Optimize logic to retrieve Resources in the Localization dashboard
The code that powers the Resources tab in the Localization dashboard should reuse the code used by other dashboards to calculate stats. We should also be able to reduce the number of DB requests.


Discussion:

Resolving patch:
diff --git a/pontoon/localizations/views.py b/pontoon/localizations/views.py
--- a/pontoon/localizations/views.py
+++ b/pontoon/localizations/views.py
@@ -1,4 +1,3 @@
-import math
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db.models import Q
@@ -42,7 +41,15 @@ def localization(request, code, slug):
         project=project,
     )
 
-    resource_count = len(locale.parts_stats(project)) - 1
+    resource_count = (
+        TranslatedResource.objects.filter(
+            resource__project=project,
+            locale=locale,
+            resource__entities__obsolete=False,
+        )
+        .distinct()
+        .count()
+    )
 
     return render(
         request,
@@ -73,65 +80,33 @@ def ajax_resources(request, code, slug):
     # Check if ProjectLocale exists
     get_object_or_404(ProjectLocale, locale=locale, project=project)
 
-    # Amend the parts dict with latest activity info.
-    translatedresources_qs = TranslatedResource.objects.filter(
-        resource__project=project, locale=locale
-    ).prefetch_related("resource", "latest_translation__user")
+    # Prefetch data needed for the latest activity column
+    translatedresources = (
+        TranslatedResource.objects.filter(
+            resource__project=project,
+            locale=locale,
+            resource__entities__obsolete=False,
+        )
+        .order_by("resource__path")
+        .prefetch_related(
+            "resource", "latest_translation__user", "latest_translation__approved_user"
+        )
+        .distinct()
+    )
 
-    if not len(translatedresources_qs):
+    if not len(translatedresources):
         raise Http404
 
-    translatedresources = {s.resource.path: s for s in translatedresources_qs}
-    translatedresources = dict(list(translatedresources.items()))
-    parts = locale.parts_stats(project)
-
     resource_priority_map = project.resource_priority_map()
 
-    for part in parts:
-        part["resource__priority"] = resource_priority_map.get(part["title"], None)
-
-        translatedresource = translatedresources.get(part["title"], None)
-        if translatedresource and translatedresource.latest_translation:
-            part[
-                "latest_activity"
-            ] = translatedresource.latest_translation.latest_activity
-        else:
-            part["latest_activity"] = None
-
-        part["chart"] = {
-            "unreviewed_strings": part["unreviewed_strings"],
-            "pretranslated_strings": part["pretranslated_strings"],
-            "strings_with_errors": part["strings_with_errors"],
-            "strings_with_warnings": part["strings_with_warnings"],
-            "total_strings": part["resource__total_strings"],
-            "approved_strings": part["approved_strings"],
-            "approved_share": round(
-                part["approved_strings"] / part["resource__total_strings"] * 100
-            ),
-            "unreviewed_share": round(
-                part["unreviewed_strings"] / part["resource__total_strings"] * 100
-            ),
-            "pretranslated_share": round(
-                part["pretranslated_strings"] / part["resource__total_strings"] * 100
-            ),
-            "errors_share": round(
-                part["strings_with_errors"] / part["resource__total_strings"] * 100
-            ),
-            "warnings_share": round(
-                part["strings_with_warnings"] / part["resource__total_strings"] * 100
-            ),
-            "completion_percent": int(
-                math.floor(
-                    (
-                        part["approved_strings"]
-                        + part["pretranslated_strings"]
-                        + part["strings_with_warnings"]
-                    )
-                    / part["resource__total_strings"]
-                    * 100
-                )
-            ),
-        }
+    for tr in translatedresources:
+        tr.title = tr.resource.path
+        tr.deadline = tr.resource.deadline
+        tr.priority = resource_priority_map.get(tr.resource.path, None)
+        tr.latest_activity = (
+            tr.latest_translation.latest_activity if tr.latest_translation else None
+        )
+        tr.chart = TranslatedResource.get_chart_dict(tr)
 
     return render(
         request,
@@ -139,9 +114,9 @@ def ajax_resources(request, code, slug):
         {
             "locale": locale,
             "project": project,
-            "resources": parts,
-            "deadline": any(part["resource__deadline"] for part in parts),
-            "priority": any(part["resource__priority"] for part in parts),
+            "resources": translatedresources,
+            "deadline": any(tr.resource.deadline for tr in translatedresources),
+            "priority": any(tr.priority for tr in translatedresources),
         },
     )
 


Test patch:
diff --git a/pontoon/localizations/tests/test_views.py b/pontoon/localizations/tests/test_views.py
--- a/pontoon/localizations/tests/test_views.py
+++ b/pontoon/localizations/tests/test_views.py
@@ -5,8 +5,8 @@
 from django.urls import reverse
 from django.shortcuts import render
 
-from pontoon.base.models import Locale
 from pontoon.base.tests import (
+    EntityFactory,
     ResourceFactory,
     TranslationFactory,
     TranslatedResourceFactory,
@@ -16,44 +16,22 @@
 
 @pytest.mark.django_db
 @patch("pontoon.localizations.views.render", wraps=render)
-@patch.object(Locale, "parts_stats")
-def test_latest_activity(mock_parts_stats, mock_render, client, project_a, locale_a):
+def test_ajax_resources(mock_render, client, project_a, locale_a):
     """Ensure that the latest_activity field is added to parts."""
     ProjectLocaleFactory.create(locale=locale_a, project=project_a)
+
     resource = ResourceFactory.create(project=project_a, path="has/stats.po")
     resource2 = ResourceFactory.create(project=project_a, path="has/stats2.po")
 
-    translation = TranslationFactory.create(entity__resource=resource, locale=locale_a)
-    TranslatedResourceFactory.create(
+    entity = EntityFactory.create(resource=resource)
+    EntityFactory.create(resource=resource2)
+    translation = TranslationFactory.create(entity=entity, locale=locale_a)
+
+    tr = TranslatedResourceFactory.create(
         resource=resource2, locale=locale_a, latest_translation=translation
     )
-
-    mock_parts_stats.return_value = [
-        {
-            "title": "has/stats.po",
-            "resource__path": "has/stats.po",
-            "resource__total_strings": 1,
-            "approved_strings": 0,
-            "unreviewed_strings": 1,
-            "pretranslated_strings": 0,
-            "strings_with_warnings": 0,
-            "strings_with_errors": 0,
-            "resource__deadline": None,
-            "resource__priority": None,
-        },
-        {
-            "title": "no/stats.po",
-            "resource__path": "no/stats.po",
-            "resource__total_strings": 1,
-            "approved_strings": 0,
-            "unreviewed_strings": 0,
-            "pretranslated_strings": 0,
-            "strings_with_warnings": 0,
-            "strings_with_errors": 0,
-            "resource__deadline": None,
-            "resource__priority": None,
-        },
-    ]
+    tr.total_strings = 1
+    tr.save()
 
     client.get(
         reverse(
@@ -65,59 +43,23 @@ def test_latest_activity(mock_parts_stats, mock_render, client, project_a, local
 
     ctx = mock_render.call_args[0][2]
 
-    assert ctx["resources"] == [
-        {
-            "latest_activity": translation.latest_activity,
-            "title": "has/stats.po",
-            "resource__path": "has/stats.po",
-            "resource__deadline": None,
-            "resource__priority": None,
-            "resource__total_strings": 1,
-            "approved_strings": 0,
-            "unreviewed_strings": 1,
-            "pretranslated_strings": 0,
-            "strings_with_errors": 0,
-            "strings_with_warnings": 0,
-            "chart": {
-                "pretranslated_strings": 0,
-                "total_strings": 1,
-                "approved_strings": 0,
-                "unreviewed_strings": 1,
-                "strings_with_errors": 0,
-                "strings_with_warnings": 0,
-                "approved_share": 0.0,
-                "unreviewed_share": 100.0,
-                "pretranslated_share": 0.0,
-                "warnings_share": 0.0,
-                "errors_share": 0.0,
-                "completion_percent": 0,
-            },
-        },
-        {
-            "latest_activity": None,
-            "title": "no/stats.po",
-            "resource__path": "no/stats.po",
-            "resource__deadline": None,
-            "resource__priority": None,
-            "resource__total_strings": 1,
-            "approved_strings": 0,
-            "unreviewed_strings": 0,
-            "pretranslated_strings": 0,
-            "strings_with_errors": 0,
-            "strings_with_warnings": 0,
-            "chart": {
-                "pretranslated_strings": 0,
-                "total_strings": 1,
-                "approved_strings": 0,
-                "unreviewed_strings": 0,
-                "strings_with_errors": 0,
-                "strings_with_warnings": 0,
-                "approved_share": 0.0,
-                "unreviewed_share": 0.0,
-                "pretranslated_share": 0.0,
-                "warnings_share": 0.0,
-                "errors_share": 0.0,
-                "completion_percent": 0,
-            },
-        },
-    ]
+    assert len(ctx["resources"]) == 2
+
+    assert ctx["resources"][0].title == "has/stats2.po"
+    assert ctx["resources"][0].deadline is None
+    assert ctx["resources"][0].priority is None
+    assert ctx["resources"][0].latest_activity == translation.latest_activity
+    assert ctx["resources"][0].chart == {
+        "pretranslated_strings": 0,
+        "total_strings": 1,
+        "approved_strings": 0,
+        "unreviewed_strings": 0,
+        "strings_with_errors": 0,
+        "strings_with_warnings": 0,
+        "approved_share": 0.0,
+        "unreviewed_share": 0.0,
+        "pretranslated_share": 0.0,
+        "warnings_share": 0.0,
+        "errors_share": 0.0,
+        "completion_percent": 0,
+    }