Add reviewer name alias for reviewers to hide their actual name
It's used when exposing reviewer name to developers for reviewer actions - regular actions still use the normal name, even when made by a reviewer.
This commit is contained in:
Родитель
e67eeccaf8
Коммит
90c8201eb9
|
@ -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.
|
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)/
|
.. 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:
|
.. _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 int id: The id for a review note.
|
||||||
:>json string action: The :ref:`type of review note<review-note-action>`.
|
:>json string action: The :ref:`type of review note<review-note-action>`.
|
||||||
:>json string action_label: The text label of the action.
|
:>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.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|null 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.username: The username of the reviewer or author.
|
||||||
:>json string comments: The text content of the review note.
|
:>json string comments: The text content of the review note.
|
||||||
:>json string date: The date the review note was created.
|
:>json string date: The date the review note was created.
|
||||||
|
|
||||||
|
|
|
@ -78,19 +78,28 @@ class UserProfileSerializer(PublicUserProfileSerializer):
|
||||||
picture_upload = serializers.ImageField(use_url=True, write_only=True)
|
picture_upload = serializers.ImageField(use_url=True, write_only=True)
|
||||||
permissions = serializers.SerializerMethodField()
|
permissions = serializers.SerializerMethodField()
|
||||||
fxa_edit_email_url = serializers.SerializerMethodField()
|
fxa_edit_email_url = serializers.SerializerMethodField()
|
||||||
|
reviewer_name = serializers.CharField(
|
||||||
|
min_length=2, max_length=50,
|
||||||
|
validators=[OneOrMorePrintableCharacterAPIValidator()])
|
||||||
|
|
||||||
class Meta(PublicUserProfileSerializer.Meta):
|
class Meta(PublicUserProfileSerializer.Meta):
|
||||||
fields = PublicUserProfileSerializer.Meta.fields + (
|
fields = PublicUserProfileSerializer.Meta.fields + (
|
||||||
'display_name', 'email', 'deleted', 'last_login', 'picture_upload',
|
'deleted', 'display_name', 'email', 'fxa_edit_email_url',
|
||||||
'last_login_ip', 'read_dev_agreement', 'permissions',
|
'last_login', 'last_login_ip', 'permissions', 'picture_upload',
|
||||||
'fxa_edit_email_url', 'username',
|
'read_dev_agreement', 'reviewer_name', 'username'
|
||||||
)
|
)
|
||||||
writeable_fields = (
|
writeable_fields = (
|
||||||
'biography', 'display_name', 'homepage', 'location', 'occupation',
|
'biography', 'display_name', 'homepage', 'location', 'occupation',
|
||||||
'picture_upload',
|
'picture_upload', 'reviewer_name',
|
||||||
)
|
)
|
||||||
read_only_fields = tuple(set(fields) - set(writeable_fields))
|
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):
|
def get_fxa_edit_email_url(self, user):
|
||||||
base_url = '{}/settings'.format(
|
base_url = '{}/settings'.format(
|
||||||
settings.FXA_CONFIG['default']['content_host']
|
settings.FXA_CONFIG['default']['content_host']
|
||||||
|
@ -111,6 +120,12 @@ class UserProfileSerializer(PublicUserProfileSerializer):
|
||||||
ugettext(u'This display name cannot be used.'))
|
ugettext(u'This display name cannot be used.'))
|
||||||
return value
|
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):
|
def validate_homepage(self, value):
|
||||||
if settings.DOMAIN.lower() in value.lower():
|
if settings.DOMAIN.lower() in value.lower():
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
|
|
|
@ -28,7 +28,8 @@ class TestBaseUserSerializer(TestCase):
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
# Manually reload the user first to clear any cached properties.
|
# Manually reload the user first to clear any cached properties.
|
||||||
self.user = UserProfile.objects.get(pk=self.user.pk)
|
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)
|
return serializer.to_representation(self.user)
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
|
@ -67,14 +68,19 @@ class TestPublicUserProfileSerializer(TestCase):
|
||||||
user_kwargs = {
|
user_kwargs = {
|
||||||
'username': 'amo',
|
'username': 'amo',
|
||||||
'biography': 'stuff', 'homepage': 'http://mozilla.org/',
|
'biography': 'stuff', 'homepage': 'http://mozilla.org/',
|
||||||
'location': 'everywhere', 'occupation': 'job'}
|
'location': 'everywhere', 'occupation': 'job',
|
||||||
|
}
|
||||||
|
user_private_kwargs = {
|
||||||
|
'reviewer_name': 'batman',
|
||||||
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.request = APIRequestFactory().get('/')
|
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):
|
def serialize(self):
|
||||||
return (self.serializer(context={'request': self.request})
|
return (self.serializer(self.user, context={'request': self.request})
|
||||||
.to_representation(self.user))
|
.to_representation(self.user))
|
||||||
|
|
||||||
def test_picture(self):
|
def test_picture(self):
|
||||||
|
@ -92,6 +98,8 @@ class TestPublicUserProfileSerializer(TestCase):
|
||||||
data = self.serialize()
|
data = self.serialize()
|
||||||
for prop, val in self.user_kwargs.items():
|
for prop, val in self.user_kwargs.items():
|
||||||
assert data[prop] == six.text_type(val), prop
|
assert data[prop] == six.text_type(val), prop
|
||||||
|
for prop, val in self.user_private_kwargs.items():
|
||||||
|
assert prop not in data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def test_addons(self):
|
def test_addons(self):
|
||||||
|
@ -142,6 +150,11 @@ class TestPublicUserProfileSerializer(TestCase):
|
||||||
assert data['has_anonymous_username'] is False
|
assert data['has_anonymous_username'] is False
|
||||||
assert data['has_anonymous_display_name'] 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):
|
class PermissionsTestMixin(object):
|
||||||
def test_permissions(self):
|
def test_permissions(self):
|
||||||
|
@ -194,6 +207,15 @@ class TestUserProfileSerializer(TestPublicUserProfileSerializer,
|
||||||
self.now.replace(microsecond=0).isoformat() + 'Z')
|
self.now.replace(microsecond=0).isoformat() + 'Z')
|
||||||
assert data['read_dev_agreement'] == data['last_login']
|
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):
|
def test_expose_fxa_edit_email_url(self):
|
||||||
fxa_host = 'http://example.com'
|
fxa_host = 'http://example.com'
|
||||||
fxa_config = {
|
fxa_config = {
|
||||||
|
|
|
@ -479,6 +479,17 @@ class ActivityLog(ModelBase):
|
||||||
def __html__(self):
|
def __html__(self):
|
||||||
return 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
|
@classmethod
|
||||||
def create(cls, action, *args, **kw):
|
def create(cls, action, *args, **kw):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,6 @@ from django.utils.translation import ugettext
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from olympia.accounts.serializers import BaseUserSerializer
|
|
||||||
from olympia.activity.models import ActivityLog
|
from olympia.activity.models import ActivityLog
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +10,7 @@ class ActivityLogSerializer(serializers.ModelSerializer):
|
||||||
action_label = serializers.SerializerMethodField()
|
action_label = serializers.SerializerMethodField()
|
||||||
comments = serializers.SerializerMethodField()
|
comments = serializers.SerializerMethodField()
|
||||||
date = serializers.DateTimeField(source='created')
|
date = serializers.DateTimeField(source='created')
|
||||||
user = BaseUserSerializer()
|
user = serializers.SerializerMethodField()
|
||||||
highlight = serializers.SerializerMethodField()
|
highlight = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -37,3 +36,16 @@ class ActivityLogSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def get_highlight(self, obj):
|
def get_highlight(self, obj):
|
||||||
return obj in self.to_highlight
|
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,
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ from rest_framework.test import APIRequestFactory
|
||||||
from olympia import amo
|
from olympia import amo
|
||||||
from olympia.activity.models import ActivityLog
|
from olympia.activity.models import ActivityLog
|
||||||
from olympia.activity.serializers import ActivityLogSerializer
|
from olympia.activity.serializers import ActivityLogSerializer
|
||||||
from olympia.amo.templatetags.jinja_helpers import absolutify
|
|
||||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,16 +23,16 @@ class LogMixin(object):
|
||||||
class TestReviewNotesSerializerOutput(TestCase, LogMixin):
|
class TestReviewNotesSerializerOutput(TestCase, LogMixin):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.request = APIRequestFactory().get('/')
|
self.request = APIRequestFactory().get('/')
|
||||||
self.user = user_factory()
|
self.user = user_factory(reviewer_name='fôo')
|
||||||
self.addon = addon_factory()
|
self.addon = addon_factory()
|
||||||
self.now = self.days_ago(0)
|
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):
|
def serialize(self, context=None):
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
context['request'] = self.request
|
context['request'] = self.request
|
||||||
serializer = ActivityLogSerializer(context=context)
|
serializer = ActivityLogSerializer(self.entry, context=context)
|
||||||
return serializer.to_representation(self.entry)
|
return serializer.to_representation(self.entry)
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
|
@ -43,33 +42,32 @@ class TestReviewNotesSerializerOutput(TestCase, LogMixin):
|
||||||
assert result['date'] == self.now.isoformat() + 'Z'
|
assert result['date'] == self.now.isoformat() + 'Z'
|
||||||
assert result['action'] == 'rejected'
|
assert result['action'] == 'rejected'
|
||||||
assert result['action_label'] == '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'] == {
|
assert result['user'] == {
|
||||||
'id': self.user.pk,
|
'id': None,
|
||||||
'name': self.user.name,
|
'name': 'fôo',
|
||||||
'url': None,
|
'url': None,
|
||||||
'username': self.user.username,
|
'username': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_url_for_yourself(self):
|
def test_url_for_yourself(self):
|
||||||
# should include account profile url for your own requests
|
|
||||||
self.request.user = self.user
|
self.request.user = self.user
|
||||||
result = self.serialize()
|
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):
|
def test_url_for_developers(self):
|
||||||
# should include account profile url for a developer
|
|
||||||
addon_factory(users=[self.user])
|
addon_factory(users=[self.user])
|
||||||
result = self.serialize()
|
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):
|
def test_url_for_admins(self):
|
||||||
# should include account profile url for admins
|
|
||||||
admin = user_factory()
|
admin = user_factory()
|
||||||
self.grant_permission(admin, 'Users:Edit')
|
self.grant_permission(admin, 'Users:Edit')
|
||||||
self.request.user = admin
|
self.request.user = admin
|
||||||
result = self.serialize()
|
result = self.serialize()
|
||||||
assert result['user']['url'] == absolutify(self.user.get_url_path())
|
assert result['user']['url'] is None
|
||||||
|
|
||||||
def test_should_highlight(self):
|
def test_should_highlight(self):
|
||||||
result = self.serialize(context={'to_highlight': [self.entry]})
|
result = self.serialize(context={'to_highlight': [self.entry]})
|
||||||
|
|
|
@ -4,6 +4,7 @@ import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from email.utils import formataddr
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
@ -21,7 +22,7 @@ from olympia.activity.utils import (
|
||||||
ACTIVITY_MAIL_GROUP, ActivityEmailEncodingError, ActivityEmailParser,
|
ACTIVITY_MAIL_GROUP, ActivityEmailEncodingError, ActivityEmailParser,
|
||||||
ActivityEmailTokenError, ActivityEmailUUIDError, add_email_to_activity_log,
|
ActivityEmailTokenError, ActivityEmailUUIDError, add_email_to_activity_log,
|
||||||
add_email_to_activity_log_wrapper, log_and_notify,
|
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.addons.models import Addon, AddonReviewerFlags
|
||||||
from olympia.amo.templatetags.jinja_helpers import absolutify
|
from olympia.amo.templatetags.jinja_helpers import absolutify
|
||||||
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
from olympia.amo.tests import TestCase, addon_factory, user_factory
|
||||||
|
@ -238,7 +239,7 @@ class TestLogAndNotify(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.developer = user_factory()
|
self.developer = user_factory()
|
||||||
self.developer2 = 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',
|
self.grant_permission(self.reviewer, 'Addons:Review',
|
||||||
'Addon Reviewers')
|
'Addon Reviewers')
|
||||||
|
|
||||||
|
@ -280,6 +281,8 @@ class TestLogAndNotify(TestCase):
|
||||||
assert days_text in body
|
assert days_text in body
|
||||||
assert 'reviewing version %s of the add-on %s' % (
|
assert 'reviewing version %s of the add-on %s' % (
|
||||||
self.version.version, self.addon.name) in body
|
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):
|
def _check_email(self, call, url, reason_text):
|
||||||
subject = call[0][0]
|
subject = call[0][0]
|
||||||
|
@ -289,6 +292,7 @@ class TestLogAndNotify(TestCase):
|
||||||
assert ('visit %s' % url) in body
|
assert ('visit %s' % url) in body
|
||||||
assert ('receiving this email because %s' % reason_text) 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 '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')
|
@mock.patch('olympia.activity.utils.send_mail')
|
||||||
def test_reviewer_request_for_information(self, send_mail_mock):
|
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)
|
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # Both authors.
|
assert send_mail_mock.call_count == 2 # Both authors.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
|
@ -331,8 +335,8 @@ class TestLogAndNotify(TestCase):
|
||||||
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
|
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # Both authors.
|
assert send_mail_mock.call_count == 2 # Both authors.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
|
@ -362,8 +366,8 @@ class TestLogAndNotify(TestCase):
|
||||||
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
|
amo.LOG.REQUEST_INFORMATION, 'blah', self.reviewer, self.version)
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # Both authors.
|
assert send_mail_mock.call_count == 2 # Both authors.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
|
@ -415,8 +419,8 @@ class TestLogAndNotify(TestCase):
|
||||||
assert logs[0].details['comments'] == u'Thïs is á reply'
|
assert logs[0].details['comments'] == u'Thïs is á reply'
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # One author, one reviewer.
|
assert send_mail_mock.call_count == 2 # One author, one reviewer.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.developer.name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
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 logs[0].details['comments'] == u'Thîs ïs a revïewer replyîng'
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # Both authors.
|
assert send_mail_mock.call_count == 2 # Both authors.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
|
@ -489,8 +493,8 @@ class TestLogAndNotify(TestCase):
|
||||||
assert not logs[0].details # No details json because no comment.
|
assert not logs[0].details # No details json because no comment.
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # One author, one reviewer.
|
assert send_mail_mock.call_count == 2 # One author, one reviewer.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.developer.name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
|
@ -518,8 +522,8 @@ class TestLogAndNotify(TestCase):
|
||||||
assert len(logs) == 1
|
assert len(logs) == 1
|
||||||
|
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.developer.name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
# self.reviewers wasn't on the thread, but gets an email anyway.
|
# self.reviewers wasn't on the thread, but gets an email anyway.
|
||||||
|
@ -545,8 +549,8 @@ class TestLogAndNotify(TestCase):
|
||||||
assert len(logs) == 1
|
assert len(logs) == 1
|
||||||
|
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr(
|
||||||
self.developer.name, settings.INBOUND_EMAIL_DOMAIN)
|
(self.developer.name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['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]
|
developer_subject = send_mail_mock.call_args_list[0][0][0]
|
||||||
assert developer_subject == (
|
assert developer_subject == (
|
||||||
|
@ -675,7 +679,7 @@ class TestLogAndNotify(TestCase):
|
||||||
|
|
||||||
@mock.patch('olympia.activity.utils.send_mail')
|
@mock.patch('olympia.activity.utils.send_mail')
|
||||||
def test_from_name_escape(self, send_mail_mock):
|
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.
|
# One from the reviewer.
|
||||||
self._create(amo.LOG.REJECT_VERSION, self.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 ActivityLog.objects.count() == 1 # No new activity created.
|
||||||
|
|
||||||
assert send_mail_mock.call_count == 2 # Both authors.
|
assert send_mail_mock.call_count == 2 # Both authors.
|
||||||
sender = '%s <notifications@%s>' % (
|
sender = formataddr((
|
||||||
self.reviewer.name, settings.INBOUND_EMAIL_DOMAIN)
|
self.reviewer.reviewer_name, NOTIFICATIONS_FROM_EMAIL))
|
||||||
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
assert sender == send_mail_mock.call_args_list[0][1]['from_email']
|
||||||
recipients = self._recipients(send_mail_mock)
|
recipients = self._recipients(send_mail_mock)
|
||||||
assert len(recipients) == 2
|
assert len(recipients) == 2
|
||||||
|
|
|
@ -242,7 +242,7 @@ def notify_about_activity_log(addon, version, note, perm_setting=None,
|
||||||
author_context_dict = {
|
author_context_dict = {
|
||||||
'name': addon.name,
|
'name': addon.name,
|
||||||
'number': version.version,
|
'number': version.version,
|
||||||
'author': note.user.name,
|
'author': note.author_name,
|
||||||
'comments': comments,
|
'comments': comments,
|
||||||
'url': absolutify(addon.get_dev_url('versions')),
|
'url': absolutify(addon.get_dev_url('versions')),
|
||||||
'SITE_URL': settings.SITE_URL,
|
'SITE_URL': settings.SITE_URL,
|
||||||
|
@ -279,7 +279,7 @@ def notify_about_activity_log(addon, version, note, perm_setting=None,
|
||||||
addon.name, version.version)
|
addon.name, version.version)
|
||||||
# Build and send the mail for authors.
|
# Build and send the mail for authors.
|
||||||
template = template_from_user(note.user, version)
|
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(
|
send_activity_mail(
|
||||||
subject, template.render(author_context_dict),
|
subject, template.render(author_context_dict),
|
||||||
version, addon_authors, from_email, note.id, perm_setting)
|
version, addon_authors, from_email, note.id, perm_setting)
|
||||||
|
|
|
@ -30,7 +30,8 @@
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</p>
|
</p>
|
||||||
<p class="timestamp">
|
<p class="timestamp">
|
||||||
{% trans user=item.user|user_link, ago=item.created|timesince,
|
{% trans user=item.author_name,
|
||||||
|
ago=item.created|timesince,
|
||||||
iso=item.created|isotime,
|
iso=item.created|isotime,
|
||||||
pretty=item.created|datetime %}
|
pretty=item.created|datetime %}
|
||||||
<time datetime="{{ iso }}" title="{{ pretty }}">{{ ago }}</time>
|
<time datetime="{{ iso }}" title="{{ pretty }}">{{ ago }}</time>
|
||||||
|
|
|
@ -121,8 +121,8 @@
|
||||||
<p><a href="#" class="review-history-loadmore" data-div="#{{ version.id }}-review-history">{{ _('Load older...') }}</a></p>
|
<p><a href="#" class="review-history-loadmore" data-div="#{{ version.id }}-review-history">{{ _('Load older...') }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="review-entry-empty hidden">
|
<div class="review-entry-empty hidden">
|
||||||
<p><span class="action">$action_label</span> {{ _('by') }}
|
<p><strong class="action">$action_label</strong> {{ _('by') }}
|
||||||
<a href="$user_profile">$user_name</a> <time class="timeago" datetime=$date>$date</time></p>
|
<em class="user_name">$user_name</em> <time class="timeago" datetime=$date>$date</time></p>
|
||||||
<pre>$comments</pre>
|
<pre>$comments</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -647,6 +647,8 @@ class TestActivityFeed(TestCase):
|
||||||
assert self.client.login(email='del@icio.us')
|
assert self.client.login(email='del@icio.us')
|
||||||
self.addon = Addon.objects.get(id=3615)
|
self.addon = Addon.objects.get(id=3615)
|
||||||
self.version = self.addon.versions.first()
|
self.version = self.addon.versions.first()
|
||||||
|
self.action_user = UserProfile.objects.get(
|
||||||
|
email='reviewer@mozilla.com')
|
||||||
|
|
||||||
def test_feed_for_all(self):
|
def test_feed_for_all(self):
|
||||||
response = self.client.get(reverse('devhub.feed_all'))
|
response = self.client.get(reverse('devhub.feed_all'))
|
||||||
|
@ -675,7 +677,7 @@ class TestActivityFeed(TestCase):
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
||||||
def add_log(self, action=amo.LOG.ADD_RATING):
|
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)
|
ActivityLog.create(action, self.addon, self.version)
|
||||||
|
|
||||||
def add_hidden_log(self, action=amo.LOG.COMMENT_VERSION):
|
def add_hidden_log(self, action=amo.LOG.COMMENT_VERSION):
|
||||||
|
@ -728,6 +730,32 @@ class TestActivityFeed(TestCase):
|
||||||
doc = pq(res.content)
|
doc = pq(res.content)
|
||||||
assert len(doc('#recent-activity .item')) == 1
|
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):
|
class TestAPIAgreement(TestCase):
|
||||||
fixtures = ['base/addon_3615', 'base/addon_5579', 'base/users']
|
fixtures = ['base/addon_3615', 'base/addon_5579', 'base/users']
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `users` ADD COLUMN `reviewer_name` varchar(50);
|
|
@ -570,7 +570,7 @@ class ReviewBase(object):
|
||||||
dev_ver_url = self.addon.get_dev_url('versions')
|
dev_ver_url = self.addon.get_dev_url('versions')
|
||||||
return {'name': addon.name,
|
return {'name': addon.name,
|
||||||
'number': self.version.version if self.version else '',
|
'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),
|
'addon_url': absolutify(addon_url),
|
||||||
'dev_versions_url': absolutify(dev_ver_url),
|
'dev_versions_url': absolutify(dev_ver_url),
|
||||||
'review_url': absolutify(reverse('reviewers.review',
|
'review_url': absolutify(reverse('reviewers.review',
|
||||||
|
|
|
@ -48,8 +48,8 @@ class UserAdmin(admin.ModelAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('id', 'email', 'fxa_id', 'username', 'display_name',
|
'fields': ('id', 'email', 'fxa_id', 'username', 'display_name',
|
||||||
'biography', 'homepage', 'location', 'occupation',
|
'reviewer_name', 'biography', 'homepage', 'location',
|
||||||
'picture_img'),
|
'occupation', 'picture_img'),
|
||||||
}),
|
}),
|
||||||
('Flags', {
|
('Flags', {
|
||||||
'fields': ('display_collections', 'deleted', 'is_public'),
|
'fields': ('display_collections', 'deleted', 'is_public'),
|
||||||
|
|
|
@ -166,6 +166,10 @@ class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser):
|
||||||
# newsletter
|
# newsletter
|
||||||
basket_token = models.CharField(blank=True, default='', max_length=128)
|
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:
|
class Meta:
|
||||||
db_table = 'users'
|
db_table = 'users'
|
||||||
|
|
||||||
|
|
|
@ -563,11 +563,10 @@ function initVersions() {
|
||||||
if (note["highlight"] == true) {
|
if (note["highlight"] == true) {
|
||||||
clone.addClass("new");
|
clone.addClass("new");
|
||||||
}
|
}
|
||||||
clone.find('span.action')[0].textContent = note["action_label"];
|
clone.find('.action')[0].textContent = note["action_label"];
|
||||||
var user = clone.find('a:contains("$user_name")');
|
var user = clone.find('.user_name');
|
||||||
user[0].textContent = note["user"]["name"];
|
user[0].textContent = note["user"]["name"];
|
||||||
user.attr('href', note["user"]["url"]);
|
var date = clone.find('.timeago');
|
||||||
var date = clone.find('time.timeago');
|
|
||||||
date[0].textContent = note["date"];
|
date[0].textContent = note["date"];
|
||||||
date.attr('datetime', note["date"]);
|
date.attr('datetime', note["date"]);
|
||||||
date.attr('title', note["date"]);
|
date.attr('title', note["date"]);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче