Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add role banner under user avatar #3400

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pontoon/base/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def serialize(self):
return {
"author": self.author.name_or_email,
"username": self.author.username,
"user_status": self.author.status(self.locale),
"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
17 changes: 17 additions & 0 deletions pontoon/base/models/user.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from hashlib import md5
from urllib.parse import quote, urlencode

from dateutil.relativedelta import relativedelta
from guardian.shortcuts import get_objects_for_user

from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.urls import reverse
from django.utils import timezone

from pontoon.actionlog.models import ActionLog

Expand Down Expand Up @@ -192,6 +194,20 @@ def user_locale_role(self, locale):
return "Contributor"


def user_status(self, locale):
if self.username == "Imported":
return ("", "")
if self.is_superuser:
return ("ADMIN", "Admin")
if self in locale.managers_group.user_set.all():
return ("MNGR", "Manager")
if self in locale.translators_group.user_set.all():
return ("TRNSL", "Translator")
if self.date_joined >= timezone.now() - relativedelta(months=3):
return ("NEW", "New User")
return ("", "")


@property
def contributed_translations(self):
"""Filtered contributions provided by user."""
Expand Down Expand Up @@ -423,6 +439,7 @@ def latest_action(self):
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)
User.add_to_class("status", user_status)
User.add_to_class("contributed_translations", contributed_translations)
User.add_to_class("top_contributed_locale", top_contributed_locale)
User.add_to_class("can_translate", can_translate)
Expand Down
22 changes: 22 additions & 0 deletions pontoon/base/tests/models/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,25 @@ def test_user_locale_role(user_a, user_b, user_c, locale_a):
# Admin and Manager
locale_a.managers_group.user_set.add(user_a)
assert user_a.locale_role(locale_a) == "Manager"


@pytest.mark.django_db
def test_user_status(user_a, user_b, user_c, locale_a):
# New User
assert user_a.status(locale_a)[1] == "New User"

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

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

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

