Skip to content

Commit 4f6d3c9

Browse files
Mariattadi
authored andcommitted
Send email when a new collaborator has been added to the project. (#3155)
* Send email when a new collaborator has been added to the project. - send the email to the newly added collaborator - send the email to other owners - email not sent to the person who added the collaborator Closes #1000 * Remove prints * Make linter happy. * Added functionality to send email whenever primary email is changed (#3158) Addressed code reviews * - Send a separate welcome email to the new collaborator - Add owners emails in bcc field * PEP 8 * Fix linters errors * - Deindent - Add footer to email * Rebased with master * Fix broken unit test. * Fix linter error * PEP 8 * Actually send the email to bcc recipients
1 parent 9a3b070 commit 4f6d3c9

8 files changed

+249
-3
lines changed

tests/unit/manage/test_views.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,7 +1236,7 @@ def test_post_new_role_validation_fails(self, db_request):
12361236
"form": form_obj,
12371237
}
12381238

1239-
def test_post_new_role(self, db_request):
1239+
def test_post_new_role(self, monkeypatch, db_request):
12401240
project = ProjectFactory.create(name="foobar")
12411241
user = UserFactory.create(username="testuser")
12421242

@@ -1261,6 +1261,21 @@ def test_post_new_role(self, db_request):
12611261
flash=pretend.call_recorder(lambda *a, **kw: None),
12621262
)
12631263

1264+
send_collaborator_added_email = pretend.call_recorder(lambda *a: None)
1265+
monkeypatch.setattr(
1266+
views,
1267+
'send_collaborator_added_email',
1268+
send_collaborator_added_email,
1269+
)
1270+
1271+
send_added_as_collaborator_email = pretend.call_recorder(
1272+
lambda *a: None)
1273+
monkeypatch.setattr(
1274+
views,
1275+
'send_added_as_collaborator_email',
1276+
send_added_as_collaborator_email,
1277+
)
1278+
12641279
result = views.manage_project_roles(
12651280
project, db_request, _form_class=form_class
12661281
)
@@ -1277,6 +1292,26 @@ def test_post_new_role(self, db_request):
12771292
pretend.call("Added collaborator 'testuser'", queue="success"),
12781293
]
12791294

1295+
assert send_collaborator_added_email.calls == [
1296+
pretend.call(
1297+
db_request,
1298+
user,
1299+
db_request.user,
1300+
project.name,
1301+
form_obj.role_name.data,
1302+
[]
1303+
)
1304+
]
1305+
1306+
assert send_added_as_collaborator_email.calls == [
1307+
pretend.call(
1308+
db_request,
1309+
db_request.user,
1310+
project.name,
1311+
form_obj.role_name.data,
1312+
user.email)
1313+
]
1314+
12801315
# Only one role is created
12811316
role = db_request.db.query(Role).one()
12821317

tests/unit/test_email.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,126 @@ def test_primary_email_change_email(
384384
recipients=['old_email'],
385385
),
386386
]
387+
388+
389+
class TestCollaboratorAddedEmail:
390+
391+
def test_collaborator_added_email(
392+
self, pyramid_request, pyramid_config, monkeypatch):
393+
394+
stub_user = pretend.stub(
395+
email='email',
396+
username='username',
397+
)
398+
stub_submitter_user = pretend.stub(
399+
email='submiteremail',
400+
username='submitterusername'
401+
)
402+
subject_renderer = pyramid_config.testing_add_renderer(
403+
'email/collaborator-added.subject.txt'
404+
)
405+
subject_renderer.string_response = 'Email Subject'
406+
body_renderer = pyramid_config.testing_add_renderer(
407+
'email/collaborator-added.body.txt'
408+
)
409+
body_renderer.string_response = 'Email Body'
410+
411+
send_email = pretend.stub(
412+
delay=pretend.call_recorder(lambda *args, **kwargs: None)
413+
)
414+
pyramid_request.task = pretend.call_recorder(
415+
lambda *args, **kwargs: send_email
416+
)
417+
monkeypatch.setattr(email, 'send_email', send_email)
418+
419+
result = email.send_collaborator_added_email(
420+
pyramid_request,
421+
user=stub_user,
422+
submitter=stub_submitter_user,
423+
project_name='test_project',
424+
role='Owner',
425+
email_recipients=[stub_user.email, stub_submitter_user.email]
426+
)
427+
428+
assert result == {
429+
'username': stub_user.username,
430+
'project': 'test_project',
431+
'role': 'Owner',
432+
'submitter': stub_submitter_user.username
433+
}
434+
subject_renderer.assert_()
435+
body_renderer.assert_(username=stub_user.username)
436+
body_renderer.assert_(project='test_project')
437+
body_renderer.assert_(role='Owner')
438+
body_renderer.assert_(submitter=stub_submitter_user.username)
439+
440+
assert pyramid_request.task.calls == [
441+
pretend.call(send_email),
442+
]
443+
assert send_email.delay.calls == [
444+
pretend.call(
445+
'Email Body',
446+
'Email Subject',
447+
bcc=[stub_user.email, stub_submitter_user.email],
448+
),
449+
]
450+
451+
452+
class TestAddedAsCollaboratorEmail:
453+
454+
def test_added_as_collaborator_email(
455+
self, pyramid_request, pyramid_config, monkeypatch):
456+
457+
stub_user = pretend.stub(
458+
email='email',
459+
username='username',
460+
)
461+
stub_submitter_user = pretend.stub(
462+
email='submiteremail',
463+
username='submitterusername'
464+
)
465+
subject_renderer = pyramid_config.testing_add_renderer(
466+
'email/added-as-collaborator.subject.txt'
467+
)
468+
subject_renderer.string_response = 'Email Subject'
469+
body_renderer = pyramid_config.testing_add_renderer(
470+
'email/added-as-collaborator.body.txt'
471+
)
472+
body_renderer.string_response = 'Email Body'
473+
474+
send_email = pretend.stub(
475+
delay=pretend.call_recorder(lambda *args, **kwargs: None)
476+
)
477+
pyramid_request.task = pretend.call_recorder(
478+
lambda *args, **kwargs: send_email
479+
)
480+
monkeypatch.setattr(email, 'send_email', send_email)
481+
482+
result = email.send_added_as_collaborator_email(
483+
pyramid_request,
484+
submitter=stub_submitter_user,
485+
project_name='test_project',
486+
role='Owner',
487+
user_email=stub_user.email
488+
)
489+
490+
assert result == {
491+
'project': 'test_project',
492+
'role': 'Owner',
493+
'submitter': stub_submitter_user.username
494+
}
495+
subject_renderer.assert_()
496+
body_renderer.assert_(submitter=stub_submitter_user.username)
497+
body_renderer.assert_(project='test_project')
498+
body_renderer.assert_(role='Owner')
499+
500+
assert pyramid_request.task.calls == [
501+
pretend.call(send_email),
502+
]
503+
assert send_email.delay.calls == [
504+
pretend.call(
505+
'Email Body',
506+
'Email Subject',
507+
recipients=[stub_user.email],
508+
),
509+
]

