Repository: mozilla/pontoon

Issue:
Cannot add team translators/managers to per-project translators
**STR:**
1. Have a user added to Global translators.
2. Visit https://pontoon.mozilla.org/cs/permissions/ (or reload it).
3. Paste their e-mail to a per-project permissions to find them in the Team contributors list.

**Whan should happen:**
The user is found in the Team contributors list and can be added to the per-project permissions as a translator.

**What actually happens:**
The user is not on the list.

**Possible workaround:**
1. Remove the user from the Global translators.
2. Reload https://pontoon.mozilla.org/cs/permissions/.
3. Find the user in all per-project Team contributors lists and click to add the as translators, and do the same for the General permissions.
4. Only then click the _Save_ button at the bottom of the page.


Discussion:

Resolving patch:
diff --git a/pontoon/base/models.py b/pontoon/base/models.py
--- a/pontoon/base/models.py
+++ b/pontoon/base/models.py
@@ -998,78 +998,6 @@ def cldr_id_to_plural(self, cldr_id):
     def nplurals(self):
         return len(self.cldr_id_list())
 
-    def projects_permissions(self, user):
-        """
-        List of tuples that contain informations required by the locale permissions view.
-
-        Projects are filtered by their visibility for the user.
-        A row contains:
-            id  - id of project locale
-            slug - slug of the project
-            name - name of the project
-            translators - (id, first_name, email) translators assigned to a project locale.
-            contributors - (id, first_name, email) people who contributed for a project in
-                current locale.
-            has_custom_translators - Flag if locale has a translators group for the project.
-        """
-        locale_projects = []
-
-        def create_users_map(qs, keyfield):
-            """
-            Creates a map from query set
-            """
-            user_map = defaultdict(list)
-            for row in qs:
-                key = row.pop(keyfield)
-                user_map[key].append(row)
-            return user_map
-
-        project_locales = list(
-            self.project_locale.visible()
-            .visible_for(user)
-            .prefetch_related("project", "translators_group")
-            .order_by("project__name")
-            .values(
-                "id",
-                "project__pk",
-                "project__name",
-                "project__slug",
-                "translators_group__pk",
-                "has_custom_translators",
-            )
-        )
-
-        projects_translators = create_users_map(
-            (
-                User.objects.filter(
-                    groups__pk__in=[
-                        project_locale["translators_group__pk"]
-                        for project_locale in project_locales
-                    ]
-                )
-                .exclude(email="")
-                .prefetch_related("groups")
-                .values("id", "first_name", "email", "groups__pk")
-                .distinct()
-                .order_by("email")
-            ),
-            "groups__pk",
-        )
-
-        for project_locale in project_locales:
-            group_pk = project_locale["translators_group__pk"]
-            locale_projects.append(
-                (
-                    project_locale["id"],
-                    project_locale["project__slug"],
-                    project_locale["project__name"],
-                    projects_translators[group_pk],
-                    project_locale["has_custom_translators"],
-                )
-            )
-
-        return locale_projects
-
     def available_projects_list(self, user):
         """Get a list of available project slugs."""
         return list(
diff --git a/pontoon/teams/views.py b/pontoon/teams/views.py
--- a/pontoon/teams/views.py
+++ b/pontoon/teams/views.py
@@ -5,7 +5,7 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.core.mail import EmailMessage
 from django.db import transaction
-from django.db.models import Q, Count
+from django.db.models import Q, Count, Prefetch
 from django.http import (
     Http404,
     HttpResponse,
@@ -21,9 +21,8 @@
 from guardian.decorators import permission_required_or_403
 
 from pontoon.base import forms
-from pontoon.base.models import Locale, Project
+from pontoon.base.models import Locale, Project, User
 from pontoon.base.utils import require_AJAX, get_locale_or_redirect
-from pontoon.contributors.utils import users_with_translations_counts
 from pontoon.contributors.views import ContributorsMixin
 from pontoon.insights.utils import get_locale_insights
 from pontoon.teams.forms import LocaleRequestForm
@@ -192,16 +191,43 @@ def ajax_permissions(request, locale):
     translators = locale.translators_group.user_set.exclude(pk__in=managers).order_by(
         "email"
     )
-    contributors = users_with_translations_counts(
-        None,
-        Q(locale=locale)
-        & Q(user__isnull=False)
-        & Q(user__profile__system_user=False)
-        & ~Q(user__pk__in=managers | translators),
-        None,
+
+    locale_contributors = (
+        User.objects.filter(
+            translation__locale=locale, profile__system_user=False, is_active=True
+        )
+        .distinct()
+        .order_by("email")
     )
+    contributors = [
+        contributor
+        for contributor in locale_contributors
+        if contributor not in managers and contributor not in translators
+    ]
+
+    project_locales = project_locales.prefetch_related(
+        "project",
+        Prefetch(
+            "translators_group__user_set",
+            queryset=User.objects.order_by("email"),
+            to_attr="fetched_translators",
+        ),
+    ).order_by("project__name")
+
+    for project_locale in project_locales:
+        if not project_locale.has_custom_translators:
+            continue
+
+        project_locale.translators = (
+            project_locale.translators_group.fetched_translators
+        )
+        project_locale.contributors = [
+            contributor
+            for contributor in locale_contributors
+            if contributor not in project_locale.translators
+        ]
 
-    locale_projects = locale.projects_permissions(request.user)
+    hide_project_selector = all([pl.has_custom_translators for pl in project_locales])
 
     return render(
         request,
@@ -211,9 +237,9 @@ def ajax_permissions(request, locale):
             "contributors": contributors,
             "translators": translators,
             "managers": managers,
-            "locale_projects": locale_projects,
             "project_locale_form": project_locale_form,
-            "all_projects_in_translation": all([x[4] for x in locale_projects]),
+            "project_locales": project_locales,
+            "hide_project_selector": hide_project_selector,
         },
     )
 


Test patch:
diff --git a/pontoon/teams/tests/test_views.py b/pontoon/teams/tests/test_views.py
--- a/pontoon/teams/tests/test_views.py
+++ b/pontoon/teams/tests/test_views.py
@@ -104,19 +104,27 @@ def test_ajax_permissions_project_locale_translators_order(
     "Permissions" tab.
     """
     project_locale_a.translators_group.user_set.add(*translators)
+    project_locale_a.has_custom_translators = True
+    project_locale_a.save()
 
     admin_client.get("/%s/ajax/permissions/" % locale_a.code)
     response_context = render_mock.call_args[0][2]
-    locale_projects = response_context["locale_projects"]
+    project_locales = response_context["project_locales"]
 
     # Check project_locale id in the permissions form
-    assert locale_projects[0][0] == project_locale_a.pk
+    assert project_locales[0].pk == project_locale_a.pk
 
     # Check project_locale translators
-    translators_list = [
-        {"id": u.id, "email": u.email, "first_name": u.first_name} for u in translators
-    ]
-    assert locale_projects[0][3] == translators_list
+    assert len(project_locales[0].translators) == len(translators)
+    assert project_locales[0].translators[0].pk == translators[0].pk
+    assert project_locales[0].translators[0].email == translators[0].email
+    assert project_locales[0].translators[0].first_name == translators[0].first_name
+    assert project_locales[0].translators[1].pk == translators[1].pk
+    assert project_locales[0].translators[1].email == translators[1].email
+    assert project_locales[0].translators[1].first_name == translators[1].first_name
+    assert project_locales[0].translators[2].pk == translators[2].pk
+    assert project_locales[0].translators[2].email == translators[2].email
+    assert project_locales[0].translators[2].first_name == translators[2].first_name
 
 
 @pytest.mark.django_db