diff --git a/pontoon/administration/templates/admin.html b/pontoon/administration/templates/admin.html index b771f4308..b9a49539a 100644 --- a/pontoon/administration/templates/admin.html +++ b/pontoon/administration/templates/admin.html @@ -27,7 +27,7 @@ {% set main_link = url('pontoon.admin.project', project.slug) %} {% set chart_link = url('pontoon.admin.project', project.slug) %} {% set latest_activity = project.get_latest_activity() %} - {% set chart = project.get_chart() %} + {% set chart = project_stats.get(project.id, {'total': 0}) %} {{ ProjectList.item(project, main_link, chart_link, latest_activity, chart) }} {% endfor %} diff --git a/pontoon/administration/views.py b/pontoon/administration/views.py index ae34bb48c..32657ac6f 100644 --- a/pontoon/administration/views.py +++ b/pontoon/administration/views.py @@ -49,7 +49,15 @@ def admin(request): .order_by("name") ) - return render(request, "admin.html", {"admin": True, "projects": projects}) + return render( + request, + "admin.html", + { + "admin": True, + "projects": projects, + "project_stats": projects.stats_data(), + }, + ) @login_required(redirect_field_name="", login_url="/403") diff --git a/pontoon/base/aggregated_stats.py b/pontoon/base/aggregated_stats.py index de1c9424e..8c788a089 100644 --- a/pontoon/base/aggregated_stats.py +++ b/pontoon/base/aggregated_stats.py @@ -1,5 +1,3 @@ -import math - from functools import cached_property @@ -42,7 +40,7 @@ def unreviewed_strings(self) -> int: return self._stats["unreviewed"] @property - def missing_strings(self): + def missing_strings(self) -> int: return ( self.total_strings - self.approved_strings @@ -51,42 +49,42 @@ def missing_strings(self): - self.strings_with_warnings ) + @property + def complete(self) -> bool: + return ( + self.total_strings + == self.approved_strings + + self.pretranslated_strings + + self.strings_with_warnings + ) + -def get_completed_percent(obj): - if not obj.total_strings: - return 0 - completed_strings = ( - obj.approved_strings + obj.pretranslated_strings + obj.strings_with_warnings - ) - return completed_strings / obj.total_strings * 100 - - -def get_chart_dict(obj: "AggregatedStats"): - """Get chart data dictionary""" - if ts := obj.total_strings: - return { - "total": ts, - "approved": obj.approved_strings, - "pretranslated": obj.pretranslated_strings, - "errors": obj.strings_with_errors, - "warnings": obj.strings_with_warnings, - "unreviewed": obj.unreviewed_strings, - "approved_share": round(obj.approved_strings / ts * 100), - "pretranslated_share": round(obj.pretranslated_strings / ts * 100), - "errors_share": round(obj.strings_with_errors / ts * 100), - "warnings_share": round(obj.strings_with_warnings / ts * 100), - "unreviewed_share": round(obj.unreviewed_strings / ts * 100), - "completion_percent": int(math.floor(get_completed_percent(obj))), - } - - -def get_top_instances(qs): +def get_top_instances(qs, stats: dict[int, dict[str, int]]) -> dict[str, object] | None: """ Get top instances in the queryset. """ + + if not stats: + return None + + def _missing(x: tuple[int, dict[str, int]]) -> int: + _, d = x + return ( + d["total"] + - d["approved"] + - d["pretranslated"] + - d["errors"] + - d["warnings"] + ) + + max_total_id = max(stats.items(), key=lambda x: x[1]["total"])[0] + max_approved_id = max(stats.items(), key=lambda x: x[1]["approved"])[0] + max_suggestions_id = max(stats.items(), key=lambda x: x[1]["unreviewed"])[0] + max_missing_id = max(stats.items(), key=_missing)[0] + return { - "most_strings": sorted(qs, key=lambda x: x.total_strings)[-1], - "most_translations": sorted(qs, key=lambda x: x.approved_strings)[-1], - "most_suggestions": sorted(qs, key=lambda x: x.unreviewed_strings)[-1], - "most_missing": sorted(qs, key=lambda x: x.missing_strings)[-1], + "most_strings": next(row for row in qs if row.id == max_total_id), + "most_translations": next(row for row in qs if row.id == max_approved_id), + "most_suggestions": next(row for row in qs if row.id == max_suggestions_id), + "most_missing": next(row for row in qs if row.id == max_missing_id), } diff --git a/pontoon/base/models/locale.py b/pontoon/base/models/locale.py index 73aaae2fe..83d8dfa01 100644 --- a/pontoon/base/models/locale.py +++ b/pontoon/base/models/locale.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import Group from django.core.exceptions import ValidationError from django.db import models -from django.db.models import Prefetch +from django.db.models import Prefetch, Sum from pontoon.base.aggregated_stats import AggregatedStats @@ -79,6 +79,34 @@ def prefetch_project_locale(self, project): ) ) + def stats_data(self, project=None) -> dict[int, dict[str, int]]: + """Mapping of locale `id` to dict with counts.""" + if project is not None: + query = self.filter(translatedresources__resource__project=project) + else: + query = self.filter( + translatedresources__resource__project__disabled=False, + translatedresources__resource__project__system_project=False, + translatedresources__resource__project__visibility="public", + ) + data = query.annotate( + total=Sum("translatedresources__total_strings", default=0), + approved=Sum("translatedresources__approved_strings", default=0), + pretranslated=Sum("translatedresources__pretranslated_strings", default=0), + errors=Sum("translatedresources__strings_with_errors", default=0), + warnings=Sum("translatedresources__strings_with_warnings", default=0), + unreviewed=Sum("translatedresources__unreviewed_strings", default=0), + ).values( + "id", + "total", + "approved", + "pretranslated", + "errors", + "warnings", + "unreviewed", + ) + return {row["id"]: row for row in data if row["total"]} + class Locale(models.Model, AggregatedStats): @property @@ -366,11 +394,6 @@ def get_latest_activity(self, project=None): return ProjectLocale.get_latest_activity(self, project) - def get_chart(self, project=None): - from pontoon.base.models.project_locale import ProjectLocale - - return ProjectLocale.get_chart(self, project) - def save(self, *args, **kwargs): old = Locale.objects.get(pk=self.pk) if self.pk else None super().save(*args, **kwargs) diff --git a/pontoon/base/models/project.py b/pontoon/base/models/project.py index cffdbf53a..32465c9f2 100644 --- a/pontoon/base/models/project.py +++ b/pontoon/base/models/project.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.db import models -from django.db.models import Prefetch +from django.db.models import Prefetch, Sum from django.db.models.manager import BaseManager from django.utils import timezone @@ -86,6 +86,32 @@ def prefetch_project_locale(self, locale): ) ) + def stats_data(self, locale=None) -> dict[int, dict[str, int]]: + """Mapping of project `id` to dict with counts.""" + query = ( + self + if locale is None + else self.filter(resources__translatedresources__locale=locale) + ) + tr = "resources__translatedresources" + data = query.annotate( + total=Sum(f"{tr}__total_strings", default=0), + approved=Sum(f"{tr}__approved_strings", default=0), + pretranslated=Sum(f"{tr}__pretranslated_strings", default=0), + errors=Sum(f"{tr}__strings_with_errors", default=0), + warnings=Sum(f"{tr}__strings_with_warnings", default=0), + unreviewed=Sum(f"{tr}__unreviewed_strings", default=0), + ).values( + "id", + "total", + "approved", + "pretranslated", + "errors", + "warnings", + "unreviewed", + ) + return {row["id"]: row for row in data if row["total"]} + class Project(models.Model, AggregatedStats): @property @@ -252,11 +278,6 @@ def get_latest_activity(self, locale=None): return ProjectLocale.get_latest_activity(self, locale) - def get_chart(self, locale=None): - from pontoon.base.models.project_locale import ProjectLocale - - return ProjectLocale.get_chart(self, locale) - @property def avg_string_count(self): return int(self.total_strings / self.enabled_locales) diff --git a/pontoon/base/models/project_locale.py b/pontoon/base/models/project_locale.py index 36ceab48e..40496d2ef 100644 --- a/pontoon/base/models/project_locale.py +++ b/pontoon/base/models/project_locale.py @@ -2,7 +2,7 @@ from django.db import models from pontoon.base import utils -from pontoon.base.aggregated_stats import AggregatedStats, get_chart_dict +from pontoon.base.aggregated_stats import AggregatedStats from pontoon.base.models.locale import Locale from pontoon.base.models.project import Project @@ -111,34 +111,3 @@ def get_latest_activity(cls, self, extra=None): latest_translation = project_locale.latest_translation return latest_translation.latest_activity if latest_translation else None - - @classmethod - def get_chart(cls, self, extra=None): - """ - Get chart for project, locale or combination of both. - - :param self: object to get data for, - instance of Project or Locale - :param extra: extra filter to be used, - instance of Project or Locale - """ - chart = None - - if getattr(self, "fetched_project_locale", None): - if self.fetched_project_locale: - chart = get_chart_dict(self.fetched_project_locale[0]) - - elif extra is None: - chart = get_chart_dict(self) - - else: - project = self if isinstance(self, Project) else extra - locale = self if isinstance(self, Locale) else extra - project_locale = utils.get_object_or_none( - ProjectLocale, project=project, locale=locale - ) - - if project_locale is not None: - chart = get_chart_dict(project_locale) - - return chart diff --git a/pontoon/base/models/translated_resource.py b/pontoon/base/models/translated_resource.py index 0f82cbc3c..ef10712b0 100644 --- a/pontoon/base/models/translated_resource.py +++ b/pontoon/base/models/translated_resource.py @@ -113,6 +113,16 @@ def count_total_strings(self): total += (self.locale.nplurals - 1) * plural_count return total + def stats_data(self) -> dict[str, int]: + return { + "total": self.total_strings, + "approved": self.approved_strings, + "pretranslated": self.pretranslated_strings, + "errors": self.strings_with_errors, + "warnings": self.strings_with_warnings, + "unreviewed": self.unreviewed_strings, + } + def adjust_stats( self, before: dict[str, int], after: dict[str, int], tr_created: bool ): diff --git a/pontoon/base/templates/widgets/progress_chart.html b/pontoon/base/templates/widgets/progress_chart.html index 8a0dc33ac..401e3adb2 100644 --- a/pontoon/base/templates/widgets/progress_chart.html +++ b/pontoon/base/templates/widgets/progress_chart.html @@ -1,13 +1,13 @@ {% macro span(chart, link, link_parameter=False, has_params=False) %} {% if chart != None %}