Skip to content

Add email notification on project/release removal #7071

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

Merged
merged 3 commits into from
Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
398 changes: 398 additions & 0 deletions tests/unit/email/test_init.py

Large diffs are not rendered by default.

144 changes: 143 additions & 1 deletion tests/unit/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2310,7 +2310,75 @@ def test_delete_project_disallow_deletion(self):
pretend.call("manage.project.settings", project_name="foo")
]

def test_delete_project(self, db_request):
def test_get_project_contributors(self, db_request):
project = ProjectFactory.create(name="foo")
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None),
)

db_request.user = UserFactory.create()
project.users = [db_request.user]

res = views.get_project_contributors(project.name, db_request)
assert res == [db_request.user]

def test_get_user_role_in_project_single_role_owner(self, db_request):
project = ProjectFactory.create(name="foo")
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None),
)
db_request.user = UserFactory.create()
project.users = [db_request.user]
RoleFactory(user=db_request.user, project=project)

res = views.get_user_role_in_project(
project.name, db_request.user.username, db_request
)
assert res == "Owner"

def test_get_user_role_in_project_single_role_maintainer(self, db_request):
project = ProjectFactory.create(name="foo")
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None),
)
db_request.user = UserFactory.create()
project.users = [db_request.user]
RoleFactory(user=db_request.user, project=project, role_name="Maintainer")

res = views.get_user_role_in_project(
project.name, db_request.user.username, db_request
)
assert res == "Maintainer"

def test_get_user_role_in_project_two_roles_owner_and_maintainer(self, db_request):
project = ProjectFactory.create(name="foo")
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None),
)
db_request.user = UserFactory.create()
project.users = [db_request.user]
RoleFactory(user=db_request.user, project=project, role_name="Owner")
RoleFactory(user=db_request.user, project=project, role_name="Maintainer")

res = views.get_user_role_in_project(
project.name, db_request.user.username, db_request
)
assert res == "Owner"

def test_get_user_role_in_project_no_role(self, db_request):
project = ProjectFactory.create(name="foo")
db_request.session = pretend.stub(
flash=pretend.call_recorder(lambda *a, **kw: None),
)
db_request.user = UserFactory.create()
project.users = [db_request.user]

res = views.get_user_role_in_project(
project.name, db_request.user.username, db_request
)
assert res == ""

def test_delete_project(self, monkeypatch, db_request):
project = ProjectFactory.create(name="foo")

db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
Expand All @@ -2319,6 +2387,22 @@ def test_delete_project(self, db_request):
)
db_request.POST["confirm_project_name"] = project.normalized_name
db_request.user = UserFactory.create()

get_user_role_in_project = pretend.call_recorder(
lambda project_name, username, req: "Owner"
)
monkeypatch.setattr(views, "get_user_role_in_project", get_user_role_in_project)

get_project_contributors = pretend.call_recorder(
lambda project_name, req: [db_request.user]
)
monkeypatch.setattr(views, "get_project_contributors", get_project_contributors)

send_removed_project_email = pretend.call_recorder(lambda req, user, **k: None)
monkeypatch.setattr(
views, "send_removed_project_email", send_removed_project_email
)

db_request.remote_addr = "192.168.1.1"

result = views.delete_project(project, db_request)
Expand All @@ -2329,6 +2413,26 @@ def test_delete_project(self, db_request):
assert db_request.route_path.calls == [pretend.call("manage.projects")]
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/the-redirect"

assert get_user_role_in_project.calls == [
pretend.call(project.name, db_request.user.username, db_request,),
pretend.call(project.name, db_request.user.username, db_request,),
]

assert get_project_contributors.calls == [
pretend.call(project.name, db_request,)
]

assert send_removed_project_email.calls == [
pretend.call(
db_request,
db_request.user,
project_name=project.name,
submitter_name=db_request.user.username,
submitter_role="Owner",
recipient_role="Owner",
)
]
assert not (db_request.db.query(Project).filter(Project.name == "foo").count())


Expand Down Expand Up @@ -2495,6 +2599,7 @@ def test_delete_project_release(self, monkeypatch):
project=pretend.stub(
name="foobar", record_event=pretend.call_recorder(lambda *a, **kw: None)
),
created=datetime.datetime(2017, 2, 5, 17, 18, 18, 462_634),
)
request = pretend.stub(
POST={"confirm_version": release.version},
Expand All @@ -2511,7 +2616,25 @@ def test_delete_project_release(self, monkeypatch):
)
journal_obj = pretend.stub()
journal_cls = pretend.call_recorder(lambda **kw: journal_obj)

get_user_role_in_project = pretend.call_recorder(
lambda project_name, username, req: "Owner"
)
monkeypatch.setattr(views, "get_user_role_in_project", get_user_role_in_project)
get_project_contributors = pretend.call_recorder(
lambda project_name, request: [request.user]
)
monkeypatch.setattr(views, "get_project_contributors", get_project_contributors)

