diff --git a/docs/topics/api/activity.rst b/docs/topics/api/activity.rst
index beece0eb06..9697194f32 100644
--- a/docs/topics/api/activity.rst
+++ b/docs/topics/api/activity.rst
@@ -39,6 +39,11 @@ Review Notes Detail
This endpoint allows you to fetch a single review note for a specific version of an add-on.
+ .. note::
+ To allow reviewers to stay anonymous if they wish, the ``user`` object ``name`` can point to
+ their "reviewer" name depending on the action. In addition all other fields in that object,
+ despite being present for backwards-compatibility, are set to ``null``.
+
.. http:get:: /api/v4/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/versions/(int:id)/reviewnotes/(int:id)/
.. _review-notes-version-detail-object:
@@ -46,10 +51,10 @@ This endpoint allows you to fetch a single review note for a specific version of
:>json int id: The id for a review note.
:>json string action: The :ref:`type of review note`.
:>json string action_label: The text label of the action.
- :>json int user.id: The id of the reviewer or author who left the review note.
+ :>json int|null user.id: The id of the reviewer or author who left the review note.
:>json string user.name: The name of the reviewer or author.
- :>json string user.url: The link to the profile page for of the reviewer or author.
- :>json string user.username: The username of the reviewer or author.
+ :>json string|null user.url: The link to the profile page for of the reviewer or author.
+ :>json string|null user.username: The username of the reviewer or author.
:>json string comments: The text content of the review note.
:>json string date: The date the review note was created.
diff --git a/src/olympia/accounts/serializers.py b/src/olympia/accounts/serializers.py
index bfd966afe0..e0a5357a82 100644
--- a/src/olympia/accounts/serializers.py
+++ b/src/olympia/accounts/serializers.py
@@ -78,19 +78,28 @@ class UserProfileSerializer(PublicUserProfileSerializer):
picture_upload = serializers.ImageField(use_url=True, write_only=True)
permissions = serializers.SerializerMethodField()
fxa_edit_email_url = serializers.SerializerMethodField()
+ reviewer_name = serializers.CharField(
+ min_length=2, max_length=50,
+ validators=[OneOrMorePrintableCharacterAPIValidator()])
class Meta(PublicUserProfileSerializer.Meta):
fields = PublicUserProfileSerializer.Meta.fields + (
- 'display_name', 'email', 'deleted', 'last_login', 'picture_upload',
- 'last_login_ip', 'read_dev_agreement', 'permissions',
- 'fxa_edit_email_url', 'username',
+ 'deleted', 'display_name', 'email', 'fxa_edit_email_url',
+ 'last_login', 'last_login_ip', 'permissions', 'picture_upload',
+ 'read_dev_agreement', 'reviewer_name', 'username'
)
writeable_fields = (
'biography', 'display_name', 'homepage', 'location', 'occupation',
- 'picture_upload',
+ 'picture_upload', 'reviewer_name',
)
read_only_fields = tuple(set(fields) - set(writeable_fields))
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if (not self.instance or
+ not acl.is_user_any_kind_of_reviewer(self.instance)):
+ self.fields.pop('reviewer_name', None)
+
def get_fxa_edit_email_url(self, user):
base_url = '{}/settings'.format(
settings.FXA_CONFIG['default']['content_host']
@@ -111,6 +120,12 @@ class UserProfileSerializer(PublicUserProfileSerializer):
ugettext(u'This display name cannot be used.'))
return value
+ def validate_reviewer_name(self, value):
+ if DeniedName.blocked(value):
+ raise serializers.ValidationError(
+ ugettext(u'This reviewer name cannot be used.'))
+ return value
+
def validate_homepage(self, value):
if settings.DOMAIN.lower() in value.lower():
raise serializers.ValidationError(
diff --git a/src/olympia/accounts/tests/test_serializers.py b/src/olympia/accounts/tests/test_serializers.py
index aa4e5f5e19..02574ef44d 100644
--- a/src/olympia/accounts/tests/test_serializers.py
+++ b/src/olympia/accounts/tests/test_serializers.py
@@ -28,7 +28,8 @@ class TestBaseUserSerializer(TestCase):
def serialize(self):
# Manually reload the user first to clear any cached properties.
self.user = UserProfile.objects.get(pk=self.user.pk)
- serializer = self.serializer_class(context={'request': self.request})
+ serializer = self.serializer_class(
+ self.user, context={'request': self.request})
return serializer.to_representation(self.user)
def test_basic(self):
@@ -67,14 +68,19 @@ class TestPublicUserProfileSerializer(TestCase):
user_kwargs = {
'username': 'amo',
'biography': 'stuff', 'homepage': 'http://mozilla.org/',
- 'location': 'everywhere', 'occupation': 'job'}
+ 'location': 'everywhere', 'occupation': 'job',
+ }
+ user_private_kwargs = {
+ 'reviewer_name': 'batman',
+ }
def setUp(self):
self.request = APIRequestFactory().get('/')
- self.user = user_factory(**self.user_kwargs)
+ self.user = user_factory(
+ **self.user_kwargs, **self.user_private_kwargs)
def serialize(self):
- return (self.serializer(context={'request': self.request})
+ return (self.serializer(self.user, context={'request': self.request})
.to_representation(self.user))
def test_picture(self):
@@ -92,6 +98,8 @@ class TestPublicUserProfileSerializer(TestCase):
data = self.serialize()
for prop, val in self.user_kwargs.items():
assert data[prop] == six.text_type(val), prop
+ for prop, val in self.user_private_kwargs.items():
+ assert prop not in data
return data
def test_addons(self):
@@ -142,6 +150,11 @@ class TestPublicUserProfileSerializer(TestCase):
assert data['has_anonymous_username'] is False
assert data['has_anonymous_display_name'] is False
+ def test_is_reviewer(self):
+ self.grant_permission(self.user, 'Addons:PostReview')
+ # private data should still be absent, this is a public serializer
+ self.test_basic()
+
class PermissionsTestMixin(object):
def test_permissions(self):
@@ -194,6 +207,15 @@ class TestUserProfileSerializer(TestPublicUserProfileSerializer,
self.now.replace(microsecond=0).isoformat() + 'Z')
assert data['read_dev_agreement'] == data['last_login']
+ def test_is_reviewer(self):
+ self.grant_permission(self.user, 'Addons:PostReview')
+ data = self.serialize()
+ for prop, val in self.user_kwargs.items():
+ assert data[prop] == six.text_type(val), prop
+ # We can also see private stuff, it's the same user.
+ for prop, val in self.user_private_kwargs.items():
+ assert data[prop] == six.text_type(val), prop
+
def test_expose_fxa_edit_email_url(self):
fxa_host = 'http://example.com'
fxa_config = {
diff --git a/src/olympia/activity/models.py b/src/olympia/activity/models.py
index f15d2d6619..bbfac0cf9d 100644
--- a/src/olympia/activity/models.py
+++ b/src/olympia/activity/models.py
@@ -479,6 +479,17 @@ class ActivityLog(ModelBase):
def __html__(self):
return self
+ @property
+ def author_name(self):
+ """Name of the user that triggered the activity.
+
+ If it's a reviewer action that will be shown to developers, the
+ `reviewer_name` property is used if present, otherwise `name` is
+ used."""
+ if self.action in constants.activity.LOG_REVIEW_QUEUE_DEVELOPER:
+ return self.user.reviewer_name or self.user.name
+ return self.user.name
+
@classmethod
def create(cls, action, *args, **kw):
"""
diff --git a/src/olympia/activity/serializers.py b/src/olympia/activity/serializers.py
index 5014a1762f..7f88288595 100644
--- a/src/olympia/activity/serializers.py
+++ b/src/olympia/activity/serializers.py
@@ -2,7 +2,6 @@ from django.utils.translation import ugettext
from rest_framework import serializers
-from olympia.accounts.serializers import BaseUserSerializer
from olympia.activity.models import ActivityLog
@@ -11,7 +10,7 @@ class ActivityLogSerializer(serializers.ModelSerializer):
action_label = serializers.SerializerMethodField()
comments = serializers.SerializerMethodField()
date = serializers.DateTimeField(source='created')
- user = BaseUserSerializer()
+ user = serializers.SerializerMethodField()
highlight = serializers.SerializerMethodField()
class Meta:
@@ -37,3 +36,16 @@ class ActivityLogSerializer(serializers.ModelSerializer):
def get_highlight(self, obj):
return obj in self.to_highlight
+
+ def get_user(self, obj):
+ """Return minimal user information using ActivityLog.author_name to
+ avoid revealing actual name of reviewers for their review actions if
+ they have set an alias.
+
+ id, username and url are present for backwards-compatibility only."""
+ return {
+ 'id': None,
+ 'username': None,
+ 'url': None,
+ 'name': obj.author_name,
+ }
diff --git a/src/olympia/activity/tests/test_serializers.py b/src/olympia/activity/tests/test_serializers.py
index 637a811c3e..b2df27da52 100644
--- a/src/olympia/activity/tests/test_serializers.py
+++ b/src/olympia/activity/tests/test_serializers.py
@@ -4,7 +4,6 @@ from rest_framework.test import APIRequestFactory
from olympia import amo
from olympia.activity.models import ActivityLog
from olympia.activity.serializers import ActivityLogSerializer
-from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import TestCase, addon_factory, user_factory
@@ -24,16 +23,16 @@ class LogMixin(object):
class TestReviewNotesSerializerOutput(TestCase, LogMixin):
def setUp(self):
self.request = APIRequestFactory().get('/')
- self.user = user_factory()
+ self.user = user_factory(reviewer_name='fôo')
self.addon = addon_factory()
self.now = self.days_ago(0)
- self.entry = self.log(u'Oh nôes!', amo.LOG.REJECT_VERSION, self.now)
+ self.entry = self.log(u'Oh nøes!', amo.LOG.REJECT_VERSION, self.now)
def serialize(self, context=None):
if context is None:
context = {}
context['request'] = self.request
- serializer = ActivityLogSerializer(context=context)
+ serializer = ActivityLogSerializer(self.entry, context=context)
return serializer.to_representation(self.entry)
def test_basic(self):
@@ -43,33 +42,32 @@ class TestReviewNotesSerializerOutput(TestCase, LogMixin):
assert result['date'] == self.now.isoformat() + 'Z'
assert result['action'] == 'rejected'
assert result['action_label'] == 'Rejected'
- assert result['comments'] == u'Oh nôes!'
+ assert result['comments'] == u'Oh nøes!'
+ # To allow reviewers to stay anonymous the user object only contains
+ # the "activity name", which uses the reviewer name alias if present.
assert result['user'] == {
- 'id': self.user.pk,
- 'name': self.user.name,
+ 'id': None,
+ 'name': 'fôo',
'url': None,
- 'username': self.user.username,
+ 'username': None,
}
def test_url_for_yourself(self):
- # should include account profile url for your own requests
self.request.user = self.user
result = self.serialize()
- assert result['user']['url'] == absolutify(self.user.get_url_path())
+ assert result['user']['url'] is None
def test_url_for_developers(self):
- # should include account profile url for a developer
addon_factory(users=[self.user])
result = self.serialize()
- assert result['user']['url'] == absolutify(self.user.get_url_path())
+ assert result['user']['url'] is None
def test_url_for_admins(self):
- # should include account profile url for admins
admin = user_factory()
self.grant_permission(admin, 'Users:Edit')
self.request.user = admin
result = self.serialize()
- assert result['user']['url'] == absolutify(self.user.get_url_path())
+ assert result['user']['url'] is None
def test_should_highlight(self):
result = self.serialize(context={'to_highlight': [self.entry]})
diff --git a/src/olympia/activity/tests/test_utils.py b/src/olympia/activity/tests/test_utils.py
index 564194dd4a..882061db3d 100644
--- a/src/olympia/activity/tests/test_utils.py
+++ b/src/olympia/activity/tests/test_utils.py
@@ -4,6 +4,7 @@ import json
import os
from datetime import datetime, timedelta
+from email.utils import formataddr
from django.conf import settings
from django.core import mail
@@ -21,7 +22,7 @@ from olympia.activity.utils import (
ACTIVITY_MAIL_GROUP, ActivityEmailEncodingError, ActivityEmailParser,
ActivityEmailTokenError, ActivityEmailUUIDError, add_email_to_activity_log,
add_email_to_activity_log_wrapper, log_and_notify,
- notify_about_activity_log, send_activity_mail)
+ notify_about_activity_log, NOTIFICATIONS_FROM_EMAIL, send_activity_mail)
from olympia.addons.models import Addon, AddonReviewerFlags
from olympia.amo.templatetags.jinja_helpers import absolutify
from olympia.amo.tests import TestCase, addon_factory, user_factory
@@ -238,7 +239,7 @@ class TestLogAndNotify(TestCase):
def setUp(self):
self.developer = user_factory()
self.developer2 = user_factory()
- self.reviewer = user_factory()
+ self.reviewer = user_factory(reviewer_name='Revîewer')
self.grant_permission(self.reviewer, 'Addons:Review',
'Addon Reviewers')
@@ -280,6 +281,8 @@ class TestLogAndNotify(TestCase):
assert days_text in body
assert 'reviewing version %s of the add-on %s' % (
self.version.version, self.addon.name) in body
+ assert self.reviewer.name not in body
+ assert self.reviewer.reviewer_name in body
def _check_email(self, call, url, reason_text):
subject = call[0][0]
@@ -289,6 +292,7 @@ class TestLogAndNotify(TestCase):
assert ('visit %s' % url) in body
assert ('receiving this email because %s' % reason_text) in body
assert 'If we do not hear from you within' not in body
+ assert self.reviewer.name not in body
@mock.patch('olympia.activity.utils.send_mail')
def test_reviewer_request_for_information(self, send_mail_mock):
@@ -300,8 +304,8 @@ class TestLogAndNotify(TestCase):
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
assert send_mail_mock.call_count == 2 # Both authors.
- sender = '%s ' % (
- self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
@@ -331,8 +335,8 @@ class TestLogAndNotify(TestCase):
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
assert send_mail_mock.call_count == 2 # Both authors.
- sender = '%s ' % (
- self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
@@ -362,8 +366,8 @@ class TestLogAndNotify(TestCase):
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
assert send_mail_mock.call_count == 2 # Both authors.
- sender = '%s ' % (
- self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
@@ -415,8 +419,8 @@ class TestLogAndNotify(TestCase):
assert logs[0].details['comments'] == u'Thïs is á reply'
assert send_mail_mock.call_count == 2 # One author, one reviewer.
- sender = '%s ' % (
- self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.developer.name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
@@ -456,8 +460,8 @@ class TestLogAndNotify(TestCase):
assert logs[0].details['comments'] == u'Thîs ïs a revïewer replyîng'
assert send_mail_mock.call_count == 2 # Both authors.
- sender = '%s ' % (
- self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
@@ -489,8 +493,8 @@ class TestLogAndNotify(TestCase):
assert not logs[0].details # No details json because no comment.
assert send_mail_mock.call_count == 2 # One author, one reviewer.
- sender = '%s ' % (
- self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.developer.name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
@@ -518,8 +522,8 @@ class TestLogAndNotify(TestCase):
assert len(logs) == 1
recipients = self._recipients(send_mail_mock)
- sender = '%s ' % (
- self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.developer.name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
assert len(recipients) == 2
# self.reviewers wasn't on the thread, but gets an email anyway.
@@ -545,8 +549,8 @@ class TestLogAndNotify(TestCase):
assert len(logs) == 1
recipients = self._recipients(send_mail_mock)
- sender = '%s ' % (
- self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr(
+ (self.developer.name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
developer_subject = send_mail_mock.call_args_list[0][0][0]
assert developer_subject == (
@@ -675,7 +679,7 @@ class TestLogAndNotify(TestCase):
@mock.patch('olympia.activity.utils.send_mail')
def test_from_name_escape(self, send_mail_mock):
- self.reviewer.update(display_name='mr "quote" escape')
+ self.reviewer.update(reviewer_name='mr "quote" escape')
# One from the reviewer.
self._create(amo.LOG.REJECT_VERSION, self.reviewer)
@@ -707,8 +711,8 @@ class TestLogAndNotify(TestCase):
assert ActivityLog.objects.count() == 1 # No new activity created.
assert send_mail_mock.call_count == 2 # Both authors.
- sender = '%s ' % (
- self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
+ sender = formataddr((
+ self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
recipients = self._recipients(send_mail_mock)
assert len(recipients) == 2
diff --git a/src/olympia/activity/utils.py b/src/olympia/activity/utils.py
index d1298e22f4..d375d0a3dd 100644
--- a/src/olympia/activity/utils.py
+++ b/src/olympia/activity/utils.py
@@ -242,7 +242,7 @@ def notify_about_activity_log(addon, version, note, perm_setting=None,
author_context_dict = {
'name': addon.name,
'number': version.version,
- 'author': note.user.name,
+ 'author': note.author_name,
'comments': comments,
'url': absolutify(addon.get_dev_url('versions')),
'SITE_URL': settings.SITE_URL,
@@ -279,7 +279,7 @@ def notify_about_activity_log(addon, version, note, perm_setting=None,
addon.name, version.version)
# Build and send the mail for authors.
template = template_from_user(note.user, version)
- from_email = formataddr((note.user.name, NOTIFICATIONS_FROM_EMAIL))
+ from_email = formataddr((note.author_name, NOTIFICATIONS_FROM_EMAIL))
send_activity_mail(
subject, template.render(author_context_dict),
version, addon_authors, from_email, note.id, perm_setting)
diff --git a/src/olympia/devhub/templates/devhub/addons/activity.html b/src/olympia/devhub/templates/devhub/addons/activity.html
index f3b55c71df..b244c175e5 100644
--- a/src/olympia/devhub/templates/devhub/addons/activity.html
+++ b/src/olympia/devhub/templates/devhub/addons/activity.html
@@ -30,7 +30,8 @@
{{ item }}
- {% trans user=item.user|user_link, ago=item.created|timesince,
+ {% trans user=item.author_name,
+ ago=item.created|timesince,
iso=item.created|isotime,
pretty=item.created|datetime %}
diff --git a/src/olympia/devhub/templates/devhub/versions/list.html b/src/olympia/devhub/templates/devhub/versions/list.html
index 6975394a01..4fbc91ac84 100644
--- a/src/olympia/devhub/templates/devhub/versions/list.html
+++ b/src/olympia/devhub/templates/devhub/versions/list.html
@@ -121,8 +121,8 @@
{{ _('Load older...') }}
-
$action_label {{ _('by') }}
- $user_name
+
$action_label {{ _('by') }}
+ $user_name
$comments
diff --git a/src/olympia/devhub/tests/test_views.py b/src/olympia/devhub/tests/test_views.py
index 6e6891ba84..5bf7aa9dc2 100644
--- a/src/olympia/devhub/tests/test_views.py
+++ b/src/olympia/devhub/tests/test_views.py
@@ -647,6 +647,8 @@ class TestActivityFeed(TestCase):
assert self.client.login(email='del@icio.us')
self.addon = Addon.objects.get(id=3615)
self.version = self.addon.versions.first()
+ self.action_user = UserProfile.objects.get(
+ email='reviewer@mozilla.com')
def test_feed_for_all(self):
response = self.client.get(reverse('devhub.feed_all'))
@@ -675,7 +677,7 @@ class TestActivityFeed(TestCase):
assert response.status_code == 302
def add_log(self, action=amo.LOG.ADD_RATING):
- core.set_user(UserProfile.objects.get(email='del@icio.us'))
+ core.set_user(self.action_user)
ActivityLog.create(action, self.addon, self.version)
def add_hidden_log(self, action=amo.LOG.COMMENT_VERSION):
@@ -728,6 +730,32 @@ class TestActivityFeed(TestCase):
doc = pq(res.content)
assert len(doc('#recent-activity .item')) == 1
+ def test_reviewer_name_is_used_for_reviewer_actions(self):
+ self.action_user.update(display_name='HîdeMe', reviewer_name='ShöwMe')
+ self.add_log(action=amo.LOG.APPROVE_VERSION)
+ response = self.client.get(
+ reverse('devhub.feed', args=[self.addon.slug]))
+ doc = pq(response.content)
+ assert len(doc('#recent-activity .item')) == 1
+
+ content = force_text(response.content)
+ assert self.action_user.reviewer_name in content
+ assert self.action_user.name not in content
+
+ def test_regular_name_is_used_for_non_reviewer_actions(self):
+ # Fields are inverted compared to the test above.
+ self.action_user.update(reviewer_name='HîdeMe', display_name='ShöwMe')
+ self.add_log(action=amo.LOG.ADD_RATING) # not a reviewer action.
+ response = self.client.get(
+ reverse('devhub.feed', args=[self.addon.slug]))
+ doc = pq(response.content)
+ assert len(doc('#recent-activity .item')) == 1
+
+ content = force_text(response.content)
+ # Assertions are inverted compared to the test above.
+ assert self.action_user.reviewer_name not in content
+ assert self.action_user.name in content
+
class TestAPIAgreement(TestCase):
fixtures = ['base/addon_3615', 'base/addon_5579', 'base/users']
diff --git a/src/olympia/migrations/1082-add-reviewer-name.sql b/src/olympia/migrations/1082-add-reviewer-name.sql
new file mode 100644
index 0000000000..8499bbb34f
--- /dev/null
+++ b/src/olympia/migrations/1082-add-reviewer-name.sql
@@ -0,0 +1 @@
+ALTER TABLE `users` ADD COLUMN `reviewer_name` varchar(50);
diff --git a/src/olympia/reviewers/utils.py b/src/olympia/reviewers/utils.py
index 4b77f681d8..521ddbfbf4 100644
--- a/src/olympia/reviewers/utils.py
+++ b/src/olympia/reviewers/utils.py
@@ -570,7 +570,7 @@ class ReviewBase(object):
dev_ver_url = self.addon.get_dev_url('versions')
return {'name': addon.name,
'number': self.version.version if self.version else '',
- 'reviewer': self.user.name,
+ 'reviewer': self.user.reviewer_name or self.user.name,
'addon_url': absolutify(addon_url),
'dev_versions_url': absolutify(dev_ver_url),
'review_url': absolutify(reverse('reviewers.review',
diff --git a/src/olympia/users/admin.py b/src/olympia/users/admin.py
index eb242b3afb..1c27df22ab 100644
--- a/src/olympia/users/admin.py
+++ b/src/olympia/users/admin.py
@@ -48,8 +48,8 @@ class UserAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('id', 'email', 'fxa_id', 'username', 'display_name',
- 'biography', 'homepage', 'location', 'occupation',
- 'picture_img'),
+ 'reviewer_name', 'biography', 'homepage', 'location',
+ 'occupation', 'picture_img'),
}),
('Flags', {
'fields': ('display_collections', 'deleted', 'is_public'),
diff --git a/src/olympia/users/models.py b/src/olympia/users/models.py
index cd3b89cc32..cf468d1684 100644
--- a/src/olympia/users/models.py
+++ b/src/olympia/users/models.py
@@ -166,6 +166,10 @@ class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser):
# newsletter
basket_token = models.CharField(blank=True, default='', max_length=128)
+ reviewer_name = models.CharField(
+ max_length=50, default='', null=True, blank=True,
+ validators=[validators.MinLengthValidator(2)])
+
class Meta:
db_table = 'users'
diff --git a/static/js/zamboni/devhub.js b/static/js/zamboni/devhub.js
index 8b3fd789fb..de72493962 100644
--- a/static/js/zamboni/devhub.js
+++ b/static/js/zamboni/devhub.js
@@ -563,11 +563,10 @@ function initVersions() {
if (note["highlight"] == true) {
clone.addClass("new");
}
- clone.find('span.action')[0].textContent = note["action_label"];
- var user = clone.find('a:contains("$user_name")');
+ clone.find('.action')[0].textContent = note["action_label"];
+ var user = clone.find('.user_name');
user[0].textContent = note["user"]["name"];
- user.attr('href', note["user"]["url"]);
- var date = clone.find('time.timeago');
+ var date = clone.find('.timeago');
date[0].textContent = note["date"];
date.attr('datetime', note["date"]);
date.attr('title', note["date"]);