Skip to content
This repository was archived by the owner on Mar 15, 2018. It is now read-only.

Commit 600e471

Browse files
committed
add reply tokens in reviewer emails (Bug 879535)
1 parent 4f093d3 commit 600e471

File tree

7 files changed

+216
-67
lines changed

7 files changed

+216
-67
lines changed

apps/comm/utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
from email_reply_parser import EmailReplyParser
66

77
from access import acl
8+
from access.models import Group
89
from comm.models import (CommunicationNote, CommunicationNoteRead,
910
CommunicationThreadCC, CommunicationThreadToken)
1011
from mkt.constants import comm
12+
from users.models import UserProfile
1113

1214

1315
log = commonware.log.getLogger('comm')
@@ -140,3 +142,75 @@ def filter_notes_by_read_status(queryset, profile, read_status=True):
140142
else:
141143
# Exclude read notes if they exist.
142144
return queryset.exclude(pk__in=notes) if notes else queryset.all()
145+
146+
147+
def get_reply_token(thread, user_id):
148+
tok, created = CommunicationThreadToken.objects.get_or_create(
149+
thread=thread, user_id=user_id)
150+
151+
# We expire a token after it has been used for a maximum number of times.
152+
# This is usually to prevent overusing a single token to spam to threads.
153+
# Since we're re-using tokens, we need to make sure they are valid for
154+
# replying to new notes so we reset their `use_count`.
155+
if not created:
156+
tok.update(use_count=0)
157+
else:
158+
log.info('Created token with UUID %s for user_id: %s.' %
159+
(tok.uuid, user_id))
160+
return tok
161+
162+
163+
def get_recipients(note, fresh_thread=False):
164+
"""
165+
Create/refresh tokens for users based on the thread permissions.
166+
"""
167+
thread = note.thread
168+
169+
# TODO: Possible optimization.
170+
# Fetch tokens from the database if `fresh_thread=False` and use them to
171+
# derive the list of recipients instead of doing a couple of multi-table
172+
# DB queries.
173+
recipients = set(thread.thread_cc.values_list('user__id', 'user__email'))
174+
175+
# Include devs.
176+
if note.read_permission_developer:
177+
recipients.update(thread.addon.authors.values_list('id', 'email'))
178+
179+
groups_list = []
180+
# Include app reviewers.
181+
if note.read_permission_reviewer:
182+
groups_list.append('App Reviewers')
183+
184+
# Include senior app reviewers.
185+
if note.read_permission_senior_reviewer:
186+
groups_list.append('Senior App Reviewers')
187+
188+
# Include admins.
189+
if note.read_permission_staff:
190+
groups_list.append('Admins')
191+
192+
if len(groups_list) > 0:
193+
groups = Group.objects.filter(name__in=groups_list)
194+
for group in groups:
195+
recipients.update(group.users.values_list('id', 'email'))
196+
197+
# Include Mozilla contact.
198+
if (note.read_permission_mozilla_contact and
199+
thread.addon.mozilla_contact):
200+
for moz_contact in thread.addon.get_mozilla_contacts():
201+
try:
202+
user = UserProfile.objects.get(email=moz_contact)
203+
except UserProfile.DoesNotExist:
204+
pass
205+
else:
206+
recipients.add((user.id, moz_contact))
207+
208+
if (note.author.id, note.author.email) in recipients:
209+
recipients.remove((note.author.id, note.author.email))
210+
211+
new_recipients_list = []
212+
for user_id, user_email in recipients:
213+
tok = get_reply_token(note.thread, user_id)
214+
new_recipients_list.append((user_email, tok.uuid))
215+
216+
return new_recipients_list

mkt/comm/api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
ListModelMixin, RetrieveModelMixin)
1313
from rest_framework.permissions import BasePermission
1414
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
15+
from rest_framework.response import Response
1516
from rest_framework.serializers import ModelSerializer, SerializerMethodField
1617
from rest_framework.viewsets import GenericViewSet
1718

