diff --git a/pontoon/tags/tests/test_views.py b/pontoon/tags/tests/test_views.py index e0f8023751..8728a5b67f 100644 --- a/pontoon/tags/tests/test_views.py +++ b/pontoon/tags/tests/test_views.py @@ -5,7 +5,6 @@ from django.urls import reverse from pontoon.base.models import Priority -from pontoon.tags.utils import TagTool @pytest.mark.django_db @@ -144,14 +143,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,8 +161,7 @@ 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 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_tag.py b/pontoon/tags/tests/utils/test_tag.py index 71ba19d750..95ed8b9f16 100644 --- a/pontoon/tags/tests/utils/test_tag.py +++ b/pontoon/tags/tests/utils/test_tag.py @@ -1,88 +1,8 @@ -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}] - - -@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) def test_util_tag_tool_linked_resources(resources_mock): tag_tool = TagTool( @@ -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/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 index 5ea14fb9a0..84ff69fd81 100644 --- a/pontoon/tags/tests/utils/test_tags.py +++ b/pontoon/tags/tests/utils/test_tags.py @@ -4,9 +4,7 @@ from pontoon.tags.models import Tag from pontoon.tags.utils import ( - TagsLatestTranslationsTool, TagsResourcesTool, - TagsStatsTool, TagsTool, TagTool, ) @@ -18,8 +16,6 @@ def test_util_tags_tool(): 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 @@ -28,15 +24,6 @@ def test_util_tags_tool(): 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", [ @@ -53,44 +40,15 @@ def test_util_tags_tool_resources(resources_mock, kwargs): 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): +def test_util_tags_tool_get(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}] @@ -115,67 +73,6 @@ def test_util_tags_tool_getitem(call_mock): 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}) 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/__init__.py b/pontoon/tags/utils/__init__.py index eafebd058d..86c8944852 100644 --- a/pontoon/tags/utils/__init__.py +++ b/pontoon/tags/utils/__init__.py @@ -1,22 +1,11 @@ -from .latest_activity import LatestActivity, LatestActivityUser from .resources import TagsResourcesTool -from .stats import TagsStatsTool -from .tagged import TaggedLocale -from .tag import TagTool from .utils import Tags from .tags import TagsTool -from .translations import TagsLatestTranslationsTool +from .tag import TagTool __all__ = ( - "LatestActivity", - "LatestActivityTranslation", - "LatestActivityUser", - "TagChart", - "TaggedLocale", - "TagsLatestTranslationsTool", "TagsResourcesTool", - "TagsStatsTool", "Tags", "TagsTool", "TagTool", diff --git a/pontoon/tags/utils/base.py b/pontoon/tags/utils/base.py index 7226bb38f2..7ba6f6240c 100644 --- a/pontoon/tags/utils/base.py +++ b/pontoon/tags/utils/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/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/tag.py b/pontoon/tags/utils/tag.py index 22146c8613..f7c2084c19 100644 --- a/pontoon/tags/utils/tag.py +++ b/pontoon/tags/utils/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/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/tags.py b/pontoon/tags/utils/tags.py index 6466ce0187..bc401eceb7 100644 --- a/pontoon/tags/utils/tags.py +++ b/pontoon/tags/utils/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/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)