diff --git a/pontoon/base/migrations/0053_alter_translation_index_together.py b/pontoon/base/migrations/0053_alter_translation_index_together.py
new file mode 100644
index 0000000000..8f6bde78f1
--- /dev/null
+++ b/pontoon/base/migrations/0053_alter_translation_index_together.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.2.15 on 2024-02-20 10:51
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("base", "0052_rename_logged_in_users"),
+ ]
+
+ operations = [
+ migrations.AlterIndexTogether(
+ name="translation",
+ index_together={
+ ("locale", "user", "entity"),
+ ("entity", "user", "approved", "pretranslated"),
+ ("entity", "locale", "fuzzy"),
+ ("date", "locale"),
+ ("entity", "locale", "approved"),
+ ("entity", "locale", "pretranslated"),
+ ("approved_date", "locale"),
+ },
+ ),
+ ]
diff --git a/pontoon/base/models.py b/pontoon/base/models.py
index 1c09b0f7fb..e12016083c 100644
--- a/pontoon/base/models.py
+++ b/pontoon/base/models.py
@@ -531,6 +531,25 @@ class AggregatedStats(models.Model):
class Meta:
abstract = True
+ @classmethod
+ def get_chart_dict(cls, obj):
+ """Get chart data dictionary"""
+ if obj.total_strings:
+ return {
+ "total_strings": obj.total_strings,
+ "approved_strings": obj.approved_strings,
+ "pretranslated_strings": obj.pretranslated_strings,
+ "strings_with_errors": obj.strings_with_errors,
+ "strings_with_warnings": obj.strings_with_warnings,
+ "unreviewed_strings": obj.unreviewed_strings,
+ "approved_share": round(obj.approved_percent),
+ "pretranslated_share": round(obj.pretranslated_percent),
+ "errors_share": round(obj.errors_percent),
+ "warnings_share": round(obj.warnings_percent),
+ "unreviewed_share": round(obj.unreviewed_percent),
+ "completion_percent": int(math.floor(obj.completed_percent)),
+ }
+
@classmethod
def get_stats_sum(cls, qs):
"""
@@ -1837,25 +1856,6 @@ def get_chart(cls, self, extra=None):
return chart
- @classmethod
- def get_chart_dict(cls, obj):
- """Get chart data dictionary"""
- if obj.total_strings:
- return {
- "total_strings": obj.total_strings,
- "approved_strings": obj.approved_strings,
- "pretranslated_strings": obj.pretranslated_strings,
- "strings_with_errors": obj.strings_with_errors,
- "strings_with_warnings": obj.strings_with_warnings,
- "unreviewed_strings": obj.unreviewed_strings,
- "approved_share": round(obj.approved_percent),
- "pretranslated_share": round(obj.pretranslated_percent),
- "errors_share": round(obj.errors_percent),
- "warnings_share": round(obj.warnings_percent),
- "unreviewed_share": round(obj.unreviewed_percent),
- "completion_percent": int(math.floor(obj.completed_percent)),
- }
-
def aggregate_stats(self):
TranslatedResource.objects.filter(
resource__project=self.project,
@@ -3284,6 +3284,7 @@ class Meta:
("entity", "locale", "fuzzy"),
("locale", "user", "entity"),
("date", "locale"),
+ ("approved_date", "locale"),
)
constraints = [
models.UniqueConstraint(
diff --git a/pontoon/localizations/views.py b/pontoon/localizations/views.py
index 7c9b7b46bf..4e92b25378 100644
--- a/pontoon/localizations/views.py
+++ b/pontoon/localizations/views.py
@@ -1,5 +1,4 @@
import math
-from operator import attrgetter
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q
@@ -20,7 +19,7 @@
)
from pontoon.contributors.views import ContributorsMixin
from pontoon.insights.utils import get_insights
-from pontoon.tags.utils import TagsTool
+from pontoon.tags.utils import Tags
def localization(request, code, slug):
@@ -159,13 +158,7 @@ def ajax_tags(request, code, slug):
if not project.tags_enabled:
raise Http404
- tags_tool = TagsTool(
- locales=[locale],
- projects=[project],
- priority=True,
- )
-
- tags = sorted(tags_tool, key=attrgetter("priority"), reverse=True)
+ tags = Tags(project=project, locale=locale).get()
return render(
request,
diff --git a/pontoon/projects/templates/projects/includes/teams.html b/pontoon/projects/templates/projects/includes/teams.html
index e07ac191ef..2e8d053965 100644
--- a/pontoon/projects/templates/projects/includes/teams.html
+++ b/pontoon/projects/templates/projects/includes/teams.html
@@ -19,10 +19,12 @@
{% set locale_projects = project.available_locales_list() %}
{% for locale in locales %}
+ {% if not tag %}
{% set main_link = url('pontoon.projects.project', project.slug) %}
{% set chart_link = url('pontoon.translate', locale.code, project.slug, 'all-resources') %}
{% set latest_activity = locale.get_latest_activity(project) %}
{% set chart = locale.get_chart(project) %}
+ {% endif %}
{% if locale.code in locale_projects %}
{% set class = 'limited' %}
{% if chart %}
@@ -35,6 +37,8 @@
{% if tag %}
{% set main_link = url('pontoon.projects.project', project.slug) + '?tag=' + tag.slug %}
{% set chart_link = url('pontoon.translate', locale.code, project.slug, 'all-resources') + '?tag=' + tag.slug %}
+ {% set latest_activity = locale.latest_activity %}
+ {% set chart = locale.chart %}
{% if chart %}
{% set main_link = chart_link %}
{% endif %}
diff --git a/pontoon/projects/views.py b/pontoon/projects/views.py
index 87d47b2220..d40265beb0 100644
--- a/pontoon/projects/views.py
+++ b/pontoon/projects/views.py
@@ -1,5 +1,4 @@
import uuid
-from operator import attrgetter
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
@@ -19,7 +18,7 @@
from pontoon.contributors.views import ContributorsMixin
from pontoon.insights.utils import get_insights
from pontoon.projects import forms
-from pontoon.tags.utils import TagsTool
+from pontoon.tags.utils import Tags
def projects(request):
@@ -107,12 +106,7 @@ def ajax_tags(request, slug):
if not project.tags_enabled:
raise Http404
- tags_tool = TagsTool(
- projects=[project],
- priority=True,
- )
-
- tags = sorted(tags_tool, key=attrgetter("priority"), reverse=True)
+ tags = Tags(project=project).get()
return render(
request,
diff --git a/pontoon/tags/admin/__init__.py b/pontoon/tags/admin/__init__.py
index e69de29bb2..492a55b3cf 100644
--- a/pontoon/tags/admin/__init__.py
+++ b/pontoon/tags/admin/__init__.py
@@ -0,0 +1,10 @@
+from .resources import TagsResourcesTool
+from .tags import TagsTool
+from .tag import TagTool
+
+
+__all__ = (
+ "TagsResourcesTool",
+ "TagsTool",
+ "TagTool",
+)
diff --git a/pontoon/tags/utils/base.py b/pontoon/tags/admin/base.py
similarity index 68%
rename from pontoon/tags/utils/base.py
rename to pontoon/tags/admin/base.py
index 7226bb38f2..7ba6f6240c 100644
--- a/pontoon/tags/utils/base.py
+++ b/pontoon/tags/admin/base.py
@@ -1,6 +1,5 @@
from collections import OrderedDict
-from django.db.models import Q
from django.utils.functional import cached_property
from pontoon.base.models import (
@@ -137,50 +136,3 @@ def translation_manager(self):
@property
def tr_manager(self):
return TranslatedResource.objects
-
-
-class TagsTRTool(TagsDataTool):
- """Data Tool from the perspective of TranslatedResources"""
-
- clone_kwargs = TagsDataTool.clone_kwargs + ("annotations", "groupby")
-
- @property
- def data_manager(self):
- return self.tr_manager
-
- def filter_locales(self, trs):
- return trs.filter(locale__in=self.locales) if self.locales else trs
-
- def filter_path(self, trs):
- return (
- trs.filter(resource__path__contains=self.path).distinct()
- if self.path
- else trs
- )
-
- def filter_projects(self, trs):
- return trs.filter(resource__project__in=self.projects) if self.projects else trs
-
- def filter_tag(self, trs):
- """Filters on tag.slug and tag.priority"""
-
- q = Q()
- if not self.slug:
- # if slug is not specified, then just remove all resources
- # that have no tag
- q &= ~Q(resource__tag__isnull=True)
-
- if self.slug:
- q &= Q(resource__tag__slug__contains=self.slug)
-
- if self.priority is not None:
- if self.priority is False:
- # if priority is False, exclude tags with priority
- q &= Q(resource__tag__priority__isnull=True)
- elif self.priority is True:
- # if priority is True show only tags with priority
- q &= Q(resource__tag__priority__isnull=False)
- elif isinstance(self.priority, int):
- # if priority is an int, filter on that priority
- q &= Q(resource__tag__priority=self.priority)
- return trs.filter(q)
diff --git a/pontoon/tags/exceptions.py b/pontoon/tags/admin/exceptions.py
similarity index 100%
rename from pontoon/tags/exceptions.py
rename to pontoon/tags/admin/exceptions.py
diff --git a/pontoon/tags/admin/forms.py b/pontoon/tags/admin/forms.py
index f25aade1d4..2aa29ab52b 100644
--- a/pontoon/tags/admin/forms.py
+++ b/pontoon/tags/admin/forms.py
@@ -2,7 +2,7 @@
from django.utils.functional import cached_property
from pontoon.base.models import Resource
-from pontoon.tags.utils import TagsTool
+from .tags import TagsTool
class LinkTagResourcesAdminForm(forms.Form):
diff --git a/pontoon/tags/utils/resources.py b/pontoon/tags/admin/resources.py
similarity index 98%
rename from pontoon/tags/utils/resources.py
rename to pontoon/tags/admin/resources.py
index 0623467870..1af3d97f54 100644
--- a/pontoon/tags/utils/resources.py
+++ b/pontoon/tags/admin/resources.py
@@ -1,8 +1,7 @@
from django.db.models import Q
-from pontoon.tags.exceptions import InvalidProjectError
-
from .base import TagsDataTool
+from .exceptions import InvalidProjectError
class TagsResourcesTool(TagsDataTool):
diff --git a/pontoon/tags/utils/tag.py b/pontoon/tags/admin/tag.py
similarity index 72%
rename from pontoon/tags/utils/tag.py
rename to pontoon/tags/admin/tag.py
index 22146c8613..f7c2084c19 100644
--- a/pontoon/tags/utils/tag.py
+++ b/pontoon/tags/admin/tag.py
@@ -1,9 +1,7 @@
from django.utils.functional import cached_property
-from .tagged import Tagged, TaggedLocale
-
-class TagTool(Tagged):
+class TagTool:
"""This wraps ``Tag`` model kwargs providing an API for
efficient retrieval of related information, eg tagged ``Resources``,
``Locales`` and ``Projects``, and methods for managing linked
@@ -29,15 +27,6 @@ def linked_resources(self):
"""``Resources`` that are linked to this ``Tag``"""
return self.resource_tool.get_linked_resources(self.slug).order_by("path")
- @property
- def locale_stats(self):
- return self.tags_tool.stat_tool(slug=self.slug, groupby="locale").data
-
- @cached_property
- def locale_latest(self):
- """A cached property containing latest locale changes"""
- return self.tags_tool.translation_tool(slug=self.slug, groupby="locale").data
-
@cached_property
def object(self):
"""Returns the ``Tag`` model object for this tag.
@@ -61,19 +50,6 @@ def resource_tool(self):
else self.tags_tool.resource_tool
)
- def iter_locales(self, project=None):
- """Iterate ``Locales`` that are associated with this tag
- (given any filtering in ``self.tags_tool``)
-
- yields a ``TaggedLocale`` that can be used to get eg chart data
- """
- for locale in self.locale_stats:
- yield TaggedLocale(
- project=project,
- latest_translation=self.locale_latest.get(locale["locale"]),
- **locale
- )
-
def link_resources(self, resources):
"""Link Resources to this tag, raises an Error if the tag's
Project is set, and the requested resource is not in that Project
diff --git a/pontoon/tags/utils/tags.py b/pontoon/tags/admin/tags.py
similarity index 56%
rename from pontoon/tags/utils/tags.py
rename to pontoon/tags/admin/tags.py
index 6466ce0187..bc401eceb7 100644
--- a/pontoon/tags/utils/tags.py
+++ b/pontoon/tags/admin/tags.py
@@ -4,9 +4,7 @@
from .base import Clonable
from .resources import TagsResourcesTool
-from .stats import TagsStatsTool
from .tag import TagTool
-from .translations import TagsLatestTranslationsTool
class TagsTool(Clonable):
@@ -17,19 +15,11 @@ class TagsTool(Clonable):
tag_class = TagTool
resources_class = TagsResourcesTool
- translations_class = TagsLatestTranslationsTool
- stats_class = TagsStatsTool
clone_kwargs = ("locales", "projects", "priority", "path", "slug")
def __getitem__(self, k):
return self(slug=k)
- def __iter__(self):
- return self.iter_tags(self.stat_tool.data)
-
- def __len__(self):
- return len(self.stat_tool.data)
-
@property
def tag_manager(self):
return Tag.objects
@@ -40,22 +30,6 @@ def resource_tool(self):
projects=self.projects, locales=self.locales, slug=self.slug, path=self.path
)
- @cached_property
- def stat_tool(self):
- return self.stats_class(
- slug=self.slug,
- locales=self.locales,
- projects=self.projects,
- priority=self.priority,
- path=self.path,
- )
-
- @cached_property
- def translation_tool(self):
- return self.translations_class(
- slug=self.slug, locales=self.locales, projects=self.projects
- )
-
def get(self, slug=None):
"""Get the first tag by iterating self, or by slug if set"""
if slug is None:
@@ -70,11 +44,3 @@ def get_tags(self, slug=None):
if slug:
return tags.filter(slug=slug)
return tags
-
- def iter_tags(self, tags):
- """Iterate associated tags, and create TagTool objects for
- each, adding latest translation data
- """
- for tag in tags:
- latest_translation = self.translation_tool.data.get(tag["resource__tag"])
- yield self.tag_class(self, latest_translation=latest_translation, **tag)
diff --git a/pontoon/tags/templates/tags/tag.html b/pontoon/tags/templates/tags/tag.html
index b4499e3e0a..04364d6c74 100644
--- a/pontoon/tags/templates/tags/tag.html
+++ b/pontoon/tags/templates/tags/tag.html
@@ -54,7 +54,7 @@
{{ HeadingInfo.progress_chart() }}
- {{ HeadingInfo.progress_chart_legend(tag) }}
+ {{ HeadingInfo.progress_chart_legend(tag.chart) }}
{% endblock %}
diff --git a/pontoon/tags/tests/utils/__init__.py b/pontoon/tags/tests/admin/__init__.py
similarity index 100%
rename from pontoon/tags/tests/utils/__init__.py
rename to pontoon/tags/tests/admin/__init__.py
diff --git a/pontoon/tags/tests/utils/test_base.py b/pontoon/tags/tests/admin/test_base.py
similarity index 96%
rename from pontoon/tags/tests/utils/test_base.py
rename to pontoon/tags/tests/admin/test_base.py
index ddbb406ed2..881800d8c6 100644
--- a/pontoon/tags/tests/utils/test_base.py
+++ b/pontoon/tags/tests/admin/test_base.py
@@ -8,8 +8,8 @@
Translation,
)
+from pontoon.tags.admin.base import Clonable, TagsDataTool
from pontoon.tags.models import Tag
-from pontoon.tags.utils.base import Clonable, TagsDataTool
def test_util_clonable():
diff --git a/pontoon/tags/tests/test_forms.py b/pontoon/tags/tests/admin/test_forms.py
similarity index 100%
rename from pontoon/tags/tests/test_forms.py
rename to pontoon/tags/tests/admin/test_forms.py
diff --git a/pontoon/tags/tests/utils/test_resources.py b/pontoon/tags/tests/admin/test_resources.py
similarity index 96%
rename from pontoon/tags/tests/utils/test_resources.py
rename to pontoon/tags/tests/admin/test_resources.py
index a556eec46b..651f8acca1 100644
--- a/pontoon/tags/tests/utils/test_resources.py
+++ b/pontoon/tags/tests/admin/test_resources.py
@@ -10,9 +10,9 @@
ResourceFactory,
TagFactory,
)
-from pontoon.tags.exceptions import InvalidProjectError
+from pontoon.tags.admin import TagsResourcesTool
+from pontoon.tags.admin.exceptions import InvalidProjectError
from pontoon.tags.models import Tag
-from pontoon.tags.utils import TagsResourcesTool
@pytest.fixture
@@ -161,7 +161,7 @@ def test_util_tags_resources_tool_link_bad(resource_a, tag_c, project_b):
def test_util_tags_resources_tool_linked_resources(resource_a, tag_c):
resource_tool = TagsResourcesTool()
- _patch_ctx = patch("pontoon.tags.utils.TagsResourcesTool.get")
+ _patch_ctx = patch("pontoon.tags.admin.TagsResourcesTool.get")
with _patch_ctx as m:
values = MagicMock()
values.values.return_value = 7
@@ -176,7 +176,7 @@ def test_util_tags_resources_tool_linked_resources(resource_a, tag_c):
def test_util_tags_resources_tool_linkable_resources(resource_a, tag_c):
resource_tool = TagsResourcesTool()
- _patch_ctx = patch("pontoon.tags.utils.TagsResourcesTool.find")
+ _patch_ctx = patch("pontoon.tags.admin.TagsResourcesTool.find")
with _patch_ctx as m:
values = MagicMock()
values.values.return_value = 7
@@ -245,7 +245,7 @@ def test_util_tag_resources_tool_get():
resource_tool = TagsResourcesTool()
_patch_ctx = patch(
- "pontoon.tags.utils.TagsResourcesTool.filtered_data",
+ "pontoon.tags.admin.TagsResourcesTool.filtered_data",
new_callable=PropertyMock(),
)
with _patch_ctx as p:
diff --git a/pontoon/tags/tests/utils/test_tag.py b/pontoon/tags/tests/admin/test_tag.py
similarity index 54%
rename from pontoon/tags/tests/utils/test_tag.py
rename to pontoon/tags/tests/admin/test_tag.py
index 71ba19d750..8b7f3c9eb3 100644
--- a/pontoon/tags/tests/utils/test_tag.py
+++ b/pontoon/tags/tests/admin/test_tag.py
@@ -1,89 +1,9 @@
-import types
from unittest.mock import patch, MagicMock, PropertyMock
-import pytest
-
-from pontoon.tags.utils import TagsTool, TagTool
-
-
-@pytest.mark.parametrize(
- "kwargs",
- [
- dict(
- tags_tool=None, name=None, pk=None, priority=None, project=None, slug=None
- ),
- dict(
- tags_tool=1,
- name=2,
- pk=3,
- priority=4,
- project=5,
- slug=6,
- latest_translation=7,
- total_strings=8,
- approved_strings=9,
- ),
- ],
-)
-def test_util_tag_tool_init(kwargs):
- # Test the TagTool can be instantiated with/out args
- tags_tool = TagTool(**kwargs)
- for k, v in kwargs.items():
- assert getattr(tags_tool, k) == v
-
-
-@patch("pontoon.tags.utils.tags.TagsTool.stat_tool", new_callable=PropertyMock())
-def test_util_tag_tool_locale_stats(stats_mock):
- stats_mock.configure_mock(**{"return_value.data": 23})
- tag_tool = TagTool(
- TagsTool(), name=None, pk=None, priority=None, project=None, slug=7
- )
-
- # locale_stats returns self.tags_tool.stats_tool().data
- assert tag_tool.locale_stats == 23
-
- # stats_tool was called with slug and groupby
- assert list(stats_mock.call_args) == [(), {"groupby": "locale", "slug": 7}]
+from pontoon.tags.admin import TagsTool, TagTool
-@patch("pontoon.tags.utils.tag.TagTool.locale_stats", new_callable=PropertyMock)
-@patch("pontoon.tags.utils.tag.TagTool.locale_latest", new_callable=PropertyMock)
-@patch("pontoon.tags.utils.tag.TaggedLocale")
-def test_util_tag_tool_iter_locales(locale_mock, latest_mock, stats_mock):
- tag_tool = TagTool(
- TagsTool(), name=None, pk=None, priority=None, project=None, slug=None
- )
-
- # Set mocks
- locale_mock.return_value = "X"
- latest_mock.configure_mock(**{"return_value.get.return_value": 23})
- stats_mock.return_value = [
- dict(foo=1, locale=1),
- dict(foo=2, locale=2),
- dict(foo=3, locale=3),
- ]
-
- # iter_locales - should generate 3 of 'X'
- locales = tag_tool.iter_locales()
- assert isinstance(locales, types.GeneratorType)
- assert list(locales) == ["X"] * 3
- assert len(locale_mock.call_args_list) == 3
- assert stats_mock.called
-
- # locale_latest is called with each of the locales
- assert list(list(a) for a in latest_mock.return_value.get.call_args_list) == [
- [(1,), {}],
- [(2,), {}],
- [(3,), {}],
- ]
-
- # TaggedLocale is called with locale data
- for i, args in enumerate(locale_mock.call_args_list):
- assert args[1]["foo"] == i + 1
- assert args[1]["latest_translation"] == 23
-
-
-@patch("pontoon.tags.utils.TagTool.resource_tool", new_callable=PropertyMock)
+@patch("pontoon.tags.admin.TagTool.resource_tool", new_callable=PropertyMock)
def test_util_tag_tool_linked_resources(resources_mock):
tag_tool = TagTool(
TagsTool(), name=None, pk=None, priority=None, project=None, slug=7
@@ -108,7 +28,7 @@ def test_util_tag_tool_linked_resources(resources_mock):
assert list(order_by_mock.call_args) == [("path",), {}]
-@patch("pontoon.tags.utils.TagTool.resource_tool", new_callable=PropertyMock)
+@patch("pontoon.tags.admin.TagTool.resource_tool", new_callable=PropertyMock)
def test_util_tag_tool_linkable_resources(resources_mock):
tag_tool = TagTool(
TagsTool(), name=None, pk=None, priority=None, project=None, slug=7
@@ -133,7 +53,7 @@ def test_util_tag_tool_linkable_resources(resources_mock):
assert list(order_by_mock.call_args) == [("path",), {}]
-@patch("pontoon.tags.utils.TagTool.resource_tool", new_callable=PropertyMock)
+@patch("pontoon.tags.admin.TagTool.resource_tool", new_callable=PropertyMock)
def test_util_tag_tool_link_resources(resources_mock):
tag_tool = TagTool(
TagsTool(), name=None, pk=None, priority=None, project=None, slug=7
@@ -147,7 +67,7 @@ def test_util_tag_tool_link_resources(resources_mock):
assert list(resources_mock.return_value.link.call_args) == [(7,), {"resources": 13}]
-@patch("pontoon.tags.utils.TagTool.resource_tool", new_callable=PropertyMock)
+@patch("pontoon.tags.admin.TagTool.resource_tool", new_callable=PropertyMock)
def test_util_tag_tool_unlink_resources(resources_mock):
tag_tool = TagTool(
TagsTool(), name=None, pk=None, priority=None, project=None, slug=7
@@ -164,7 +84,7 @@ def test_util_tag_tool_unlink_resources(resources_mock):
]
-@patch("pontoon.tags.utils.TagsTool.tag_manager", new_callable=PropertyMock)
+@patch("pontoon.tags.admin.TagsTool.tag_manager", new_callable=PropertyMock)
def test_util_tag_tool_object(tag_mock):
tag_mock.configure_mock(
**{"return_value.select_related" ".return_value.get.return_value": 23}
@@ -183,7 +103,7 @@ def test_util_tag_tool_object(tag_mock):
]
-@patch("pontoon.tags.utils.TagsTool.resource_tool", new_callable=PropertyMock)
+@patch("pontoon.tags.admin.TagsTool.resource_tool", new_callable=PropertyMock)
def test_util_tag_tool_resource_tool(resources_mock):
tool_mock = MagicMock(return_value=23)
resources_mock.return_value = tool_mock
@@ -202,13 +122,3 @@ def test_util_tag_tool_resource_tool(resources_mock):
# tool was called with project as args
assert tag_tool.resource_tool == 23
assert list(tool_mock.call_args) == [(), {"projects": [43]}]
-
-
-@patch("pontoon.tags.utils.TagsTool.translation_tool")
-def test_util_tag_tool_locale_latest(trans_mock):
- trans_mock.configure_mock(**{"return_value.data": 23})
- tag_tool = TagTool(
- TagsTool(), name=None, pk=None, priority=None, project=None, slug=17
- )
- assert tag_tool.locale_latest == 23
- assert list(trans_mock.call_args) == [(), {"groupby": "locale", "slug": 17}]
diff --git a/pontoon/tags/tests/admin/test_tags.py b/pontoon/tags/tests/admin/test_tags.py
new file mode 100644
index 0000000000..acaccfaa89
--- /dev/null
+++ b/pontoon/tags/tests/admin/test_tags.py
@@ -0,0 +1,100 @@
+from unittest.mock import MagicMock, patch, PropertyMock
+
+import pytest
+
+from pontoon.tags.admin import (
+ TagsResourcesTool,
+ TagsTool,
+ TagTool,
+)
+from pontoon.tags.admin.base import Clonable
+from pontoon.tags.models import Tag
+
+
+def test_util_tags_tool():
+ # test tags tool instantiation
+ tags_tool = TagsTool()
+ assert tags_tool.tag_class is TagTool
+ assert tags_tool.resources_class is TagsResourcesTool
+ assert tags_tool.locales is None
+ assert tags_tool.projects is None
+ assert tags_tool.priority is None
+ assert tags_tool.slug is None
+ assert tags_tool.path is None
+ assert tags_tool.tag_manager == Tag.objects
+
+
+@pytest.mark.parametrize(
+ "kwargs",
+ [
+ dict(slug=None, locales=None, projects=None, path=None),
+ dict(slug=1, locales=2, projects=3, path=4),
+ ],
+)
+@patch("pontoon.tags.admin.TagsTool.resources_class")
+def test_util_tags_tool_resources(resources_mock, kwargs):
+ # tests instantiation of tag.resources_tool with different args
+ tags_tool = TagsTool(**kwargs)
+ resources_mock.return_value = 23
+ assert tags_tool.resource_tool == 23
+ assert resources_mock.call_args[1] == kwargs
+
+
+@patch("pontoon.tags.admin.TagsTool.tag_class")
+@patch("pontoon.tags.admin.TagsTool.get_tags")
+def test_util_tags_tool_get(tags_mock, class_mock):
+ # tests getting a TagTool from TagsTool
+ tags_tool = TagsTool()
+ class_mock.return_value = 23
+
+ # calling with slug creates a TagTool instance
+ assert tags_tool.get(113) == 23
+ assert list(class_mock.call_args) == [(tags_tool,), {}]
+ assert list(tags_mock.call_args) == [(), {"slug": 113}]
+
+
+def test_util_tags_tool_call_and_clone():
+ # tests cloning a TagsTool
+ tags_tool = TagsTool()
+ cloned = tags_tool()
+ assert cloned is not tags_tool
+ assert isinstance(tags_tool, Clonable)
+ assert isinstance(cloned, Clonable)
+
+
+@patch("pontoon.tags.admin.TagsTool.__call__")
+def test_util_tags_tool_getitem(call_mock):
+ # test that calling __getitem__ calls __call__ with slug
+ tags_tool = TagsTool()
+ slugs = ["foo", "bar"]
+ for slug in slugs:
+ tags_tool[slug]
+ assert call_mock.call_args_list[0][1] == dict(slug=slugs[0])
+ assert call_mock.call_args_list[1][1] == dict(slug=slugs[1])
+
+
+@patch("pontoon.tags.admin.TagsTool.tag_manager", new_callable=PropertyMock)
+def test_util_tags_tool_get_tags(tag_mock):
+ filter_mock = MagicMock(**{"filter.return_value": 23})
+ tag_mock.configure_mock(
+ **{"return_value.filter.return_value.values.return_value": filter_mock}
+ )
+ tags_tool = TagsTool()
+
+ # no slug provided, returns `values`
+ assert tags_tool.get_tags() is filter_mock
+ assert not filter_mock.called
+ assert list(tag_mock.return_value.filter.return_value.values.call_args) == [
+ ("pk", "name", "slug", "priority", "project"),
+ {},
+ ]
+
+ tag_mock.reset_mock()
+
+ # slug provided, `values` is filtered
+ assert tags_tool.get_tags("FOO") == 23
+ assert list(filter_mock.filter.call_args) == [(), {"slug": "FOO"}]
+ assert list(tag_mock.return_value.filter.return_value.values.call_args) == [
+ ("pk", "name", "slug", "priority", "project"),
+ {},
+ ]
diff --git a/pontoon/tags/tests/admin/test_views.py b/pontoon/tags/tests/admin/test_views.py
new file mode 100644
index 0000000000..0b68669189
--- /dev/null
+++ b/pontoon/tags/tests/admin/test_views.py
@@ -0,0 +1,133 @@
+from unittest.mock import patch
+
+import pytest
+
+from django.urls import reverse
+
+
+@pytest.mark.django_db
+@patch("pontoon.tags.admin.views.ProjectTagAdminAjaxView.get_form_class")
+def test_view_project_tag_admin_ajax_form(
+ form_mock,
+ client,
+ admin,
+ project_a,
+ tag_a,
+):
+ form_mock.configure_mock(
+ **{
+ "return_value.return_value.is_valid.return_value": True,
+ "return_value.return_value.save.return_value": [7, 23],
+ "return_value.return_value.errors": [],
+ }
+ )
+ client.force_login(admin)
+ url = reverse(
+ "pontoon.admin.project.ajax.tag",
+ kwargs=dict(
+ project=project_a.slug,
+ tag=tag_a.slug,
+ ),
+ )
+
+ response = client.post(
+ url,
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ assert form_mock.return_value.return_value.is_valid.called
+ assert form_mock.return_value.return_value.save.called
+ assert response.json() == {"data": [7, 23]}
+
+
+@pytest.mark.django_db
+@patch("pontoon.tags.admin.views.ProjectTagAdminAjaxView.get_form_class")
+def test_view_project_tag_admin_ajax_form_bad(
+ form_mock,
+ client,
+ admin,
+ project_a,
+ tag_a,
+):
+ form_mock.configure_mock(
+ **{
+ "return_value.return_value.is_valid.return_value": False,
+ "return_value.return_value.errors": ["BIG PROBLEM"],
+ }
+ )
+ client.force_login(admin)
+ url = reverse(
+ "pontoon.admin.project.ajax.tag",
+ kwargs=dict(
+ project=project_a.slug,
+ tag=tag_a.slug,
+ ),
+ )
+
+ response = client.post(
+ url,
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ assert response.status_code == 400
+ assert form_mock.return_value.call_args[1]["project"] == project_a
+ assert dict(form_mock.return_value.call_args[1]["data"]) == dict(tag=["tag"])
+ assert form_mock.return_value.return_value.is_valid.called
+ assert not form_mock.return_value.return_value.save.called
+ assert response.json() == {"errors": ["BIG PROBLEM"]}
+
+ form_mock.return_value.reset_mock()
+ response = client.post(
+ url,
+ data=dict(foo="bar", bar="baz"),
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ assert response.status_code == 400
+ assert form_mock.return_value.call_args[1]["project"] == project_a
+ assert dict(form_mock.return_value.call_args[1]["data"]) == dict(
+ foo=["bar"],
+ bar=["baz"],
+ tag=["tag"],
+ )
+ assert form_mock.return_value.return_value.is_valid.called
+ assert not form_mock.return_value.return_value.save.called
+ assert response.json() == {"errors": ["BIG PROBLEM"]}
+
+
+@pytest.mark.django_db
+@patch("pontoon.tags.admin.views.ProjectTagAdminAjaxView.get_form")
+def test_view_project_tag_admin_ajax(form_mock, member, project_a, tag_a):
+ form_mock.configure_mock(**{"return_value.save.return_value": 23})
+ url = reverse(
+ "pontoon.admin.project.ajax.tag",
+ kwargs=dict(
+ project=project_a.slug,
+ tag=tag_a.slug,
+ ),
+ )
+
+ # no `get` here
+ response = member.client.get(url)
+ assert response.status_code == 404
+
+ # need xhr headers
+ response = member.client.post(url)
+ assert response.status_code == 400
+
+ # must be superuser!
+ response = member.client.post(
+ url,
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
+ )
+ if not response.wsgi_request.user.is_superuser:
+ assert not form_mock.called
+ assert not form_mock.return_value.is_valid.called
+ if response.wsgi_request.user.is_anonymous:
+ assert response.status_code == 404
+ else:
+ assert response.status_code == 403
+ return
+
+ # Form.get_form was called
+ assert form_mock.called
+ assert form_mock.return_value.is_valid.called
+ assert response.status_code == 200
+ assert response.json() == {"data": 23}
diff --git a/pontoon/tags/tests/test_utils.py b/pontoon/tags/tests/test_utils.py
new file mode 100644
index 0000000000..6e5454e457
--- /dev/null
+++ b/pontoon/tags/tests/test_utils.py
@@ -0,0 +1,76 @@
+import pytest
+
+from pontoon.tags.utils import Tags
+
+
+@pytest.fixture
+def chart_0():
+ return {
+ "total_strings": 0,
+ "approved_strings": 0,
+ "pretranslated_strings": 0,
+ "strings_with_errors": 0,
+ "strings_with_warnings": 0,
+ "unreviewed_strings": 0,
+ "approved_share": 0,
+ "pretranslated_share": 0,
+ "errors_share": 0,
+ "warnings_share": 0,
+ "unreviewed_share": 0,
+ "completion_percent": 0,
+ }
+
+
+@pytest.mark.django_db
+def test_tags_get_no_project():
+ tags = Tags().get()
+ assert len(tags) == 0
+
+
+@pytest.mark.django_db
+def test_tags_get(
+ chart_0,
+ project_a,
+ tag_a,
+ translation_a,
+):
+ tags = Tags(project=project_a).get()
+ tag = tags[0]
+
+ assert tag.name == tag_a.name
+ assert tag.slug == tag_a.slug
+ assert tag.priority == tag_a.priority
+ assert tag.latest_activity == translation_a.latest_activity
+
+ chart = chart_0
+ chart["total_strings"] = 1
+ chart["unreviewed_strings"] = 1
+ chart["unreviewed_share"] = 100.0
+ assert tag.chart == chart
+
+
+@pytest.mark.django_db
+def test_tags_get_tag_locales(
+ chart_0,
+ project_a,
+ project_locale_a,
+ tag_a,
+):
+ tags = Tags(project=project_a, slug=tag_a.slug)
+ tag = tags.get_tag_locales()
+
+ assert tag.name == tag_a.name
+ assert tag.priority == tag_a.priority
+ assert tag.locales.count() == project_a.locales.all().count()
+ assert tag.locales.first() == project_a.locales.all().first()
+
+ with pytest.raises(AttributeError):
+ tag.latest_activity
+
+ chart = chart_0
+ chart["total_strings"] = 1
+ assert tag.chart == chart
+
+ locale = tag.locales.first()
+ assert locale.latest_activity is None
+ assert locale.chart == chart
diff --git a/pontoon/tags/tests/test_views.py b/pontoon/tags/tests/test_views.py
index 5e2e3144ea..5b18f97504 100644
--- a/pontoon/tags/tests/test_views.py
+++ b/pontoon/tags/tests/test_views.py
@@ -1,139 +1,8 @@
-from unittest.mock import patch
-
import pytest
from django.urls import reverse
from pontoon.base.models import Priority
-from pontoon.tags.utils import TaggedLocale, TagTool
-
-
-@pytest.mark.django_db
-@patch("pontoon.tags.admin.views.ProjectTagAdminAjaxView.get_form_class")
-def test_view_project_tag_admin_ajax_form(
- form_mock,
- client,
- admin,
- project_a,
- tag_a,
-):
- form_mock.configure_mock(
- **{
- "return_value.return_value.is_valid.return_value": True,
- "return_value.return_value.save.return_value": [7, 23],
- "return_value.return_value.errors": [],
- }
- )
- client.force_login(admin)
- url = reverse(
- "pontoon.admin.project.ajax.tag",
- kwargs=dict(
- project=project_a.slug,
- tag=tag_a.slug,
- ),
- )
-
- response = client.post(
- url,
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
- assert form_mock.return_value.return_value.is_valid.called
- assert form_mock.return_value.return_value.save.called
- assert response.json() == {"data": [7, 23]}
-
-
-@pytest.mark.django_db
-@patch("pontoon.tags.admin.views.ProjectTagAdminAjaxView.get_form_class")
-def test_view_project_tag_admin_ajax_form_bad(
- form_mock,
- client,
- admin,
- project_a,
- tag_a,
-):
- form_mock.configure_mock(
- **{
- "return_value.return_value.is_valid.return_value": False,
- "return_value.return_value.errors": ["BIG PROBLEM"],
- }
- )
- client.force_login(admin)
- url = reverse(
- "pontoon.admin.project.ajax.tag",
- kwargs=dict(
- project=project_a.slug,
- tag=tag_a.slug,
- ),
- )
-
- response = client.post(
- url,
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
- assert response.status_code == 400
- assert form_mock.return_value.call_args[1]["project"] == project_a
- assert dict(form_mock.return_value.call_args[1]["data"]) == dict(tag=["tag"])
- assert form_mock.return_value.return_value.is_valid.called
- assert not form_mock.return_value.return_value.save.called
- assert response.json() == {"errors": ["BIG PROBLEM"]}
-
- form_mock.return_value.reset_mock()
- response = client.post(
- url,
- data=dict(foo="bar", bar="baz"),
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
- assert response.status_code == 400
- assert form_mock.return_value.call_args[1]["project"] == project_a
- assert dict(form_mock.return_value.call_args[1]["data"]) == dict(
- foo=["bar"],
- bar=["baz"],
- tag=["tag"],
- )
- assert form_mock.return_value.return_value.is_valid.called
- assert not form_mock.return_value.return_value.save.called
- assert response.json() == {"errors": ["BIG PROBLEM"]}
-
-
-@pytest.mark.django_db
-@patch("pontoon.tags.admin.views.ProjectTagAdminAjaxView.get_form")
-def test_view_project_tag_admin_ajax(form_mock, member, project_a, tag_a):
- form_mock.configure_mock(**{"return_value.save.return_value": 23})
- url = reverse(
- "pontoon.admin.project.ajax.tag",
- kwargs=dict(
- project=project_a.slug,
- tag=tag_a.slug,
- ),
- )
-
- # no `get` here
- response = member.client.get(url)
- assert response.status_code == 404
-
- # need xhr headers
- response = member.client.post(url)
- assert response.status_code == 400
-
- # must be superuser!
- response = member.client.post(
- url,
- HTTP_X_REQUESTED_WITH="XMLHttpRequest",
- )
- if not response.wsgi_request.user.is_superuser:
- assert not form_mock.called
- assert not form_mock.return_value.is_valid.called
- if response.wsgi_request.user.is_anonymous:
- assert response.status_code == 404
- else:
- assert response.status_code == 403
- return
-
- # Form.get_form was called
- assert form_mock.called
- assert form_mock.return_value.is_valid.called
- assert response.status_code == 200
- assert response.json() == {"data": 23}
@pytest.mark.django_db
@@ -144,14 +13,11 @@ def test_view_project_tag_locales(client, project_a, tag_a):
)
# tag is not associated with project
+ project_a.tag_set.remove(tag_a)
response = client.get(url)
assert response.status_code == 404
- # tag has no priority so still wont show up...
project_a.tag_set.add(tag_a)
- response = client.get(url)
- assert response.status_code == 404
-
tag_a.priority = Priority.NORMAL
tag_a.save()
response = client.get(url)
@@ -165,12 +31,11 @@ def test_view_project_tag_locales(client, project_a, tag_a):
assert response.context_data["project"] == project_a
res_tag = response.context_data["tag"]
- assert isinstance(res_tag, TagTool)
- assert res_tag.object == tag_a
+ assert res_tag == tag_a
@pytest.mark.django_db
-def test_view_project_tag_locales_ajax(client, project_a, project_locale_a, tag_a):
+def test_view_project_tag_locales_ajax(client, project_a, tag_a):
url = reverse(
"pontoon.tags.ajax.teams",
kwargs=dict(project=project_a.slug, tag=tag_a.slug),
@@ -192,7 +57,6 @@ def test_view_project_tag_locales_ajax(client, project_a, project_locale_a, tag_
for i, locale in enumerate(locales):
locale = response.context_data["locales"][i]
- assert isinstance(locale, TaggedLocale)
assert locale.code == locales[i].locale.code
assert locale.name == locales[i].locale.name
diff --git a/pontoon/tags/tests/utils/test_stats.py b/pontoon/tags/tests/utils/test_stats.py
deleted file mode 100644
index 6fe79d43d6..0000000000
--- a/pontoon/tags/tests/utils/test_stats.py
+++ /dev/null
@@ -1,195 +0,0 @@
-from unittest.mock import MagicMock, patch, PropertyMock
-
-import pytest
-
-from django.db.models import QuerySet
-
-from pontoon.base.models import TranslatedResource
-from pontoon.tags.utils import TagsStatsTool
-
-
-def test_util_tags_stats_tool(tag_data_init_kwargs):
- # tests instantiation of stats tool
- kwargs = tag_data_init_kwargs
- stats_tool = TagsStatsTool(**kwargs)
- for k, v in kwargs.items():
- assert getattr(stats_tool, k) == v
- assert stats_tool.tr_manager == TranslatedResource.objects
-
-
-def test_util_tags_stats_tool_annotations():
- # tests annotations can be overridden
- stats_tool = TagsStatsTool()
- assert stats_tool.get_annotations() == stats_tool.default_annotations
-
- anno = dict(foo="foo0", bar="bar0")
- stats_tool = TagsStatsTool(annotations=anno)
- assert stats_tool.get_annotations() != stats_tool.default_annotations
- assert stats_tool.get_annotations() != anno
- anno.update(stats_tool.default_annotations)
- assert stats_tool.get_annotations() == anno
-
-
-@patch("pontoon.tags.utils.TagsStatsTool.get_data")
-def test_util_tags_stats_tool_data(data_mock):
- # tests coalescing and caching of data
- stats_tool = TagsStatsTool()
- data_mock.return_value = (1, 2, 3)
- result = stats_tool.data
- assert result == [1, 2, 3]
- assert data_mock.called
- data_mock.reset_mock()
- data_mock.return_value = (4, 5, 6)
- result = stats_tool.data
- assert not data_mock.called
- assert result == [1, 2, 3]
- del stats_tool.__dict__["data"]
- result = stats_tool.data
- assert data_mock.called
- assert result == [4, 5, 6]
-
-
-@patch(
- "pontoon.tags.utils.TagsStatsTool.data",
- new_callable=PropertyMock,
-)
-def test_util_tags_stats_tool_len(data_pmock):
- # tests len(stats) is taken from data
- stats_tool = TagsStatsTool()
- data_pmock.return_value = [7, 23]
- result = len(stats_tool)
- assert data_pmock.called
- assert result == 2
-
-
-@patch("pontoon.tags.utils.TagsStatsTool.data", new_callable=PropertyMock)
-def test_util_tags_stats_tool_iter(data_pmock):
- # tests iter(stats) iterates the data
- stats_tool = TagsStatsTool()
- data_pmock.return_value = [7, 23]
- result = list(stats_tool)
- assert data_pmock.called
- assert result == [7, 23]
-
-
-def test_util_tags_stats_tool_filters():
- # tests stats tool has expected filters
- stats_tool = TagsStatsTool()
- assert stats_tool.filters == [
- getattr(stats_tool, "filter_%s" % f) for f in stats_tool.filter_methods
- ]
-
-
-@patch(
- "pontoon.tags.utils.TagsStatsTool.tr_manager",
- new_callable=PropertyMock,
-)
-@patch("pontoon.tags.utils.TagsStatsTool.filter_tag")
-@patch("pontoon.tags.utils.TagsStatsTool.filter_projects")
-@patch("pontoon.tags.utils.TagsStatsTool.filter_locales")
-@patch("pontoon.tags.utils.TagsStatsTool.filter_path")
-def test_util_tags_stats_tool_fitered_data(
- m_path,
- m_locales,
- m_proj,
- m_tag,
- trs_mock,
-):
- # tests all filter functions are called when filtering data
- # and that they are called with the result of previous
-
- stats_tool = TagsStatsTool()
- m = m_tag, m_proj, m_locales, m_path
-
- # mock trs for translated_resources.all()
- _m = MagicMock()
- _m.all.return_value = 0
- trs_mock.return_value = _m
-
- for i, _m in enumerate(m):
- if i >= len(m) - 1:
- _m.return_value = 23
- else:
- _m.return_value = i
-
- # get the filtered_data
- result = stats_tool.filtered_data
- assert result == 23
- for i, _m in enumerate(m):
- assert _m.called
- if i > 0:
- assert _m.call_args[0][0] == i - 1
-
-
-@pytest.mark.django_db
-def test_util_tags_stats_tool_get_data_empty(calculate_tags, assert_tags):
- # tests stats tool and test calculation doesnt break if there is no data
- stats_tool = TagsStatsTool()
- data = stats_tool.get_data()
- assert isinstance(data, QuerySet)
- assert list(data) == []
- assert_tags(
- calculate_tags(),
- data,
- )
-
-
-@pytest.mark.django_db
-def test_util_tags_stats_tool_get_data_matrix(
- tag_matrix,
- calculate_tags,
- assert_tags,
- tag_test_kwargs,
-):
- # for different parametrized kwargs, tests that the calculated stat data
- # matches expectations from long-hand calculation
- name, kwargs = tag_test_kwargs
- stats_tool = TagsStatsTool(**kwargs)
- data = stats_tool.get_data()
- assert isinstance(data, QuerySet)
- _tags = calculate_tags(**kwargs)
- assert_tags(_tags, data)
-
- if name.endswith("_exact"):
- assert len(data) == 1
- elif name.endswith("_no_match"):
- assert len(data) == 0
- elif name.endswith("_match"):
- assert len(data) > 0
- elif name.endswith("_contains"):
- assert 1 < len(data) < len(tag_matrix["tags"])
- elif name == "empty":
- pass
- else:
- raise ValueError(f"Unsupported assertion type: {name}")
-
- if name.startswith("slug_") and "slug" in kwargs:
- for result in data:
- assert kwargs["slug"] in result["slug"]
-
-
-@pytest.mark.django_db
-def test_util_tags_stats_tool_groupby_locale(
- tag_matrix,
- calculate_tags,
- assert_tags,
- tag_test_kwargs,
-):
- name, kwargs = tag_test_kwargs
-
- # this is only used with slug set to a unique slug, and doesnt work
- # correctly without
- if name == "slug_contains" or not kwargs.get("slug"):
- kwargs["slug"] = tag_matrix["tags"][0].slug
-
- stats_tool = TagsStatsTool(groupby="locale", **kwargs)
- data = stats_tool.get_data()
- # assert isinstance(data, QuerySet)
- exp = calculate_tags(groupby="locale", **kwargs)
- data = stats_tool.coalesce(data)
- assert len(data) == len(exp)
- for locale in data:
- locale_exp = exp[locale["locale"]]
- assert locale_exp["total_strings"] == locale["total_strings"]
- assert locale_exp["pretranslated_strings"] == locale["pretranslated_strings"]
- assert locale_exp["approved_strings"] == locale["approved_strings"]
diff --git a/pontoon/tags/tests/utils/test_tagged.py b/pontoon/tags/tests/utils/test_tagged.py
deleted file mode 100644
index 2797ad34dd..0000000000
--- a/pontoon/tags/tests/utils/test_tagged.py
+++ /dev/null
@@ -1,198 +0,0 @@
-from unittest.mock import patch
-
-import pytest
-
-from pontoon.tags.utils import LatestActivity, LatestActivityUser, TaggedLocale
-from pontoon.tags.utils.chart import TagChart
-from pontoon.tags.utils.tagged import Tagged
-
-
-def test_util_tag_tagged():
- # Tests the base Tagged class
-
- # called with no args - defaults
- tagged = Tagged()
- assert tagged.latest_activity is None
- assert tagged.chart is None
- assert tagged.kwargs == {}
-
- # called with random arg - added to kwargs
- tagged = Tagged(foo="bar")
- assert tagged.latest_activity is None
- assert tagged.chart is None
- assert tagged.kwargs == dict(foo="bar")
-
- # called with total_strings - chart added
- tagged = Tagged(total_strings=23)
- assert tagged.latest_activity is None
- assert isinstance(tagged.chart, TagChart)
- assert tagged.chart.total_strings == 23
-
- # called with latest_translation - latest activity added
- tagged = Tagged(latest_translation=23)
- with patch("pontoon.tags.utils.tagged.LatestActivity") as m:
- m.return_value = "y"
- assert tagged.latest_activity == "y"
-
- assert tagged.chart is None
- assert tagged.kwargs == {}
-
-
-def test_util_tag_tagged_locale():
- # Tests instantiation of TaggedLocale wrapper
-
- # defaults
- tagged = TaggedLocale()
- assert tagged.code is None
- assert tagged.name is None
- assert tagged.total_strings is None
- assert tagged.latest_activity is None
- assert tagged.tag is None
- assert tagged.project is None
- assert tagged.population is None
- assert tagged.kwargs == {}
-
- # call with locale data
- tagged = TaggedLocale(
- slug="bar", pk=23, code="foo", name="A foo", project=43, population=113
- )
- assert tagged.tag == "bar"
- assert tagged.code == "foo"
- assert tagged.name == "A foo"
- assert tagged.total_strings is None
- assert tagged.latest_activity is None
- assert tagged.kwargs == dict(
- slug="bar", pk=23, code="foo", name="A foo", project=43, population=113
- )
-
- # call with latest_translation and stat data
- tagged = TaggedLocale(latest_translation=7, total_strings=23)
- with patch("pontoon.tags.utils.tagged.LatestActivity") as m:
- m.return_value = "y"
- assert tagged.latest_activity == "y"
- assert isinstance(tagged.chart, TagChart)
- assert tagged.chart.total_strings == 23
-
-
-def test_util_latest_activity():
- # Tests instantiating the latest_activity wrapper
-
- # call with random activity - defaults
- activity = LatestActivity(dict(foo="bar"))
- assert activity.activity == dict(foo="bar")
- assert activity.type == "submitted"
- assert activity.translation == dict(string="")
- assert activity.user is None
-
- # check approved_date
- activity = LatestActivity(dict(approved_date=7))
- assert activity.approved_date == 7
-
- # check date
- activity = LatestActivity(dict(date=23))
- assert activity.date == 23
- activity = LatestActivity(dict(date=23, approved_date=113))
- assert activity.date == 113
-
- # check type is approved
- activity = LatestActivity(dict(date=23, approved_date=113))
- assert activity.type == "approved"
-
- # check user is created if present
- activity = LatestActivity(dict(user__email=43))
- assert isinstance(activity.user, LatestActivityUser)
- assert activity.user.activity == {"user__email": 43}
-
- # check translation is created if present
- activity = LatestActivity(dict(string="foo"))
- assert activity.translation == {"string": "foo"}
-
-
-@patch("pontoon.tags.utils.latest_activity.user_gravatar_url")
-def test_util_latest_activity_user(avatar_mock):
- # Tests instantiating a latest activity user wrapper
-
- avatar_mock.return_value = 113
-
- # call with random user data - defaults
- user = LatestActivityUser(
- dict(foo="bar"),
- "submitted",
- )
- assert user.prefix == ""
- assert user.email is None
- assert user.first_name is None
- assert user.name_or_email is None
- assert user.gravatar_url(23) is None
-
- # call with email - user data added
- user = LatestActivityUser(
- dict(user__email="bar@ba.z"),
- "submitted",
- )
- assert user.prefix == ""
- assert user.email == "bar@ba.z"
- assert user.first_name is None
- assert user.name_or_email == "bar@ba.z"
- assert user.gravatar_url(23) == 113
- assert list(avatar_mock.call_args) == [(user, 23), {}]
-
- avatar_mock.reset_mock()
-
- # call with email and first_name - correct first_name
- user = LatestActivityUser(
- dict(user__email="bar@ba.z", user__first_name="FOOBAR"),
- "submitted",
- )
- assert user.prefix == ""
- assert user.email == "bar@ba.z"
- assert user.first_name == "FOOBAR"
- assert user.name_or_email == "FOOBAR"
- assert user.gravatar_url(23) == 113
- assert list(avatar_mock.call_args) == [(user, 23), {}]
-
- # call with approved user and activity type - correct prefix
- user = LatestActivityUser(
- dict(
- approved_user__email="bar@ba.z",
- approved_user__first_name="FOOBAR",
- user__email="foo.bar@ba.z",
- user__first_name="FOOBARBAZ",
- ),
- "approved",
- )
- assert user.prefix == "approved_"
- assert user.email == "bar@ba.z"
- assert user.first_name == "FOOBAR"
- assert user.name_or_email == "FOOBAR"
- assert user.gravatar_url(23) == 113
- assert list(avatar_mock.call_args) == [(user, 23), {}]
-
-
-def test_util_tag_chart():
-
- chart = TagChart()
- assert chart.approved_strings is None
- assert chart.pretranslated_strings is None
- assert chart.total_strings is None
- assert chart.unreviewed_strings is None
-
- # `total_strings` should be set - otherwise TagChart throws
- # errors
- with pytest.raises(TypeError):
- chart.approved_share
-
- with pytest.raises(TypeError):
- chart._share(23)
-
- chart = TagChart(
- total_strings=73,
- pretranslated_strings=7,
- approved_strings=13,
- strings_with_warnings=0,
- unreviewed_strings=23,
- )
- assert chart.approved_share == 18.0
- assert chart.pretranslated_share == 10.0
- assert chart.unreviewed_share == 32.0
- assert chart.completion_percent == 27
diff --git a/pontoon/tags/tests/utils/test_tags.py b/pontoon/tags/tests/utils/test_tags.py
deleted file mode 100644
index 5ea14fb9a0..0000000000
--- a/pontoon/tags/tests/utils/test_tags.py
+++ /dev/null
@@ -1,203 +0,0 @@
-from unittest.mock import MagicMock, patch, PropertyMock
-
-import pytest
-
-from pontoon.tags.models import Tag
-from pontoon.tags.utils import (
- TagsLatestTranslationsTool,
- TagsResourcesTool,
- TagsStatsTool,
- TagsTool,
- TagTool,
-)
-from pontoon.tags.utils.base import Clonable
-
-
-def test_util_tags_tool():
- # test tags tool instantiation
- tags_tool = TagsTool()
- assert tags_tool.tag_class is TagTool
- assert tags_tool.resources_class is TagsResourcesTool
- assert tags_tool.translations_class is TagsLatestTranslationsTool
- assert tags_tool.stats_class is TagsStatsTool
- assert tags_tool.locales is None
- assert tags_tool.projects is None
- assert tags_tool.priority is None
- assert tags_tool.slug is None
- assert tags_tool.path is None
- assert tags_tool.tag_manager == Tag.objects
-
-
-@patch("pontoon.tags.utils.TagsTool.stats_class")
-def test_util_tags_tool_stats(stats_mock, tag_init_kwargs):
- # tests instantiation of tag.stats_tool with different args
- tags_tool = TagsTool(**tag_init_kwargs)
- stats_mock.return_value = 23
- assert tags_tool.stat_tool == 23
- assert stats_mock.call_args[1] == tag_init_kwargs
-
-
-@pytest.mark.parametrize(
- "kwargs",
- [
- dict(slug=None, locales=None, projects=None, path=None),
- dict(slug=1, locales=2, projects=3, path=4),
- ],
-)
-@patch("pontoon.tags.utils.TagsTool.resources_class")
-def test_util_tags_tool_resources(resources_mock, kwargs):
- # tests instantiation of tag.resources_tool with different args
- tags_tool = TagsTool(**kwargs)
- resources_mock.return_value = 23
- assert tags_tool.resource_tool == 23
- assert resources_mock.call_args[1] == kwargs
-
-
-@pytest.mark.parametrize(
- "kwargs",
- [dict(slug=None, locales=None, projects=None), dict(slug=1, locales=2, projects=3)],
-)
-@patch("pontoon.tags.utils.TagsTool.translations_class")
-def test_util_tags_tool_translations(trans_mock, kwargs):
- # tests instantiation of tag.translations_tool with different args
- tags_tool = TagsTool(**kwargs)
- trans_mock.return_value = 23
- assert tags_tool.translation_tool == 23
- assert trans_mock.call_args[1] == kwargs
-
-
-@patch("pontoon.tags.utils.TagsTool.tag_class")
-@patch("pontoon.tags.utils.TagsTool.get_tags")
-@patch("pontoon.tags.utils.TagsTool.__len__")
-@patch("pontoon.tags.utils.TagsTool.__iter__")
-def test_util_tags_tool_get(iter_mock, len_mock, tags_mock, class_mock):
- # tests getting a TagTool from TagsTool
- tags_tool = TagsTool()
- class_mock.return_value = 23
- len_mock.return_value = 7
- iter_mock.return_value = iter([3, 17, 73])
-
- # with no slug returns first result from iter(self)
- assert tags_tool.get() == 3
- assert not class_mock.called
- assert not tags_mock.called
- assert len_mock.called
- assert iter_mock.called
- len_mock.reset_mock()
- iter_mock.reset_mock()
-
- # calling with slug creates a TagTool instance
- # and doesnt call iter(self) at all
- assert tags_tool.get(113) == 23
- assert not len_mock.called
- assert not iter_mock.called
- assert list(class_mock.call_args) == [(tags_tool,), {}]
- assert list(tags_mock.call_args) == [(), {"slug": 113}]
-
-
-def test_util_tags_tool_call_and_clone():
- # tests cloning a TagsTool
- tags_tool = TagsTool()
- cloned = tags_tool()
- assert cloned is not tags_tool
- assert isinstance(tags_tool, Clonable)
- assert isinstance(cloned, Clonable)
-
-
-@patch("pontoon.tags.utils.TagsTool.__call__")
-def test_util_tags_tool_getitem(call_mock):
- # test that calling __getitem__ calls __call__ with slug
- tags_tool = TagsTool()
- slugs = ["foo", "bar"]
- for slug in slugs:
- tags_tool[slug]
- assert call_mock.call_args_list[0][1] == dict(slug=slugs[0])
- assert call_mock.call_args_list[1][1] == dict(slug=slugs[1])
-
-
-@patch("pontoon.tags.utils.TagsTool.iter_tags")
-@patch("pontoon.tags.utils.TagsTool.stat_tool", new_callable=PropertyMock)
-def test_util_tags_tool_iter(stats_mock, iter_mock):
- # tests that when you iter it calls iter_tags with
- # stats data
- tags_tool = TagsTool()
- stats_mock.configure_mock(**{"return_value.data": [7, 23]})
- iter_mock.return_value = iter([])
- assert list(tags_tool) == []
- assert stats_mock.called
- assert list(iter_mock.call_args) == [([7, 23],), {}]
-
-
-@patch("pontoon.tags.utils.TagsTool.stat_tool", new_callable=PropertyMock)
-def test_util_tags_tool_len(stats_mock):
- # tests that when you len() you get the len
- # of the stats data
- m_len = MagicMock()
- m_len.__len__.return_value = 23
- stats_mock.configure_mock(**{"return_value.data": m_len})
- tags_tool = TagsTool()
- assert len(tags_tool) == 23
- assert m_len.__len__.called
-
-
-@patch("pontoon.tags.utils.TagsTool.translation_tool", new_callable=PropertyMock)
-@patch("pontoon.tags.utils.TagsTool.tag_class")
-def test_util_tags_tool_iter_tags(tag_mock, trans_mock):
- # tests that iter_tags calls instantiates a TagTool with
- # stat data and latest_translation data
-
- trans_mock.configure_mock(**{"return_value.data.get.return_value": 23})
- tags_tool = TagsTool()
- list(
- tags_tool.iter_tags(
- [
- dict(resource__tag=1, foo="bar"),
- dict(resource__tag=2, foo="bar"),
- dict(resource__tag=3, foo="bar"),
- ]
- )
- )
-
- # translation_tool.data.get() was called 3 times with tag pks
- assert [x[0][0] for x in trans_mock.return_value.data.get.call_args_list] == [
- 1,
- 2,
- 3,
- ]
-
- # TagTool was called 3 times with the tags tool as arg
- assert [x[0][0] for x in tag_mock.call_args_list] == [tags_tool] * 3
-
- # and stat + translation data as kwargs
- assert [x[1] for x in tag_mock.call_args_list] == [
- {"resource__tag": 1, "latest_translation": 23, "foo": "bar"},
- {"resource__tag": 2, "latest_translation": 23, "foo": "bar"},
- {"resource__tag": 3, "latest_translation": 23, "foo": "bar"},
- ]
-
-
-@patch("pontoon.tags.utils.TagsTool.tag_manager", new_callable=PropertyMock)
-def test_util_tags_tool_get_tags(tag_mock):
- filter_mock = MagicMock(**{"filter.return_value": 23})
- tag_mock.configure_mock(
- **{"return_value.filter.return_value.values.return_value": filter_mock}
- )
- tags_tool = TagsTool()
-
- # no slug provided, returns `values`
- assert tags_tool.get_tags() is filter_mock
- assert not filter_mock.called
- assert list(tag_mock.return_value.filter.return_value.values.call_args) == [
- ("pk", "name", "slug", "priority", "project"),
- {},
- ]
-
- tag_mock.reset_mock()
-
- # slug provided, `values` is filtered
- assert tags_tool.get_tags("FOO") == 23
- assert list(filter_mock.filter.call_args) == [(), {"slug": "FOO"}]
- assert list(tag_mock.return_value.filter.return_value.values.call_args) == [
- ("pk", "name", "slug", "priority", "project"),
- {},
- ]
diff --git a/pontoon/tags/tests/utils/test_translations.py b/pontoon/tags/tests/utils/test_translations.py
deleted file mode 100644
index 0b395e4674..0000000000
--- a/pontoon/tags/tests/utils/test_translations.py
+++ /dev/null
@@ -1,147 +0,0 @@
-from unittest.mock import MagicMock, patch
-
-import pytest
-
-from pontoon.base.models import Translation
-from pontoon.tags.utils import TagsLatestTranslationsTool
-
-
-def test_util_tags_stats_tool(tag_data_init_kwargs):
- # tests instantiation of translations tool
- kwargs = tag_data_init_kwargs
- tr_tool = TagsLatestTranslationsTool(**kwargs)
- for k, v in kwargs.items():
- assert getattr(tr_tool, k) == v
-
-
-@pytest.mark.django_db
-def test_util_tags_translation_tool_get_data(
- tag_matrix,
- calculate_tags_latest,
- tag_test_kwargs,
-):
- # for different parametrized kwargs, tests that the calculated
- # latest data matches expectations from long-hand calculation
- name, kwargs = tag_test_kwargs
-
- # calculate expectations
- exp = calculate_tags_latest(**kwargs)
-
- # get the data, and coalesce to translations dictionary
- tr_tool = TagsLatestTranslationsTool(**kwargs)
- data = tr_tool.coalesce(tr_tool.get_data())
-
- # get a pk dictionary of all translations
- translations = Translation.objects.select_related("user").in_bulk()
-
- assert len(data) == len(exp)
-
- for k, (pk, date) in exp.items():
- assert data[k]["date"] == date
- assert data[k]["string"] == translations.get(pk).string
-
- if name.endswith("_exact"):
- assert len(data) == 1
- elif name.endswith("_no_match"):
- assert len(data) == 0
- elif name.endswith("_match"):
- assert len(data) > 0
- elif name.endswith("_contains"):
- assert 1 < len(data) < len(tag_matrix["tags"])
- elif name == "empty":
- pass
- else:
- raise ValueError(f"Unsupported assertion type: {name}")
-
-
-@patch("pontoon.tags.utils.TagsLatestTranslationsTool.get_data")
-def test_util_tags_translation_tool_data(data_mock):
- # ensures latest translation data is coalesced and cached
- # correctly
- tr_tool = TagsLatestTranslationsTool()
-
- # set up mock return for get_data that can be used like
- # qs.iterator()
- data_m = [
- dict(entity__resource__tag="foo"),
- dict(entity__resource__tag="bar"),
- ]
- data_m2 = [dict(entity__resource__tag="baz")]
- iterator_m = MagicMock()
- iterator_m.iterator.return_value = data_m
- data_mock.return_value = iterator_m
-
- # get data from the tool
- result = tr_tool.data
-
- # we got back data from data_m coalesced to a dictionary
- # with the groupby fields as keys
- assert result == dict(foo=data_m[0], bar=data_m[1])
- assert iterator_m.iterator.called
-
- # lets reset the mock and change the return value
- iterator_m.reset_mock()
- iterator_m.iterator.return_value = data_m2
-
- # and get the data again
- result = tr_tool.data
-
- # which was cached, so nothing changed
- assert not iterator_m.iterator.called
- assert result == dict(foo=data_m[0], bar=data_m[1])
-
- # after deleting the cache...
- del tr_tool.__dict__["data"]
-
- # ...we get the new value
- result = tr_tool.data
- assert iterator_m.iterator.called
- assert result == dict(baz=data_m2[0])
-
-
-@pytest.mark.django_db
-def test_util_tags_translation_tool_groupby(
- tag_matrix,
- tag_test_kwargs,
- calculate_tags_latest,
- user_a,
- user_b,
-):
- name, kwargs = tag_test_kwargs
-
- # hmm, translations have no users
- # - set first 3rd to user_a, and second 3rd to user_b
- total = Translation.objects.count()
- first_third_users = Translation.objects.all()[: total / 3].values_list("pk")
- second_third_users = Translation.objects.all()[
- total / 3 : 2 * total / 3
- ].values_list("pk")
- (Translation.objects.filter(pk__in=first_third_users).update(user=user_a))
- (Translation.objects.filter(pk__in=second_third_users).update(user=user_b))
-
- # calculate expectations grouped by locale
- exp = calculate_tags_latest(groupby="locale", **kwargs)
-
- # calculate data from tool grouped by locale
- tr_tool = TagsLatestTranslationsTool(groupby="locale", **kwargs)
- data = tr_tool.coalesce(tr_tool.get_data())
-
- # get a pk dictionary of all translations
- translations = Translation.objects.select_related("user").in_bulk()
-
- assert len(data) == len(exp)
-
- for k, (pk, date) in exp.items():
- # check all of the expected values are correct for the
- # translation and user
- translation = translations.get(pk)
- assert data[k]["date"] == date
- assert data[k]["string"] == translation.string
- assert data[k]["approved_date"] == translation.approved_date
- user = translation.user
- if user:
- assert data[k]["user__email"] == user.email
- assert data[k]["user__first_name"] == user.first_name
- else:
- assert data[k]["user__email"] is None
- assert data[k]["user__first_name"] is None
diff --git a/pontoon/tags/utils.py b/pontoon/tags/utils.py
new file mode 100644
index 0000000000..e455198525
--- /dev/null
+++ b/pontoon/tags/utils.py
@@ -0,0 +1,115 @@
+from django.db.models import Q, Max, Sum
+
+from pontoon.base.models import TranslatedResource, Translation
+from pontoon.tags.models import Tag
+
+
+class Tags:
+ """This provides an API for retrieving related ``Tags`` for given filters,
+ providing statistical information and latest activity data.
+ """
+
+ def __init__(self, **kwargs):
+ self.project = kwargs.get("project")
+ self.locale = kwargs.get("locale")
+ self.slug = kwargs.get("slug")
+ self.tag = Tag.objects.filter(project=self.project, slug=self.slug).first()
+
+ def get(self):
+ tags = (
+ Tag.objects.filter(project=self.project, resources__isnull=False)
+ .distinct()
+ .order_by("-priority", "name")
+ )
+
+ chart = self.chart(Q(), "resource__tag")
+ latest_activity = self.latest_activity(Q(), "resource__tag")
+ for tag in tags:
+ tag.chart = chart.get(tag.pk)
+ tag.latest_activity = latest_activity.get(tag.pk)
+
+ return tags
+
+ def get_tag_locales(self):
+ tag = self.tag
+
+ if tag is None:
+ return None
+
+ chart = self.chart(Q(resource__tag=self.tag), "resource__tag")
+ tag.chart = chart.get(tag.pk)
+ tag.locales = self.project.locales.all()
+
+ locale_chart = self.chart(Q(resource__tag=self.tag), "locale")
+ locale_latest_activity = self.latest_activity(
+ Q(resource__tag=self.tag), "locale"
+ )
+ for locale in tag.locales:
+ locale.chart = locale_chart.get(locale.pk)
+ locale.latest_activity = locale_latest_activity.get(locale.pk)
+
+ return tag
+
+ def chart(self, query, group_by):
+ trs = (
+ self.translated_resources.filter(query)
+ .values(group_by)
+ .annotate(
+ total_strings=Sum("resource__total_strings"),
+ approved_strings=Sum("approved_strings"),
+ pretranslated_strings=Sum("pretranslated_strings"),
+ strings_with_errors=Sum("strings_with_errors"),
+ strings_with_warnings=Sum("strings_with_warnings"),
+ unreviewed_strings=Sum("unreviewed_strings"),
+ )
+ )
+
+ return {
+ tr[group_by]: TranslatedResource.get_chart_dict(
+ TranslatedResource(**{key: tr[key] for key in list(tr.keys())[1:]})
+ )
+ for tr in trs
+ }
+
+ def latest_activity(self, query, group_by):
+ latest_activity = {}
+ dates = {}
+ translations = Translation.objects.none()
+
+ trs = (
+ self.translated_resources.exclude(latest_translation__isnull=True)
+ .filter(query)
+ .values(group_by)
+ .annotate(
+ date=Max("latest_translation__date"),
+ approved_date=Max("latest_translation__approved_date"),
+ )
+ )
+
+ for tr in trs:
+ date = max(tr["date"], tr["approved_date"] or tr["date"])
+ dates[date] = tr[group_by]
+ prefix = "entity__" if group_by == "resource__tag" else ""
+
+ # Find translations with matching date and tag/locale
+ translations |= Translation.objects.filter(
+ Q(**{"date": date, f"{prefix}{group_by}": tr[group_by]})
+ ).prefetch_related("user", "approved_user")
+
+ for t in translations:
+ key = dates[t.latest_activity["date"]]
+ latest_activity[key] = t.latest_activity
+
+ return latest_activity
+
+ @property
+ def translated_resources(self):
+ trs = TranslatedResource.objects
+
+ if self.project is not None:
+ trs = trs.filter(resource__project=self.project)
+
+ if self.locale is not None:
+ trs = trs.filter(locale=self.locale)
+
+ return trs
diff --git a/pontoon/tags/utils/__init__.py b/pontoon/tags/utils/__init__.py
deleted file mode 100644
index 44d19cf824..0000000000
--- a/pontoon/tags/utils/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from .latest_activity import LatestActivity, LatestActivityUser
-from .resources import TagsResourcesTool
-from .stats import TagsStatsTool
-from .tagged import TaggedLocale
-from .tag import TagTool
-from .tags import TagsTool
-from .translations import TagsLatestTranslationsTool
-
-
-__all__ = (
- "LatestActivity",
- "LatestActivityTranslation",
- "LatestActivityUser",
- "TagChart",
- "TaggedLocale",
- "TagsLatestTranslationsTool",
- "TagsResourcesTool",
- "TagsStatsTool",
- "TagsTool",
- "TagTool",
-)
diff --git a/pontoon/tags/utils/chart.py b/pontoon/tags/utils/chart.py
deleted file mode 100644
index 7b091a4472..0000000000
--- a/pontoon/tags/utils/chart.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import math
-
-
-class TagChart:
- def __init__(self, **kwargs):
- self.kwargs = kwargs
- self.approved_strings = kwargs.get("approved_strings")
- self.pretranslated_strings = kwargs.get("pretranslated_strings")
- self.strings_with_warnings = kwargs.get("strings_with_warnings")
- self.strings_with_errors = kwargs.get("strings_with_errors")
- self.total_strings = kwargs.get("total_strings")
- self.unreviewed_strings = kwargs.get("unreviewed_strings")
-
- @property
- def completion_percent(self):
- return int(
- math.floor(
- (
- self.approved_strings
- + self.pretranslated_strings
- + self.strings_with_warnings
- )
- / float(self.total_strings)
- * 100
- )
- )
-
- @property
- def approved_share(self):
- return self._share(self.approved_strings)
-
- @property
- def pretranslated_share(self):
- return self._share(self.pretranslated_strings)
-
- @property
- def warnings_share(self):
- return self._share(self.strings_with_warnings)
-
- @property
- def errors_share(self):
- return self._share(self.strings_with_errors)
-
- @property
- def unreviewed_share(self):
- return self._share(self.unreviewed_strings)
-
- def _share(self, item):
- return round(item / float(self.total_strings) * 100) or 0
diff --git a/pontoon/tags/utils/latest_activity.py b/pontoon/tags/utils/latest_activity.py
deleted file mode 100644
index 159c10e44a..0000000000
--- a/pontoon/tags/utils/latest_activity.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# The classes here provide similar functionality to
-# ProjectLocale.get_latest_activity in mangling latest activity data,
-# although they use queryset `values` rather than objects
-from pontoon.base.models import user_gravatar_url
-
-
-class LatestActivityUser:
- def __init__(self, activity, activity_type):
- self.activity = activity
- self.type = activity_type
-
- @property
- def prefix(self):
- return "approved_" if self.type == "approved" else ""
-
- @property
- def email(self):
- return self.activity.get(self.prefix + "user__email")
-
- @property
- def first_name(self):
- return self.activity.get(self.prefix + "user__first_name")
-
- @property
- def name_or_email(self):
- return self.first_name or self.email
-
- @property
- def display_name(self):
- return self.first_name or self.email.split("@")[0]
-
- @property
- def username(self):
- return self.activity.get(self.prefix + "user__username")
-
- def gravatar_url(self, *args):
- if self.email:
- return user_gravatar_url(self, *args)
-
-
-class LatestActivity:
- def __init__(self, activity):
- self.activity = activity
-
- @property
- def approved_date(self):
- return self.activity.get("approved_date")
-
- @property
- def date(self):
- if self.type == "approved":
- return self.approved_date
- return self.activity.get("date")
-
- @property
- def translation(self):
- return dict(string=self.activity.get("string", ""))
-
- @property
- def user(self):
- return (
- LatestActivityUser(self.activity, self.type)
- if "user__email" in self.activity or "approved_user__email" in self.activity
- else None
- )
-
- @property
- def type(self):
- if self.approved_date is not None and self.approved_date > self.activity.get(
- "date"
- ):
- return "approved"
- return "submitted"
diff --git a/pontoon/tags/utils/stats.py b/pontoon/tags/utils/stats.py
deleted file mode 100644
index 5660a6ec9c..0000000000
--- a/pontoon/tags/utils/stats.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# The classes here provide similar functionality to
-# TranslatedResource.stats in mangling stats data,
-# although they use queryset `values` rather than objects
-
-from django.db.models import F, Sum, Value
-from django.db.models.functions import Coalesce
-
-from .base import TagsTRTool
-
-
-class TagsStatsTool(TagsTRTool):
- """Creates aggregated stat data for tags according to
- filters
- """
-
- coalesce = list
-
- filter_methods = ("tag", "projects", "locales", "path")
-
- # from the perspective of translated resources
- _default_annotations = (
- ("total_strings", Coalesce(Sum("resource__total_strings"), Value(0))),
- ("pretranslated_strings", Coalesce(Sum("pretranslated_strings"), Value(0))),
- ("strings_with_warnings", Coalesce(Sum("strings_with_warnings"), Value(0))),
- ("strings_with_errors", Coalesce(Sum("strings_with_errors"), Value(0))),
- ("approved_strings", Coalesce(Sum("approved_strings"), Value(0))),
- ("unreviewed_strings", Coalesce(Sum("unreviewed_strings"), Value(0))),
- )
-
- def get_data(self):
- """Stats can be generated either grouping by tag or by locale
-
- Once the tags/locales are found a second query is made to get
- their data
-
- """
- if self.get_groupby()[0] == "resource__tag":
- stats = {
- stat["resource__tag"]: stat
- for stat in super(TagsStatsTool, self).get_data()
- }
-
- # get the found tags as values
- tags = self.tag_manager.filter(pk__in=stats.keys())
- tags = tags.values("pk", "slug", "name", "priority", "project")
- tags = tags.annotate(resource__tag=F("pk"))
- for tag in tags:
- # update the stats with tag data
- tag.update(stats[tag["pk"]])
- return tags
- elif self.get_groupby()[0] == "locale":
- result = list(super().get_data())
- # get the found locales as values
- locales = {
- loc["pk"]: loc
- for loc in self.locale_manager.filter(
- pk__in=(r["locale"] for r in result)
- ).values("pk", "name", "code", "population")
- }
- for r in result:
- # update the stats with locale data
- r.update(locales[r["locale"]])
- return sorted(result, key=lambda r: r["name"])
diff --git a/pontoon/tags/utils/tagged.py b/pontoon/tags/utils/tagged.py
deleted file mode 100644
index b9fc64a6d6..0000000000
--- a/pontoon/tags/utils/tagged.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from .latest_activity import LatestActivity
-from .chart import TagChart
-
-
-class Tagged:
- """Base class for wrapping `values` dictionaries of related
- tag information
- """
-
- def __init__(self, **kwargs):
- self._latest_translation = kwargs.pop("latest_translation", None)
- self.approved_strings = kwargs.get("approved_strings")
- self.pretranslated_strings = kwargs.get("pretranslated_strings")
- self.strings_with_warnings = kwargs.get("strings_with_warnings")
- self.strings_with_errors = kwargs.get("strings_with_errors")
- self.total_strings = kwargs.get("total_strings")
- self.unreviewed_strings = kwargs.get("unreviewed_strings")
- self.kwargs = kwargs
-
- @property
- def chart(self):
- """Generate a dict of chart information"""
- return TagChart(**self.kwargs) if self.total_strings else None
-
- @property
- def latest_translation(self):
- return self._latest_translation
-
- @property
- def latest_activity(self):
- """Returns wrapped LatestActivity data if available"""
- return (
- LatestActivity(self.latest_translation) if self.latest_translation else None
- )
-
- @property
- def tag(self):
- return self.kwargs.get("slug")
-
- def get_latest_activity(self, x):
- return self.latest_activity
-
- def get_chart(self, x):
- return self.chart
-
-
-class TaggedLocale(Tagged):
- """Wraps a Locale to provide stats and latest information"""
-
- @property
- def code(self):
- return self.kwargs.get("code")
-
- @property
- def name(self):
- return self.kwargs.get("name")
-
- @property
- def population(self):
- return self.kwargs.get("population")
-
- @property
- def project(self):
- return self.kwargs.get("project")
diff --git a/pontoon/tags/utils/translations.py b/pontoon/tags/utils/translations.py
deleted file mode 100644
index 68bc125638..0000000000
--- a/pontoon/tags/utils/translations.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from django.db.models import Max, Q
-
-from .base import TagsTRTool
-
-
-class TagsLatestTranslationsTool(TagsTRTool):
- """For given filters this tool will find the latest ``Translations``
- for a ``Tag``. It uses TranslatedResources to find the translations
- but returns translations.
- """
-
- filter_methods = ("tag", "projects", "latest", "locales", "path")
-
- _default_annotations = (
- ("date", Max("latest_translation__date")),
- ("approved_date", Max("latest_translation__approved_date")),
- )
-
- @property
- def groupby_prefix(self):
- # as we find latest_translations for translated_resources
- # and use that to retrieve the translations, we need to map the groupby
- # field here
- groupby = list(self.get_groupby())
- if groupby == ["resource__tag"]:
- return "entity__resource__tag"
- elif groupby == ["locale"]:
- return "locale"
-
- def coalesce(self, data):
- return {
- translation[self.groupby_prefix]: translation
- for translation in data.iterator()
- }
-
- def get_data(self):
- _translations = self.translation_manager.none()
- stats = super().get_data()
-
- for tr in stats.iterator():
- if tr["approved_date"] is not None and tr["approved_date"] > tr["date"]:
- key = "approved_date"
- else:
- key = "date"
-
- # find translations with matching date and tag/locale
- _translations |= self.translation_manager.filter(
- Q(**{key: tr[key], self.groupby_prefix: tr[self.get_groupby()[0]]})
- )
-
- return _translations.values(
- *(
- "string",
- "date",
- "approved_date",
- "approved_user__email",
- "approved_user__first_name",
- "approved_user__username",
- "user__email",
- "user__first_name",
- "user__username",
- )
- + (self.groupby_prefix,)
- )
-
- def filter_latest(self, qs):
- return qs.exclude(latest_translation__isnull=True)
diff --git a/pontoon/tags/views.py b/pontoon/tags/views.py
index 5f79403ac4..cde2ca847f 100644
--- a/pontoon/tags/views.py
+++ b/pontoon/tags/views.py
@@ -1,8 +1,8 @@
from django.http import Http404
-from .utils import TagsTool
from pontoon.base.models import Project
from pontoon.base.utils import is_ajax
+from pontoon.tags.utils import Tags
from django.views.generic import DetailView
@@ -29,18 +29,16 @@ def get_AJAX(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
- try:
- tag = TagsTool(
- projects=[self.object],
- priority=True,
- )[self.kwargs["tag"]].get()
- except IndexError:
+ tags = Tags(project=self.object, slug=self.kwargs["tag"])
+ tag = tags.get_tag_locales()
+
+ if not tag:
raise Http404
if is_ajax(self.request):
return dict(
project=self.object,
- locales=list(tag.iter_locales()),
+ locales=tag.locales,
tag=tag,
)