@@ -24,8 +25,8 @@
2425
from mkt.api.authentication import (RestOAuthAuthentication,
2526
RestSharedSecretAuthentication)
2627
from mkt.api.base import CORSMixin
28+
from mkt.reviewers.utils import send_note_emails
2729
from mkt.webpay.forms import PrepareForm
28-
from rest_framework.response import Response
2930

3031

3132
class AuthorSerializer(ModelSerializer):
@@ -262,8 +263,13 @@ def inherit_permissions(self, obj, parent):
262263
for key in ('developer', 'reviewer', 'senior_reviewer',
263264
'mozilla_contact', 'staff'):
264265
perm = 'read_permission_%s' % key
266+
265267
setattr(obj, perm, getattr(parent, perm))
266268

269+
def post_save(self, obj, created=False):
270+
if created:
271+
send_note_emails(obj)
272+
267273
def pre_save(self, obj):
268274
"""Inherit permissions from the thread."""
269275
self.inherit_permissions(obj, self.comm_thread)

mkt/comm/tests/test_api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22

33
from django.conf import settings
4+
from django.core import mail
45
from django.core.urlresolvers import reverse
56

67
import mock
@@ -248,6 +249,22 @@ def test_reply_create(self):
248249
eq_(res.status_code, 201)
249250
eq_(note.replies.count(), 1)
250251

252+
def test_note_emails(self):
253+
self.create_switch(name='comm-dashboard')
254+
note = CommunicationNote.objects.create(author=self.profile,
255+
thread=self.thread, note_type=0, body='something',
256+
read_permission_developer=True)
257+
res = self.client.post(reverse('comm-note-replies-list',
258+
kwargs={'thread_id': self.thread.id,
259+
'note_id': note.id}),
260+
data=json.dumps({'note_type': '0',
261+
'body': 'something'}))
262+
eq_(res.status_code, 201)
263+
264+
# Decrement authors.count() by 1 because the author of the note is
265+
# one of the authors of the addon.
266+
eq_(len(mail.outbox), self.thread.addon.authors.count() - 1)
267+
251268

252269
def test_mark_read(self):
253270
note = CommunicationNote.objects.create(author=self.profile,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
If you have any questions about this review, please reply to this email or join #app-reviewers on irc.mozilla.org. For general developer support, please contact {{ MKT_SUPPORT_EMAIL }}.
2+
3+
{% if waffle.switch('comm-dashboard') %}
4+
You can also post to {{ ('/comm/thread/' + thread_id)|absolutify }}
5+
{% endif %}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends 'reviewers/emails/base.txt' -%}
2+
{% block content %}
3+
A user posted on a discussion of {{ name }}.
4+
5+
{{ sender }} wrote:
6+
{{ comments }}
7+
{% include 'reviewers/emails/decisions/includes/questions.txt' %}
8+
{% endblock %}

