зеркало из https://github.com/mozilla/pontoon.git
Introduce project manager status banner (#3422)
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 <matjaz.horvat@gmail.com>
This commit is contained in:
Родитель
5b4a3c111c
Коммит
4b23a5dfc9
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -465,8 +467,8 @@ User.add_to_class("display_name_and_email", user_display_name_and_email)
|
|||
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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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] == ""
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 %}">
|
||||
|
@ -227,7 +227,7 @@
|
|||
<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">
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
<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>
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -32,8 +32,8 @@ describe('useTranslator', () => {
|
|||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
managerForLocales: ['mylocale'],
|
||||
translatorForLocales: [],
|
||||
canManageLocales: ['mylocale'],
|
||||
canTranslateLocales: [],
|
||||
translatorForProjects: {},
|
||||
}),
|
||||
);
|
||||
|
@ -44,8 +44,8 @@ describe('useTranslator', () => {
|
|||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
managerForLocales: [],
|
||||
translatorForLocales: ['mylocale'],
|
||||
canManageLocales: [],
|
||||
canTranslateLocales: ['mylocale'],
|
||||
translatorForProjects: {},
|
||||
}),
|
||||
);
|
||||
|
@ -56,8 +56,8 @@ describe('useTranslator', () => {
|
|||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
managerForLocales: ['localeA'],
|
||||
translatorForLocales: ['localeB'],
|
||||
canManageLocales: ['localeA'],
|
||||
canTranslateLocales: ['localeB'],
|
||||
translatorForProjects: { 'mylocale-myproject': true },
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -14,8 +14,8 @@ export function useTranslator(): boolean {
|
|||
const { slug } = useProject();
|
||||
const {
|
||||
isAuthenticated,
|
||||
managerForLocales,
|
||||
translatorForLocales,
|
||||
canManageLocales,
|
||||
canTranslateLocales,
|
||||
translatorForProjects,
|
||||
} = useAppSelector((state) => state[USER]);
|
||||
|
||||
|
@ -23,7 +23,7 @@ export function useTranslator(): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (managerForLocales.includes(code)) {
|
||||
if (canManageLocales.includes(code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -32,5 +32,5 @@ export function useTranslator(): boolean {
|
|||
return translatorForProjects[localeProject];
|
||||
}
|
||||
|
||||
return translatorForLocales.includes(code);
|
||||
return canTranslateLocales.includes(code);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import { useUserStatus } from './useUserStatus';
|
|||
|
||||
beforeAll(() => {
|
||||
sinon.stub(Hooks, 'useAppSelector');
|
||||
sinon.stub(React, 'useContext').returns({ code: 'mylocale' });
|
||||
sinon
|
||||
.stub(React, 'useContext')
|
||||
.returns({ code: 'mylocale', project: 'myproject' });
|
||||
});
|
||||
afterAll(() => {
|
||||
Hooks.useAppSelector.restore();
|
||||
|
@ -31,28 +33,81 @@ describe('useUserStatus', () => {
|
|||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
managerForLocales: ['mylocale'],
|
||||
pmForProjects: [],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: [],
|
||||
}),
|
||||
);
|
||||
expect(useUserStatus()).toStrictEqual(['ADMIN', 'Admin']);
|
||||
});
|
||||
|
||||
it('returns [PM, Project Manager] if user is a project manager for the project', () => {
|
||||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
pmForProjects: ['myproject'],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: [],
|
||||
}),
|
||||
);
|
||||
expect(useUserStatus()).toStrictEqual(['PM', 'Project Manager']);
|
||||
});
|
||||
|
||||
it('returns [PM, Project Manager] if user is a project manager for the project, even if user is an Admin', () => {
|
||||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
pmForProjects: ['myproject'],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: [],
|
||||
}),
|
||||
);
|
||||
expect(useUserStatus()).toStrictEqual(['PM', 'Project Manager']);
|
||||
});
|
||||
|
||||
it('returns [MNGR, Manager] if user is a manager of the locale', () => {
|
||||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
pmForProjects: [],
|
||||
managerForLocales: ['mylocale'],
|
||||
translatorForLocales: [],
|
||||
}),
|
||||
);
|
||||
expect(useUserStatus()).toStrictEqual(['MNGR', 'Manager']);
|
||||
expect(useUserStatus()).toStrictEqual(['MNGR', 'Team Manager']);
|
||||
});
|
||||
|
||||
it('returns [MNGR, Manager] if user is a manager of the locale, even if user is an Admin', () => {
|
||||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
pmForProjects: [],
|
||||
managerForLocales: ['mylocale'],
|
||||
translatorForLocales: [],
|
||||
}),
|
||||
);
|
||||
expect(useUserStatus()).toStrictEqual(['MNGR', 'Team Manager']);
|
||||
});
|
||||
|
||||
it('returns [MNGR, Manager] if user is a manager of the locale, even if user is a Project Manager', () => {
|
||||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
pmForProjects: ['myproject'],
|
||||
managerForLocales: ['mylocale'],
|
||||
translatorForLocales: [],
|
||||
}),
|
||||
);
|
||||
expect(useUserStatus()).toStrictEqual(['MNGR', 'Team Manager']);
|
||||
});
|
||||
|
||||
it('returns [TRNSL, Translator] if user is a translator for the locale', () => {
|
||||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
pmForProjects: [],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: ['mylocale'],
|
||||
}),
|
||||
|
@ -66,6 +121,7 @@ describe('useUserStatus', () => {
|
|||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
pmForProjects: [],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: [],
|
||||
dateJoined: dateJoined,
|
||||
|
@ -78,6 +134,7 @@ describe('useUserStatus', () => {
|
|||
Hooks.useAppSelector.callsFake(
|
||||
fakeSelector({
|
||||
isAuthenticated: true,
|
||||
pmForProjects: [],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: [],
|
||||
dateJoined: dateJoined,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useContext } from 'react';
|
||||
|
||||
import { Locale } from '~/context/Locale';
|
||||
import { Location } from '~/context/Location';
|
||||
import { USER } from '~/modules/user';
|
||||
import { useAppSelector } from '~/hooks';
|
||||
|
||||
|
@ -9,11 +10,13 @@ import { useAppSelector } from '~/hooks';
|
|||
*/
|
||||
export function useUserStatus(): Array<string> {
|
||||
const { code } = useContext(Locale);
|
||||
const { project } = useContext(Location);
|
||||
const {
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
managerForLocales,
|
||||
translatorForLocales,
|
||||
pmForProjects,
|
||||
dateJoined,
|
||||
} = useAppSelector((state) => state[USER]);
|
||||
|
||||
|
@ -21,18 +24,23 @@ export function useUserStatus(): Array<string> {
|
|||
return ['', ''];
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
return ['ADMIN', 'Admin'];
|
||||
}
|
||||
|
||||
// Check status within the locale before checking for Project Manager or Admin
|
||||
if (managerForLocales.includes(code)) {
|
||||
return ['MNGR', 'Manager'];
|
||||
return ['MNGR', 'Team Manager'];
|
||||
}
|
||||
|
||||
if (translatorForLocales.includes(code)) {
|
||||
return ['TRNSL', 'Translator'];
|
||||
}
|
||||
|
||||
if (pmForProjects.includes(project)) {
|
||||
return ['PM', 'Project Manager'];
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
return ['ADMIN', 'Admin'];
|
||||
}
|
||||
|
||||
const dateJoinedObj = new Date(dateJoined);
|
||||
let threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||
|
|
|
@ -87,7 +87,7 @@ export function AddComment({
|
|||
const [mentionIndex, setMentionIndex] = useState(0);
|
||||
const [mentionSearch, setMentionSearch] = useState('');
|
||||
const [requireUsers, setRequireUsers] = useState(false);
|
||||
const role = useUserStatus();
|
||||
const status = useUserStatus();
|
||||
|
||||
const { initMentions, mentionUsers } = useContext(MentionUsers);
|
||||
const [slateKey, resetValue] = useReducer((key) => key + 1, 0);
|
||||
|
@ -249,7 +249,7 @@ export function AddComment({
|
|||
<UserAvatar
|
||||
username={username}
|
||||
imageUrl={gravatarURLSmall}
|
||||
userStatus={role}
|
||||
userStatus={status}
|
||||
/>
|
||||
<div className='container'>
|
||||
<Slate
|
||||
|
|
|
@ -19,7 +19,7 @@ jest.mock('react-redux', () => ({
|
|||
selector({
|
||||
user: {
|
||||
isAuthenticated: true,
|
||||
managerForLocales: ['en'],
|
||||
canManageLocales: ['en'],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
|
|
@ -67,7 +67,7 @@ describe('<FailedChecks>', () => {
|
|||
it('renders suggest anyway button if user does not have sufficient permissions', () => {
|
||||
const wrapper = mountFailedChecks(
|
||||
{ errors: [], warnings: ['a warning'], source: 'submitted' },
|
||||
{ manager_for_locales: [] },
|
||||
{ can_manage_locales: [] },
|
||||
);
|
||||
|
||||
expect(wrapper.find('.suggest.anyway')).toHaveLength(1);
|
||||
|
|
|
@ -141,7 +141,7 @@ describe('<EntitiesList>', () => {
|
|||
});
|
||||
|
||||
// HACK to get isTranslator === true in Entity
|
||||
createDefaultUser(store, { translator_for_locales: [''] });
|
||||
createDefaultUser(store, { can_translate_locales: [''] });
|
||||
|
||||
const wrapper = mountComponentWithStore(EntitiesList, store);
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export function TeamComments({
|
|||
const renderComment = (comment: TranslationComment) => (
|
||||
<Comment
|
||||
comment={comment}
|
||||
canPin={user.isAdmin}
|
||||
canPin={user.isPM}
|
||||
key={comment.id}
|
||||
togglePinnedStatus={togglePinnedStatus}
|
||||
/>
|
||||
|
|
|
@ -38,18 +38,22 @@
|
|||
font-size: 7px;
|
||||
}
|
||||
|
||||
.user-avatar .avatar-container .user-status-banner.admin {
|
||||
color: var(--status-error);
|
||||
.user-avatar .avatar-container .user-status-banner.ADMIN {
|
||||
color: var(--user-admin);
|
||||
}
|
||||
|
||||
.user-avatar .avatar-container .user-status-banner.manager {
|
||||
color: var(--status-unreviewed);
|
||||
.user-avatar .avatar-container .user-status-banner.PM {
|
||||
color: var(--user-pm);
|
||||
}
|
||||
|
||||
.user-avatar .avatar-container .user-status-banner.translator {
|
||||
color: var(--status-translated);
|
||||
.user-avatar .avatar-container .user-status-banner.MNGR {
|
||||
color: var(--user-manager);
|
||||
}
|
||||
|
||||
.user-avatar .avatar-container .user-status-banner.new-user {
|
||||
color: var(--status-fuzzy);
|
||||
.user-avatar .avatar-container .user-status-banner.TRNSL {
|
||||
color: var(--user-translator);
|
||||
}
|
||||
|
||||
.user-avatar .avatar-container .user-status-banner.NEW {
|
||||
color: var(--user-new);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ type Props = {
|
|||
|
||||
export function UserAvatar(props: Props): React.ReactElement<'div'> {
|
||||
const { username, title, imageUrl, userStatus } = props;
|
||||
const [role, tooltip] = userStatus;
|
||||
const [status, tooltip] = userStatus;
|
||||
|
||||
return (
|
||||
<div className='user-avatar'>
|
||||
|
@ -27,12 +27,9 @@ export function UserAvatar(props: Props): React.ReactElement<'div'> {
|
|||
<Localized id='user-UserAvatar--alt-text' attrs={{ alt: true }}>
|
||||
<img src={imageUrl} alt='User Profile' height='44' width='44' />
|
||||
</Localized>
|
||||
{role && (
|
||||
<span
|
||||
className={`user-status-banner ${tooltip.toLowerCase().split(' ').join('-')}`}
|
||||
title={tooltip}
|
||||
>
|
||||
{role}
|
||||
{status && (
|
||||
<span className={`user-status-banner ${status}`} title={tooltip}>
|
||||
{status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('<UserMenuDialog>', () => {
|
|||
};
|
||||
|
||||
function createUserMenu({
|
||||
isAdmin = false,
|
||||
isPM = false,
|
||||
isReadOnly = false,
|
||||
isTranslator = true,
|
||||
isAuthenticated = true,
|
||||
|
@ -46,7 +46,7 @@ describe('<UserMenuDialog>', () => {
|
|||
<EntityView.Provider
|
||||
value={{ entity: { pk: 42, readonly: isReadOnly } }}
|
||||
>
|
||||
<UserMenuDialog user={{ isAuthenticated, isAdmin }} />
|
||||
<UserMenuDialog user={{ isAuthenticated, isPM }} />
|
||||
</EntityView.Provider>
|
||||
</MockLocalizationProvider>
|
||||
</Location.Provider>,
|
||||
|
@ -92,7 +92,7 @@ describe('<UserMenuDialog>', () => {
|
|||
it('hides admin · current project menu item when translating all projects', () => {
|
||||
const wrapper = createUserMenu({
|
||||
location: { ...LOCATION, project: 'all-projects' },
|
||||
isAdmin: true,
|
||||
isPM: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
|
@ -101,7 +101,7 @@ describe('<UserMenuDialog>', () => {
|
|||
});
|
||||
|
||||
it('shows admin · current project menu item when translating a project', () => {
|
||||
const wrapper = createUserMenu({ isAdmin: true });
|
||||
const wrapper = createUserMenu({ isPM: true });
|
||||
|
||||
expect(wrapper.find('a[href="/admin/projects/proj/"]')).toHaveLength(1);
|
||||
});
|
||||
|
@ -130,7 +130,7 @@ describe('<UserMenuDialog>', () => {
|
|||
});
|
||||
|
||||
it('shows the admin menu items when the user is an admin', () => {
|
||||
const wrapper = createUserMenu({ isAdmin: true });
|
||||
const wrapper = createUserMenu({ isPM: true });
|
||||
|
||||
expect(wrapper.find('a[href="/admin/"]')).toHaveLength(1);
|
||||
expect(wrapper.find('a[href="/admin/projects/proj/"]')).toHaveLength(1);
|
||||
|
|
|
@ -230,7 +230,7 @@ export function UserMenuDialog({
|
|||
|
||||
{user.isAuthenticated && <li className='horizontal-separator'></li>}
|
||||
|
||||
{user.isAdmin && (
|
||||
{user.isPM && (
|
||||
<>
|
||||
<li>
|
||||
<Localized
|
||||
|
|
|
@ -67,16 +67,20 @@ export type Notifications = {
|
|||
export type UserState = {
|
||||
readonly isAuthenticated: boolean | null; // null while loading
|
||||
readonly isAdmin: boolean;
|
||||
readonly isPM: boolean;
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly nameOrEmail: string;
|
||||
readonly email: string;
|
||||
readonly username: string;
|
||||
readonly dateJoined: string;
|
||||
readonly contributorForLocales: Array<string>;
|
||||
readonly canManageLocales: Array<string>;
|
||||
readonly canTranslateLocales: Array<string>;
|
||||
readonly managerForLocales: Array<string>;
|
||||
readonly translatorForLocales: Array<string>;
|
||||
readonly contributorForLocales: Array<string>;
|
||||
readonly translatorForProjects: Record<string, boolean>;
|
||||
readonly pmForProjects: Array<string>;
|
||||
readonly settings: SettingsState;
|
||||
readonly tourStatus: number | null | undefined;
|
||||
readonly hasDismissedAddonPromotion: boolean;
|
||||
|
@ -91,16 +95,20 @@ export type UserState = {
|
|||
const initial: UserState = {
|
||||
isAuthenticated: null,
|
||||
isAdmin: false,
|
||||
isPM: false,
|
||||
id: '',
|
||||
displayName: '',
|
||||
nameOrEmail: '',
|
||||
email: '',
|
||||
username: '',
|
||||
dateJoined: '',
|
||||
contributorForLocales: [],
|
||||
canManageLocales: [],
|
||||
canTranslateLocales: [],
|
||||
managerForLocales: [],
|
||||
translatorForLocales: [],
|
||||
contributorForLocales: [],
|
||||
translatorForProjects: {},
|
||||
pmForProjects: [],
|
||||
settings: initialSettings,
|
||||
tourStatus: null,
|
||||
hasDismissedAddonPromotion: false,
|
||||
|
@ -122,16 +130,20 @@ export function reducer(state: UserState = initial, action: Action): UserState {
|
|||
return {
|
||||
isAuthenticated: action.data.is_authenticated ?? null,
|
||||
isAdmin: action.data.is_admin ?? false,
|
||||
isPM: action.data.is_pm ?? false,
|
||||
id: action.data.id ?? '',
|
||||
email: action.data.email ?? '',
|
||||
displayName: action.data.display_name ?? '',
|
||||
nameOrEmail: action.data.name_or_email ?? '',
|
||||
email: action.data.email ?? '',
|
||||
username: action.data.username ?? '',
|
||||
dateJoined: action.data.date_joined ?? '',
|
||||
contributorForLocales: action.data.contributor_for_locales ?? [],
|
||||
canManageLocales: action.data.can_manage_locales ?? [],
|
||||
canTranslateLocales: action.data.can_translate_locales ?? [],
|
||||
managerForLocales: action.data.manager_for_locales ?? [],
|
||||
translatorForLocales: action.data.translator_for_locales ?? [],
|
||||
contributorForLocales: action.data.contributor_for_locales ?? [],
|
||||
translatorForProjects: action.data.translator_for_projects ?? {},
|
||||
pmForProjects: action.data.pm_for_projects ?? [],
|
||||
settings: settings(state.settings, action),
|
||||
tourStatus: action.data.tour_status ?? null,
|
||||
hasDismissedAddonPromotion:
|
||||
|
|
|
@ -53,8 +53,8 @@ export function createDefaultUser(store, initial = {}) {
|
|||
settings: { force_suggestions: false },
|
||||
username: 'Franck',
|
||||
is_authenticated: true,
|
||||
manager_for_locales: ['kg'],
|
||||
translator_for_locales: [],
|
||||
can_manage_locales: ['kg'],
|
||||
can_translate_locales: [],
|
||||
translator_for_projects: {},
|
||||
...initial,
|
||||
};
|
||||
|
|
Загрузка…
Ссылка в новой задаче