# Translator
locale_a.translators_group.user_set.add(user_c)
assert user_c.status(locale_a)[1] == "Translator"
2 changes: 2 additions & 0 deletions pontoon/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ def get_translation_history(request):
"uid": u.id,
"username": u.username,
"user_gravatar_url_small": u.gravatar_url(88),
"user_status": u.status(locale),
"date": t.date,
"approved_user": User.display_name_or_blank(t.approved_user),
"approved_date": t.approved_date,
Expand Down Expand Up @@ -868,6 +869,7 @@ def user_data(request):
"display_name": user.display_name,
"name_or_email": user.name_or_email,
"username": user.username,
"date_joined": user.date_joined,
harmitgoswami marked this conversation as resolved.
Show resolved Hide resolved
"contributor_for_locales": list(
user.translation_set.values_list("locale__code", flat=True).distinct()
),
Expand Down
1 change: 1 addition & 0 deletions translate/src/api/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { keysToCamelCase } from './utils/keysToCamelCase';
export type TranslationComment = {
readonly author: string;
readonly username: string;
readonly userStatus: string[];
readonly userGravatarUrlSmall: string;
readonly createdAt: string;
readonly dateIso: string;
Expand Down
1 change: 1 addition & 0 deletions translate/src/api/translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type HistoryTranslation = {
readonly user: string;
readonly username: string;
readonly userGravatarUrlSmall: string;
readonly userStatus: string[];
readonly comments: Array<TranslationComment>;
};

Expand Down
1 change: 1 addition & 0 deletions translate/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type ApiUserData = {
name_or_email?: string;
email?: string;
username?: string;
date_joined?: string;
manager_for_locales?: string[];
translator_for_locales?: string[];
translator_for_projects?: Record<string, boolean>;
Expand Down
88 changes: 88 additions & 0 deletions translate/src/hooks/useUserStatus.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import sinon from 'sinon';

import { USER } from '~/modules/user';
import * as Hooks from '~/hooks';

import { useUserStatus } from './useUserStatus';

beforeAll(() => {
sinon.stub(Hooks, 'useAppSelector');
sinon.stub(React, 'useContext').returns({ code: 'mylocale' });
});
afterAll(() => {
Hooks.useAppSelector.restore();
React.useContext.restore();
});

const fakeSelector = (user) => (sel) =>
sel({
[USER]: user,
});

describe('useUserStatus', () => {
it('returns empty parameters for non-authenticated users', () => {
Hooks.useAppSelector.callsFake(fakeSelector({ isAuthenticated: false })),
expect(useUserStatus()).toStrictEqual(['', '']);
});

it('returns [ADMIN, Admin] if user has admin permissions', () => {
Hooks.useAppSelector.callsFake(
fakeSelector({
isAuthenticated: true,
isAdmin: true,
managerForLocales: ['mylocale'],
translatorForLocales: [],
}),
);
expect(useUserStatus()).toStrictEqual(['ADMIN', 'Admin']);
});

it('returns [MNGR, Manager] if user is a manager of the locale', () => {
Hooks.useAppSelector.callsFake(
fakeSelector({
isAuthenticated: true,
managerForLocales: ['mylocale'],
translatorForLocales: [],
}),
);
expect(useUserStatus()).toStrictEqual(['MNGR', 'Manager']);
});

it('returns [TRNSL, Translator] if user is a translator for the locale', () => {
Hooks.useAppSelector.callsFake(
fakeSelector({
isAuthenticated: true,
managerForLocales: [],
translatorForLocales: ['mylocale'],
}),
);
expect(useUserStatus()).toStrictEqual(['TRNSL', 'Translator']);
});

it('returns [NEW, New User] if user created their account within the last 3 months', () => {
const dateJoined = new Date();
dateJoined.setMonth(dateJoined.getMonth() - 2);
Hooks.useAppSelector.callsFake(
fakeSelector({
isAuthenticated: true,
managerForLocales: [],
translatorForLocales: [],
dateJoined: dateJoined,
}),
);
expect(useUserStatus()).toStrictEqual(['NEW', 'New User']);

// Set join date to be 6 months ago (no longer a new user)
dateJoined.setMonth(dateJoined.getMonth() - 6);
Hooks.useAppSelector.callsFake(
fakeSelector({
isAuthenticated: true,
managerForLocales: [],
translatorForLocales: [],
dateJoined: dateJoined,
}),
);
expect(useUserStatus()).toStrictEqual(['', '']);
});
});
44 changes: 44 additions & 0 deletions translate/src/hooks/useUserStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useContext } from 'react';

import { Locale } from '~/context/Locale';
import { USER } from '~/modules/user';
import { useAppSelector } from '~/hooks';

/**
* Return the user's status within the given locale, to display on the user banner
*/
export function useUserStatus(): Array<string> {
const { code } = useContext(Locale);
const {
isAuthenticated,
isAdmin,
managerForLocales,
translatorForLocales,
dateJoined,
} = useAppSelector((state) => state[USER]);

if (!isAuthenticated) {
return ['', ''];
}

if (isAdmin) {
return ['ADMIN', 'Admin'];
}

if (managerForLocales.includes(code)) {
return ['MNGR', 'Manager'];
}

if (translatorForLocales.includes(code)) {
return ['TRNSL', 'Translator'];
}

const dateJoinedObj = new Date(dateJoined);
let threeMonthsAgo = new Date();
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
if (dateJoinedObj > threeMonthsAgo) {
return ['NEW', 'New User'];
}

return ['', ''];
}
13 changes: 9 additions & 4 deletions translate/src/modules/comments/components/AddComment.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';

Expand All @@ -15,16 +14,22 @@ const USER = {

describe('<AddComment>', () => {
it('calls submitComment function', () => {
const store = createReduxStore();
const submitCommentFn = sinon.spy();
const wrapper = shallow(
<AddComment submitComment={submitCommentFn} user={USER} />,
const Wrapper = () => (
<MentionUsers.Provider
value={{ initMentions: sinon.spy(), mentionUsers: [] }}
>
<AddComment onAddComment={submitCommentFn} user={USER} />
</MentionUsers.Provider>
);
const wrapper = mountComponentWithStore(Wrapper, store);

const event = {
preventDefault: sinon.spy(),
};

wrapper.find('button').simulate('onClick', event);
wrapper.find('button').simulate('click', event);
expect(submitCommentFn.calledOnce).toBeTruthy;
});

Expand Down
8 changes: 7 additions & 1 deletion translate/src/modules/comments/components/AddComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { MentionUser } from '~/api/user';
import { MentionUsers } from '~/context/MentionUsers';
import type { UserState } from '~/modules/user';
import { UserAvatar } from '~/modules/user';
import { useUserStatus } from '~/hooks/useUserStatus';

import './AddComment.css';
import { MentionList } from './MentionList';
Expand Down Expand Up @@ -86,6 +87,7 @@ export function AddComment({
const [mentionIndex, setMentionIndex] = useState(0);
const [mentionSearch, setMentionSearch] = useState('');
const [requireUsers, setRequireUsers] = useState(false);
const role = useUserStatus();

const { initMentions, mentionUsers } = useContext(MentionUsers);
const [slateKey, resetValue] = useReducer((key) => key + 1, 0);
Expand Down Expand Up @@ -244,7 +246,11 @@ export function AddComment({

return (
<div className='comment add-comment'>
<UserAvatar username={username} imageUrl={gravatarURLSmall} />
<UserAvatar
username={username}
imageUrl={gravatarURLSmall}
userStatus={role}
/>
<div className='container'>
<Slate
editor={editor}
Expand Down
1 change: 1 addition & 0 deletions translate/src/modules/comments/components/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function Comment(props: Props): null | React.ReactElement<'li'> {
<UserAvatar
username={comment.username}
imageUrl={comment.userGravatarUrlSmall}
userStatus={comment.userStatus}
/>
<div className='container'>
<div className='content'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ describe('<CommentsList>', () => {
translation: {
...DEFAULT_TRANSLATION,
comments: [
{ id: 1, content: '11' },
{ id: 2, content: '22' },
{ id: 3, content: '33' },
{ id: 1, content: '11', userStatus: '' },
{ id: 2, content: '22', userStatus: '' },
{ id: 3, content: '33', userStatus: '' },
],
},
user: DEFAULT_USER,
Expand Down
1 change: 1 addition & 0 deletions translate/src/modules/comments/components/CommentsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function CommentsList({
user,
}: Props): React.ReactElement<'div'> {
const onAddComment = useAddCommentAndRefresh(translation);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please restore this change and we can ship it! :)

return (
<div className='comments-list'>
<section className='all-comments'>
Expand Down
6 changes: 5 additions & 1 deletion translate/src/modules/history/components/History.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ function mountHistory(fetching, translations) {

describe('<History>', () => {
it('shows the correct number of translations', () => {
const wrapper = mountHistory(false, [{ pk: 1 }, { pk: 2 }, { pk: 3 }]);
const wrapper = mountHistory(false, [
{ pk: 1, userStatus: '' },
{ pk: 2, userStatus: '' },
{ pk: 3, userStatus: '' },
]);

expect(wrapper.find('ul > *')).toHaveLength(3);
});
Expand Down
15 changes: 0 additions & 15 deletions translate/src/modules/history/components/HistoryTranslation.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,6 @@
background: var(--dark-grey-1);
}

.history .translation .user-avatar {
padding-right: 8px;
position: relative;
top: 3px;
}

.history .translation .user-avatar img {
border-radius: 6px;
border: 2px solid var(--icon-border-1);
}

.history .translation:hover .user-avatar img {
border-color: var(--translation-border);
}

.history .translation .content {
flex-grow: 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export function HistoryTranslationBase({
username={translation.username}
title=''
imageUrl={translation.userGravatarUrlSmall}
userStatus={translation.userStatus}
/>
</Localized>
{translation.machinerySources ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ describe('<TeamComments>', () => {
teamComments: {
entity: 267,
comments: [
{ id: 1, content: '11' },
{ id: 2, content: '22' },
{ id: 3, content: '33' },
{ id: 1, content: '11', userStatus: '' },
{ id: 2, content: '22', userStatus: '' },
{ id: 3, content: '33', userStatus: '' },
],
},
user: DEFAULT_USER,
Expand Down
Loading