Skip to content

Commit

Permalink
Introduce project manager status banner (mozilla#3422)
Browse files Browse the repository at this point in the history
1. Add a banner for users defined as "Project Manager" within a project. To reduce confusion, the MNGR tooltip has been changed from from "Manager" to "Team Manager".

2. Consolidate roles between backend and frontend:
* If a user as a role within the locale (translator, manager), we use that for the banner
* If a user is set as PM, we use that even if the user is an Admin
* The isAdmin flag is true if the user is a superuser, not PM
* Introduce isPM flag and use it where isAdmin was used before
* Rename: managerForLocales -> canManageLocale, translatorForLocales -> canTranslateLocales
* Introduce managerForLocales and translatorForLocales and use them in UserStatus instead of canManageLocales and canTranslateLocales

3. Other changes:
* Add CSS variables for users, instead of reusing the ones for translation status
* Ignore system users for banners
* Use status identifier as a class name
* The term role is already taken, let's settle for status consistently

---------

Co-authored-by: Matjaž Horvat <[email protected]>
  • Loading branch information
flodolo and mathjazz authored Nov 5, 2024
1 parent 5b4a3c1 commit 4b23a5d
Show file tree
Hide file tree
Showing 30 changed files with 220 additions and 94 deletions.
2 changes: 1 addition & 1 deletion pontoon/administration/static/css/admin_project.css
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ textarea.strings-source {
color: var(--status-error);
display: inline-block;
font-size: 12px;
line-height: 14px; /* Strange, but needed to keep controls in line when error occures */
line-height: 14px; /* Strange, but needed to keep controls in line when error occurs */
list-style: none;
margin-left: 0;
margin-top: 2px;
Expand Down
4 changes: 2 additions & 2 deletions pontoon/base/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ class Comment(models.Model):
def __str__(self):
return self.content

def serialize(self):
def serialize(self, project_contact):
locale = self.locale or self.translation.locale
return {
"author": self.author.name_or_email,
"username": self.author.username,
"user_status": self.author.status(locale),
"user_status": self.author.status(locale, project_contact),
"user_gravatar_url_small": self.author.gravatar_url(88),
"created_at": self.timestamp.strftime("%b %d, %Y %H:%M"),
"date_iso": self.timestamp.isoformat(),
Expand Down
26 changes: 14 additions & 12 deletions pontoon/base/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def user_manager_for_locales(self):


@property
def user_translated_locales(self):
def user_can_translate_locales(self):
"""A list of locale codes the user has permission to translate.
Includes all locales for superusers.
Expand All @@ -114,7 +114,7 @@ def user_translated_locales(self):


@property
def user_managed_locales(self):
def user_can_manage_locales(self):
"""A list of locale codes the user has permission to manage.
Includes all locales for superusers.
Expand Down Expand Up @@ -164,18 +164,18 @@ def user_role(self, managers=None, translators=None):
if self in managers:
return "Manager for " + ", ".join(managers[self])
else:
if self.managed_locales:
if self.can_manage_locales:
return "Manager for " + ", ".join(
self.managed_locales.values_list("code", flat=True)
self.can_manage_locales.values_list("code", flat=True)
)

if translators is not None:
if self in translators:
return "Translator for " + ", ".join(translators[self])
else:
if self.translated_locales:
if self.can_translate_locales:
return "Translator for " + ", ".join(
self.translated_locales.values_list("code", flat=True)
self.can_translate_locales.values_list("code", flat=True)
)

return "Contributor"
Expand All @@ -194,13 +194,15 @@ def user_locale_role(self, locale):
return "Contributor"


def user_status(self, locale):
if self.username == "Imported":
def user_status(self, locale, project_contact):
if self.pk is None or self.profile.system_user:
return ("", "")
if self in locale.managers_group.user_set.all():
return ("MNGR", "Manager")
return ("MNGR", "Team Manager")
if self in locale.translators_group.user_set.all():
return ("TRNSL", "Translator")
if project_contact and self.pk == project_contact.pk:
return ("PM", "Project Manager")
if self.is_superuser:
return ("ADMIN", "Admin")
if self.date_joined >= timezone.now() - relativedelta(months=3):
Expand Down Expand Up @@ -269,7 +271,7 @@ def can_translate(self, locale, project):
from pontoon.base.models.project_locale import ProjectLocale

# Locale managers can translate all projects
if locale in self.managed_locales:
if locale in self.can_manage_locales:
return True

project_locale = ProjectLocale.objects.get(project=project, locale=locale)
Expand Down Expand Up @@ -465,8 +467,8 @@ def latest_action(self):
User.add_to_class("display_name_or_blank", user_display_name_or_blank)
User.add_to_class("translator_for_locales", user_translator_for_locales)
User.add_to_class("manager_for_locales", user_manager_for_locales)
User.add_to_class("translated_locales", user_translated_locales)
User.add_to_class("managed_locales", user_managed_locales)
User.add_to_class("can_translate_locales", user_can_translate_locales)
User.add_to_class("can_manage_locales", user_can_manage_locales)
User.add_to_class("translated_projects", user_translated_projects)
User.add_to_class("role", user_role)
User.add_to_class("locale_role", user_locale_role)
Expand Down
7 changes: 7 additions & 0 deletions pontoon/base/static/css/dark-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
--main-border-1: #4d5967;
--moz-logo: url(../img/moz-logo-light.svg);

/* User banner and details */
--user-admin: #ff3366;
--user-pm: #ffa10f;
--user-manager: #4fc4f6;
--user-translator: #7bc876;
--user-new: #fed271;

/* Primary (darker) background */
--background-1: #272a2f;
--background-hover-1: #333941;
Expand Down
7 changes: 7 additions & 0 deletions pontoon/base/static/css/light-theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
--main-border-1: #d8d8d8;
--moz-logo: url(../img/moz-logo.svg);

/* User banner and details */
--user-admin: #ff3366;
--user-pm: #ffa10f;
--user-manager: #4fc4f6;
--user-translator: #7bc876;
--user-new: #fed271;

/* Primary (darker) background */
--background-1: #f6f6f6;
--background-hover-1: #ffffff;
Expand Down
15 changes: 10 additions & 5 deletions pontoon/base/tests/models/test_comment.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import pytest

from pontoon.test.factories import TeamCommentFactory, TranslationCommentFactory
from pontoon.test.factories import (
ProjectFactory,
TeamCommentFactory,
TranslationCommentFactory,
)


@pytest.mark.django_db
def test_serialize_comments():
tr = TranslationCommentFactory.create()
team = TeamCommentFactory.create()
project = ProjectFactory.create()

assert tr.serialize() == {
assert tr.serialize(project.contact) == {
"author": tr.author.name_or_email,
"username": tr.author.username,
"user_status": tr.author.status(tr.translation.locale),
"user_status": tr.author.status(tr.translation.locale, project.contact),
"user_gravatar_url_small": tr.author.gravatar_url(88),
"created_at": tr.timestamp.strftime("%b %d, %Y %H:%M"),
"date_iso": tr.timestamp.isoformat(),
Expand All @@ -20,10 +25,10 @@ def test_serialize_comments():
"id": tr.id,
}

assert team.serialize() == {
assert team.serialize(project.contact) == {
"author": team.author.name_or_email,
"username": team.author.username,
"user_status": team.author.status(team.locale),
"user_status": team.author.status(team.locale, project.contact),
"user_gravatar_url_small": team.author.gravatar_url(88),
"created_at": team.timestamp.strftime("%b %d, %Y %H:%M"),
"date_iso": team.timestamp.isoformat(),
Expand Down
21 changes: 15 additions & 6 deletions pontoon/base/tests/models/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,31 @@ def test_user_locale_role(user_a, user_b, user_c, locale_a):


@pytest.mark.django_db
def test_user_status(user_a, user_b, user_c, locale_a):
def test_user_status(user_a, user_b, user_c, user_d, gt_user, locale_a, project_a):
project_contact = user_d

# New User
assert user_a.status(locale_a)[1] == "New User"
assert user_a.status(locale_a, project_contact)[1] == "New User"

# Fake user object
imported = User(username="Imported")
assert imported.status(locale_a)[1] == ""
assert imported.status(locale_a, project_contact)[1] == ""

# Admin
user_a.is_superuser = True
assert user_a.status(locale_a)[1] == "Admin"
assert user_a.status(locale_a, project_contact)[1] == "Admin"

# Manager
locale_a.managers_group.user_set.add(user_b)
assert user_b.status(locale_a)[1] == "Manager"
assert user_b.status(locale_a, project_contact)[1] == "Team Manager"

# Translator
locale_a.translators_group.user_set.add(user_c)
assert user_c.status(locale_a)[1] == "Translator"
assert user_c.status(locale_a, project_contact)[1] == "Translator"

# PM
assert user_d.status(locale_a, project_contact)[1] == "Project Manager"

# System user (Google Translate)
project_contact = gt_user
assert gt_user.status(locale_a, project_contact)[1] == ""
23 changes: 15 additions & 8 deletions pontoon/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ def get_translation_history(request):

entity = get_object_or_404(Entity, pk=entity)
locale = get_object_or_404(Locale, code=locale)
project_contact = entity.resource.project.contact

translations = Translation.objects.filter(
entity=entity,
Expand Down Expand Up @@ -430,13 +431,13 @@ def get_translation_history(request):
"uid": u.id,
"username": u.username,
"user_gravatar_url_small": u.gravatar_url(88),
"user_status": u.status(locale),
"user_status": u.status(locale, project_contact),
"date": t.date,
"approved_user": User.display_name_or_blank(t.approved_user),
"approved_date": t.approved_date,
"rejected_user": User.display_name_or_blank(t.rejected_user),
"rejected_date": t.rejected_date,
"comments": [c.serialize() for c in t.comments.all()],
"comments": [c.serialize(project_contact) for c in t.comments.all()],
"machinery_sources": t.machinery_sources_values,
}
)
Expand All @@ -459,13 +460,15 @@ def get_team_comments(request):

entity = get_object_or_404(Entity, pk=entity)
locale = get_object_or_404(Locale, code=locale)
project_contact = entity.resource.project.contact

comments = (
Comment.objects.filter(entity=entity)
.filter(Q(locale=locale) | Q(pinned=True))
.order_by("timestamp")
)

payload = [c.serialize() for c in comments]
payload = [c.serialize(project_contact) for c in comments]

return JsonResponse(payload, safe=False)

Expand Down Expand Up @@ -873,7 +876,8 @@ def user_data(request):
return JsonResponse(
{
"is_authenticated": True,
"is_admin": user.has_perm("base.can_manage_project"),
"is_admin": user.is_superuser,
"is_pm": user.has_perm("base.can_manage_project"),
"id": user.id,
"email": user.email,
"display_name": user.display_name,
Expand All @@ -883,12 +887,15 @@ def user_data(request):
"contributor_for_locales": list(
user.translation_set.values_list("locale__code", flat=True).distinct()
),
"manager_for_locales": list(
user.managed_locales.values_list("code", flat=True)
"can_manage_locales": list(
user.can_manage_locales.values_list("code", flat=True)
),
"translator_for_locales": list(
user.translated_locales.values_list("code", flat=True)
"can_translate_locales": list(
user.can_translate_locales.values_list("code", flat=True)
),
"manager_for_locales": [loc.code for loc in user.manager_for_locales],
"translator_for_locales": [loc.code for loc in user.translator_for_locales],
"pm_for_projects": list(user.contact_for.values_list("slug", flat=True)),
"translator_for_projects": user.translated_projects,
"settings": {
"quality_checks": user.profile.quality_checks,
Expand Down
4 changes: 2 additions & 2 deletions pontoon/contributors/static/css/profile.css
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ h4 {
}

h4.superuser {
border: 1px solid var(--status-error);
border: 1px solid var(--user-admin);
border-radius: 16px;
line-height: 30px;
text-align: center;
color: var(--status-error);
color: var(--user-admin);
}

/* Approval Ratio */
Expand Down
4 changes: 2 additions & 2 deletions pontoon/contributors/templates/contributors/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{% set has_badges = badges.review_master_badge.level or badges.translation_champion_badge.level or badges.community_builder_badge.level %}
{% set translator_for_locales = contributor.translator_for_locales %}
{% set manager_for_locales = contributor.manager_for_locales %}
{% set user_is_translator = user.translated_locales %}
{% set user_is_translator = user.can_translate_locales %}
{% set profile_is_disabled = contributor.is_active == False %}

<a class="avatar" href="{% if is_my_profile %}https://gravatar.com/{% endif %}">
Expand Down Expand Up @@ -227,7 +227,7 @@ <h4 class="subtitle superuser">Administrator</h4>
<div class="right-column">

{% set approval_rate_visibile = profile.visibility_approval == "Public" or user_is_translator %}
{% set contributor_is_translator = contributor.translated_locales %}
{% set contributor_is_translator = contributor.can_translate_locales %}
{% set self_approval_rate_visibile = contributor_is_translator and (profile.visibility_self_approval == "Public" or user_is_translator) %}

<div class="clearfix">
Expand Down
2 changes: 1 addition & 1 deletion pontoon/contributors/templates/contributors/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ <h3>Editor</h3>
<ul class="check-list">
{{ Checkbox.checkbox('Translate Toolkit checks', class='quality-checks', attribute='quality_checks', is_enabled=user.profile.quality_checks, title='Run Translate Toolkit checks before submitting translations') }}

{% if user.translated_locales %}
{% if user.can_translate_locales %}
{{ Checkbox.checkbox('Make suggestions', class='force-suggestions', attribute='force_suggestions', is_enabled=user.profile.force_suggestions, title='Save suggestions instead of translations') }}
{% endif %}
</ul>
Expand Down
4 changes: 2 additions & 2 deletions pontoon/teams/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def ajax_projects(request, locale):

pretranslation_request_enabled = (
request.user.is_authenticated
and locale in request.user.translated_locales
and locale in request.user.can_translate_locales
and locale.code in settings.GOOGLE_AUTOML_SUPPORTED_LOCALES
and pretranslated_projects.count() < enabled_projects.count()
)
Expand Down Expand Up @@ -347,7 +347,7 @@ def request_pretranslation(request, locale):
locale = get_object_or_404(Locale, code=locale)

# Validate user
if locale not in user.translated_locales:
if locale not in user.can_translate_locales:
return HttpResponseBadRequest(
"Bad Request: Requester is not a translator or manager for the locale"
)
Expand Down
5 changes: 5 additions & 0 deletions pontoon/test/fixtures/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ def user_c():
return factories.UserFactory(username="user_c")


@pytest.fixture
def user_d():
return factories.UserFactory(username="user_d")


@pytest.fixture
def member(client, user_a):
"""Provides a `LoggedInMember` with the attributes `user` and `client`
Expand Down
5 changes: 3 additions & 2 deletions specs/0119-gamification-badges.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ The badge system consists of various badges awarded based on user activities, su
**Icons:**
- **New User Icon:** For users who joined less than 3 months ago.
- **Translator Icon:** For users contributing as translators.
- **Manager Icon:** For users serving as locale managers.
- **Team Manager Icon:** For users serving as locale managers.
- **Project Manager Icon:** For users serving as project managers.
- **Admin Icon:** For users holding administrative roles.

# Technical Specification
Expand Down Expand Up @@ -78,4 +79,4 @@ The badge system will be integrated into the existing Pontoon interface, with ba

# Out of Scope
### Progress Bar
A visual progress bar for badges with different levels to indicate how close a user is to reaching the next level.
A visual progress bar for badges with different levels to indicate how close a user is to reaching the next level.
5 changes: 5 additions & 0 deletions translate/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,20 @@ export type Notification = {
export type ApiUserData = {
is_authenticated?: boolean;
is_admin?: boolean;
is_pm?: boolean;
id?: string;
display_name?: string;
name_or_email?: string;
email?: string;
username?: string;
date_joined?: string;
can_manage_locales?: string[];
can_translate_locales?: string[];
manager_for_locales?: string[];
translator_for_locales?: string[];
contributor_for_locales?: string[];
translator_for_projects?: Record<string, boolean>;
pm_for_projects?: string[];
settings?: { quality_checks: boolean; force_suggestions: boolean };
tour_status?: number;
has_dismissed_addon_promotion?: boolean;
Expand Down
Loading

0 comments on commit 4b23a5d

Please sign in to comment.