monkeypatch.setattr(views, "JournalEntry", journal_cls)
send_removed_project_release_email = pretend.call_recorder(
lambda req, contrib, **k: None
)
monkeypatch.setattr(
views,
"send_removed_project_release_email",
send_removed_project_release_email,
)

view = views.ManageProjectRelease(release, request)

Expand All @@ -2520,6 +2643,25 @@ def test_delete_project_release(self, monkeypatch):
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/the-redirect"

assert get_user_role_in_project.calls == [
pretend.call(release.project.name, request.user.username, request,),
pretend.call(release.project.name, request.user.username, request,),
]
assert get_project_contributors.calls == [
pretend.call(release.project.name, request,)
]

assert send_removed_project_release_email.calls == [
pretend.call(
request,
request.user,
release=release,
submitter_name=request.user.username,
submitter_role="Owner",
recipient_role="Owner",
)
]

assert request.db.delete.calls == [pretend.call(release)]
assert request.db.add.calls == [pretend.call(journal_obj)]
assert request.flags.enabled.calls == [
Expand Down
34 changes: 34 additions & 0 deletions warehouse/email/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,40 @@ def send_two_factor_removed_email(request, user, method):
return {"method": pretty_methods[method], "username": user.username}


@_email("removed-project")
def send_removed_project_email(
request, user, *, project_name, submitter_name, submitter_role, recipient_role
):
recipient_role_descr = "an owner"
if recipient_role == "Maintainer":
recipient_role_descr = "a maintainer"

return {
"project": project_name,
"submitter": submitter_name,
"submitter_role": submitter_role.lower(),
"recipient_role_descr": recipient_role_descr,
}


@_email("removed-project-release")
def send_removed_project_release_email(
request, user, *, release, submitter_name, submitter_role, recipient_role
):
recipient_role_descr = "an owner"
if recipient_role == "Maintainer":
recipient_role_descr = "a maintainer"

return {
"project": release.project.name,
"release": release.version,
"release_date": release.created.strftime("%Y-%m-%d"),
"submitter": submitter_name,
"submitter_role": submitter_role.lower(),
"recipient_role_descr": recipient_role_descr,
}


def includeme(config):
email_sending_class = config.maybe_dotted(config.registry.settings["mail.backend"])
config.register_service_factory(email_sending_class.create_service, IEmailSender)
Expand Down
52 changes: 47 additions & 5 deletions warehouse/locale/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -169,25 +169,25 @@ msgstr ""
msgid "Email address ${email_address} verified. ${confirm_message}."
msgstr ""

#: warehouse/manage/views.py:172
#: warehouse/manage/views.py:174
msgid "Email ${email_address} added - check your email for a verification link"
msgstr ""

#: warehouse/manage/views.py:653 warehouse/manage/views.py:689
#: warehouse/manage/views.py:655 warehouse/manage/views.py:691
msgid ""
"You must provision a two factor method before recovery codes can be "
"generated"
msgstr ""

#: warehouse/manage/views.py:664
#: warehouse/manage/views.py:666
msgid "Recovery codes already generated"
msgstr ""

#: warehouse/manage/views.py:665
#: warehouse/manage/views.py:667
msgid "Generating new recovery codes will invalidate your existing codes."
msgstr ""

#: warehouse/manage/views.py:715
#: warehouse/manage/views.py:717
msgid "Invalid credentials. Try again"
msgstr ""

Expand Down Expand Up @@ -1255,6 +1255,48 @@ msgid ""
"<code>%(new_email)s</code>"
msgstr ""

#: warehouse/templates/email/removed-project/body.html:25
#, python-format
msgid "The project %(project)s has been deleted."
msgstr ""

#: warehouse/templates/email/removed-project-release/body.html:26
#: warehouse/templates/email/removed-project/body.html:26
#, python-format
msgid ""
"<strong>Deleted by:</strong> %(submitter)s with a role:\n"
" %(role)s."
msgstr ""

#: warehouse/templates/email/removed-project-release/body.html:32
#: warehouse/templates/email/removed-project/body.html:32
#, python-format
msgid ""
"If this was a mistake, you can email <a\n"
" href=\"%(href)s\">%(email_address)s</a> to communicate with the PyPI "
"administrators."
msgstr ""

#: warehouse/templates/email/removed-project/body.html:38
#, python-format
msgid ""
"You are receiving this because you are %(recipient_role_descr)s of this "
"project."
msgstr ""

#: warehouse/templates/email/removed-project-release/body.html:25
#, python-format
msgid "The %(project)s release %(release)s released on %(date)s has been deleted."
msgstr ""

#: warehouse/templates/email/removed-project-release/body.html:38
#, python-format
msgid ""
"\n"
"You are receiving this because you are %(recipient_role_descr)s of this "
"project."
msgstr ""

#: warehouse/templates/email/two-factor-added/body.html:18
#, python-format
msgid ""
Expand Down
Loading