warehouse/email.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919

2020

2121
@tasks.task(bind=True, ignore_result=True, acks_late=True)
22-
def send_email(task, request, body, subject, *, recipients=None):
22+
def send_email(task, request, body, subject, *, recipients=None, bcc=None):
2323

2424
mailer = get_mailer(request)
2525
message = Message(
2626
body=body,
2727
recipients=recipients,
28+
bcc=bcc,
2829
sender=request.registry.settings.get('mail.sender'),
2930
subject=subject
3031
)
@@ -145,3 +146,46 @@ def send_primary_email_change_email(request, user, email):
145146
request.task(send_email).delay(body, subject, recipients=[email])
146147

147148
return fields
149+
150+
151+
def send_collaborator_added_email(request, user, submitter, project_name, role,
152+
email_recipients):
153+
fields = {
154+
'username': user.username,
155+
'project': project_name,
156+
'submitter': submitter.username,
157+
'role': role
158+
}
159+
160+
subject = render(
161+
'email/collaborator-added.subject.txt', fields, request=request
162+
)
163+
164+
body = render(
165+
'email/collaborator-added.body.txt', fields, request=request
166+
)
167+
168+
request.task(send_email).delay(body, subject, bcc=email_recipients)
169+
170+
return fields
171+
172+
173+
def send_added_as_collaborator_email(request, submitter, project_name, role,
174+
user_email):
175+
fields = {
176+
'project': project_name,
177+
'submitter': submitter.username,
178+
'role': role
179+
}
180+
181+
subject = render(
182+
'email/added-as-collaborator.subject.txt', fields, request=request
183+
)
184+
185+
body = render(
186+
'email/added-as-collaborator.body.txt', fields, request=request
187+
)
188+
189+
request.task(send_email).delay(body, subject, recipients=[user_email])
190+
191+
return fields

warehouse/manage/views.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
from warehouse.accounts.models import User, Email
2323
from warehouse.accounts.views import logout
2424
from warehouse.email import (
25-
send_account_deletion_email, send_email_verification_email,
25+
send_account_deletion_email, send_added_as_collaborator_email,
26+
send_collaborator_added_email, send_email_verification_email,
2627
send_password_change_email, send_primary_email_change_email
2728
)
2829
from warehouse.manage.forms import (
@@ -551,6 +552,32 @@ def manage_project_roles(project, request, _form_class=CreateRoleForm):
551552
submitted_from=request.remote_addr,
552553
),
553554
)
555+
556+
owners = (
557+
request.db.query(Role)
558+
.join(Role.user)
559+
.filter(Role.role_name == 'Owner', Role.project == project)
560+
)
561+
owner_emails = [owner.user.email for owner in owners]
562+
owner_emails.remove(request.user.email)
563+
564+
send_collaborator_added_email(
565+
request,
566+
user,
567+
request.user,
568+
project.name,
569+
form.role_name.data,
570+
owner_emails
571+
)
572+
573+
send_added_as_collaborator_email(
574+
request,
575+
request.user,
576+
project.name,
577+
form.role_name.data,
578+
user.email
579+
)
580+
554581
request.session.flash(
555582
f"Added collaborator '{form.username.data}'",
556583
queue="success"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
You have been added as {{ role }} to the PyPI project {{ project }} by {{ submitter }}.
2+
3+
Congratulations!
4+
5+
If this was a mistake, you can reply to this email directly to communicate with
6+
the PyPI administrators.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PyPI Collaborator Added Notification
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
A new collaborator has been added to a project you own on PyPI:
2+
3+
Username: {{ username }}
4+
Role: {{ role }}
5+
Collaborator for: {{ project }}
6+
Added by: {{ submitter }}
7+
8+
If this was a mistake, you can reply to this email directly to communicate with
9+
the PyPI administrators.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PyPI Collaborator Added Notification

0 commit comments

Comments
 (0)