mkt/reviewers/tests/test_views.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import amo.tests
2323
import reviews
2424
from abuse.models import AbuseReport
25-
from access.models import GroupUser
25+
from access.models import Group, GroupUser
2626
from addons.models import AddonDeviceType
2727
from amo.helpers import absolutify
2828
from amo.tests import (app_factory, check_links, days_ago,
@@ -982,10 +982,6 @@ def get_app(self):
982982
return Webapp.objects.get(id=337141)
983983

984984
def post(self, data, queue='pending'):
985-
# Set some action visibility form values.
986-
data['action_visibility'] = ('senior_reviewer', 'reviewer', 'staff',
987-
'mozilla_contact')
988-
self.create_switch(name='comm-dashboard')
989985
res = self.client.post(self.url, data)
990986
self.assert3xx(res, reverse('reviewers.apps.queue_%s' % queue))
991987

@@ -1063,7 +1059,7 @@ def _check_thread(self):
10631059
eq_(thread.count(), 1)
10641060

10651061
thread = thread.get()
1066-
perms = ('senior_reviewer', 'reviewer', 'staff', 'mozilla_contact')
1062+
perms = ('developer', 'reviewer', 'staff')
10671063

10681064
for key in perms:
10691065
assert getattr(thread, 'read_permission_%s' % key)
@@ -1090,6 +1086,26 @@ def _check_score(self, reviewed_type):
10901086
eq_(scores[0].score, amo.REVIEWED_SCORES[reviewed_type])
10911087
eq_(scores[0].note_key, reviewed_type)
10921088

1089+
def test_comm_emails(self):
1090+
data = {'action': 'reject', 'comments': 'suxor',
1091+
'action_visibility': ('developer', 'reviewer', 'staff')}
1092+
data.update(self._attachment_management_form(num=0))
1093+
self.create_switch(name='comm-dashboard')
1094+
self.post(data)
1095+
self._check_thread()
1096+
1097+
recipients = set(self.app.authors.values_list('email', flat=True))
1098+
recipients.update(Group.objects.get(
1099+
name='App Reviewers').users.values_list('email', flat=True))
1100+
recipients.update(Group.objects.get(
1101+
name='Admins').users.values_list('email', flat=True))
1102+
1103+
recipients.remove('editor@mozilla.com')
1104+
1105+
eq_(len(mail.outbox), len(recipients))
1106+
eq_(mail.outbox[0].subject, '%s has been reviewed.' %
1107+
self.get_app().name)
1108+
10931109
def test_xss(self):
10941110
data = {'action': 'comment',
10951111
'comments': '<script>alert("xss")</script>'}
@@ -1120,7 +1136,6 @@ def test_pending_to_public_w_device_overrides(self):
11201136
msg = mail.outbox[0]
11211137
self._check_email(msg, 'App Approved but waiting')
11221138
self._check_email_body(msg)
1123-
self._check_thread()
11241139

11251140
def test_pending_to_reject_w_device_overrides(self):
11261141
# This shouldn't be possible unless there's form hacking.
@@ -1144,7 +1159,6 @@ def test_pending_to_reject_w_device_overrides(self):
11441159
msg = mail.outbox[0]
11451160
self._check_email(msg, 'Submission Update')
11461161
self._check_email_body(msg)
1147-
self._check_thread()
11481162

11491163
def test_pending_to_public_w_requirements_overrides(self):
11501164
self.create_switch(name='buchets')
@@ -1209,7 +1223,6 @@ def test_pending_to_public(self, update_name, update_locales,
12091223
msg = mail.outbox[0]
12101224
self._check_email(msg, 'App Approved')
12111225
self._check_email_body(msg)
1212-
self._check_thread()
12131226
self._check_score(amo.REVIEWED_WEBAPP_HOSTED)
12141227

12151228
assert update_name.called
@@ -1281,7 +1294,6 @@ def test_pending_to_public_no_mozilla_contact(self):
12811294
msg = mail.outbox[0]
12821295
self._check_email(msg, 'App Approved', with_mozilla_contact=False)
12831296
self._check_email_body(msg)
1284-
self._check_thread()
12851297
self._check_score(amo.REVIEWED_WEBAPP_HOSTED)
12861298

12871299
@mock.patch('addons.tasks.index_addons')
@@ -1306,7 +1318,6 @@ def test_pending_to_public_waiting(self, update_name, update_locales,
13061318
msg = mail.outbox[0]
13071319
self._check_email(msg, 'App Approved but waiting')
13081320
self._check_email_body(msg)
1309-
self._check_thread()
13101321
self._check_score(amo.REVIEWED_WEBAPP_HOSTED)
13111322

13121323
assert not update_name.called
@@ -1340,7 +1351,6 @@ def test_pending_to_reject(self):
13401351
msg = mail.outbox[0]
13411352
self._check_email(msg, 'Submission Update')
13421353
self._check_email_body(msg)
1343-
self._check_thread()
13441354
self._check_score(amo.REVIEWED_WEBAPP_HOSTED)
13451355

13461356
def test_multiple_versions_reject_hosted(self):
@@ -1394,7 +1404,6 @@ def test_pending_to_escalation(self):
13941404
self._check_email(dev_msg, 'Submission Update')
13951405
adm_msg = mail.outbox[1]
13961406
self._check_admin_email(adm_msg, 'Escalated Review Requested')
1397-
self._check_thread()
13981407

13991408
def test_pending_to_disable_senior_reviewer(self):
14001409
self.login_as_senior_reviewer()
@@ -1409,7 +1418,6 @@ def test_pending_to_disable_senior_reviewer(self):
14091418
self._check_log(amo.LOG.APP_DISABLED)
14101419
eq_(len(mail.outbox), 1)
14111420
self._check_email(mail.outbox[0], 'App disabled by reviewer')
1412-
self._check_thread()
14131421

14141422
def test_pending_to_disable(self):
14151423
self.app.update(status=amo.STATUS_PUBLIC)
@@ -1438,7 +1446,6 @@ def test_escalation_to_public(self):
14381446
msg = mail.outbox[0]
14391447
self._check_email(msg, 'App Approved')
14401448
self._check_email_body(msg)
1441-
self._check_thread()
14421449

14431450
def test_escalation_to_reject(self):
14441451
EscalationQueue.objects.create(addon=self.app)
@@ -1457,7 +1464,6 @@ def test_escalation_to_reject(self):
14571464
eq_(len(mail.outbox), 1)
14581465
msg = mail.outbox[0]
14591466
self._check_email(msg, 'Submission Update')
1460-
self._check_thread()
14611467
self._check_email_body(msg)
14621468
self._check_score(amo.REVIEWED_WEBAPP_HOSTED)
14631469

@@ -1476,7 +1482,6 @@ def test_escalation_to_disable_senior_reviewer(self):
14761482
eq_(EscalationQueue.objects.count(), 0)
14771483
eq_(len(mail.outbox), 1)
14781484
self._check_email(mail.outbox[0], 'App disabled by reviewer')
1479-
self._check_thread()
14801485

14811486
def test_escalation_to_disable(self):
14821487
EscalationQueue.objects.create(addon=self.app)
@@ -1517,7 +1522,6 @@ def test_rereview_to_reject(self):
15171522
self._check_email(msg, 'Submission Update')
15181523
self._check_email_body(msg)
15191524
self._check_score(amo.REVIEWED_WEBAPP_REREVIEW)
1520-
self._check_thread()
15211525

15221526
def test_rereview_to_disable_senior_reviewer(self):
15231527
self.login_as_senior_reviewer()
@@ -1533,7 +1537,6 @@ def test_rereview_to_disable_senior_reviewer(self):
15331537
eq_(RereviewQueue.objects.filter(addon=self.app).count(), 0)
15341538
eq_(len(mail.outbox), 1)
15351539
self._check_email(mail.outbox[0], 'App disabled by reviewer')
1536-
self._check_thread()
15371540

15381541
def test_rereview_to_disable(self):
15391542
RereviewQueue.objects.create(addon=self.app)
@@ -1572,7 +1575,6 @@ def test_rereview_to_escalation(self):
15721575
self._check_email(dev_msg, 'Submission Update')
15731576
adm_msg = mail.outbox[1]
15741577
self._check_admin_email(adm_msg, 'Escalated Review Requested')
1575-
self._check_thread()
15761578

15771579
def test_more_information(self):
15781580
# Test the same for all queues.
@@ -1607,7 +1609,6 @@ def test_comment(self):
16071609
self.post(data)
16081610
eq_(len(mail.outbox), 0)
16091611
self._check_log(amo.LOG.COMMENT_VERSION)
1610-
self._check_thread()
16111612

16121613
def test_receipt_no_node(self):
16131614
res = self.client.get(self.url)

0 commit comments

Comments
 (0)