Skip to content

Commit

Permalink
Add email templates for removed file from release
Browse files Browse the repository at this point in the history
Until now, when there are multiple contributors on a single
the project, if one of them deletes a file from certain release
the other contributors don't get any notification,
which is problematic.

Connected with issue pypi#5714

Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
  • Loading branch information
MVrachev committed Mar 18, 2020
1 parent 4146653 commit c66ad74
Show file tree
Hide file tree
Showing 7 changed files with 380 additions and 1 deletion.
212 changes: 212 additions & 0 deletions tests/unit/email/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,218 @@ def test_send_removed_project_release_emai_to_owner(
]


class TestRemovedReleaseFileEmail:
def test_send_removed_project_release_file_email_to_owner(
self, pyramid_request, pyramid_config, monkeypatch
):
stub_user = pretend.stub(
username="username",
name="",
email="email@example.com",
primary_email=pretend.stub(email="email@example.com", verified=True),
)
stub_submitter_user = pretend.stub(
username="submitterusername",
name="",
email="submiteremail@example.com",
primary_email=pretend.stub(
email="submiteremail@example.com", verified=True
),
)

subject_renderer = pyramid_config.testing_add_renderer(
"email/removed-project-release-file/subject.txt"
)
subject_renderer.string_response = "Email Subject"
body_renderer = pyramid_config.testing_add_renderer(
"email/removed-project-release-file/body.txt"
)
body_renderer.string_response = "Email Body"
html_renderer = pyramid_config.testing_add_renderer(
"email/removed-project-release-file/body.html"
)
html_renderer.string_response = "Email HTML Body"

send_email = pretend.stub(
delay=pretend.call_recorder(lambda *args, **kwargs: None)
)
pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
monkeypatch.setattr(email, "send_email", send_email)

release = pretend.stub(
version="0.0.0",
project=pretend.stub(name="test_project"),
created=datetime.datetime(2017, 2, 5, 0, 0, 0, 0),
)

result = email.send_removed_project_release_file_email(
pyramid_request,
[stub_user, stub_submitter_user],
file="test-file-0.0.0.tar.gz",
release=release,
submitter_name=stub_submitter_user.username,
submitter_role="Owner",
recipient_role="Owner",
)

assert result == {
"file": "test-file-0.0.0.tar.gz",
"project_name": release.project.name,
"release_version": release.version,
"submitter_name": stub_submitter_user.username,
"submitter_role": "owner",
"recipient_role_descr": "an owner",
}

subject_renderer.assert_(project_name="test_project")
subject_renderer.assert_(release_version="0.0.0")
body_renderer.assert_(file="test-file-0.0.0.tar.gz")
body_renderer.assert_(release_version="0.0.0")
body_renderer.assert_(project_name="test_project")
body_renderer.assert_(submitter_name=stub_submitter_user.username)
body_renderer.assert_(submitter_role="owner")
body_renderer.assert_(recipient_role_descr="an owner")

assert pyramid_request.task.calls == [
pretend.call(send_email),
pretend.call(send_email),
]

assert send_email.delay.calls == [
pretend.call(
"username <email@example.com>",
attr.asdict(
EmailMessage(
subject="Email Subject",
body_text="Email Body",
body_html=(
"<html>\n<head></head>\n"
"<body><p>Email HTML Body</p></body>\n</html>\n"
),
),
),
),
pretend.call(
"submitterusername <submiteremail@example.com>",
attr.asdict(
EmailMessage(
subject="Email Subject",
body_text="Email Body",
body_html=(
"<html>\n<head></head>\n"
"<body><p>Email HTML Body</p></body>\n</html>\n"
),
)
),
),
]

def test_send_removed_project_release_file_email_to_maintainer(
self, pyramid_request, pyramid_config, monkeypatch
):
stub_user = pretend.stub(
username="username",
name="",
email="email@example.com",
primary_email=pretend.stub(email="email@example.com", verified=True),
)
stub_submitter_user = pretend.stub(
username="submitterusername",
name="",
email="submiteremail@example.com",
primary_email=pretend.stub(
email="submiteremail@example.com", verified=True
),
)

subject_renderer = pyramid_config.testing_add_renderer(
"email/removed-project-release-file/subject.txt"
)
subject_renderer.string_response = "Email Subject"
body_renderer = pyramid_config.testing_add_renderer(
"email/removed-project-release-file/body.txt"
)
body_renderer.string_response = "Email Body"
html_renderer = pyramid_config.testing_add_renderer(
"email/removed-project-release-file/body.html"
)
html_renderer.string_response = "Email HTML Body"

send_email = pretend.stub(
delay=pretend.call_recorder(lambda *args, **kwargs: None)
)
pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
monkeypatch.setattr(email, "send_email", send_email)

release = pretend.stub(
version="0.0.0",
project=pretend.stub(name="test_project"),
created=datetime.datetime(2017, 2, 5, 0, 0, 0, 0),
)

result = email.send_removed_project_release_file_email(
pyramid_request,
[stub_user, stub_submitter_user],
file="test-file-0.0.0.tar.gz",
release=release,
submitter_name=stub_submitter_user.username,
submitter_role="Owner",
recipient_role="Maintainer",
)

assert result == {
"file": "test-file-0.0.0.tar.gz",
"project_name": release.project.name,
"release_version": release.version,
"submitter_name": stub_submitter_user.username,
"submitter_role": "owner",
"recipient_role_descr": "a maintainer",
}

subject_renderer.assert_(project_name="test_project")
subject_renderer.assert_(release_version="0.0.0")
body_renderer.assert_(file="test-file-0.0.0.tar.gz")
body_renderer.assert_(release_version="0.0.0")
body_renderer.assert_(project_name="test_project")
body_renderer.assert_(submitter_name=stub_submitter_user.username)
body_renderer.assert_(submitter_role="owner")
body_renderer.assert_(recipient_role_descr="a maintainer")

assert pyramid_request.task.calls == [
pretend.call(send_email),
pretend.call(send_email),
]

assert send_email.delay.calls == [
pretend.call(
"username <email@example.com>",
attr.asdict(
EmailMessage(
subject="Email Subject",
body_text="Email Body",
body_html=(
"<html>\n<head></head>\n"
"<body><p>Email HTML Body</p></body>\n</html>\n"
),
),
),
),
pretend.call(
"submitterusername <submiteremail@example.com>",
attr.asdict(
EmailMessage(
subject="Email Subject",
body_text="Email Body",
body_html=(
"<html>\n<head></head>\n"
"<body><p>Email HTML Body</p></body>\n</html>\n"
),
)
),
),
]


class TestTwoFactorEmail:
@pytest.mark.parametrize(
("action", "method", "pretty_method"),
Expand Down
42 changes: 41 additions & 1 deletion tests/unit/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2813,7 +2813,7 @@ def test_delete_project_release_file_disallow_deletion(self):
)
]

def test_delete_project_release_file(self, db_request):
def test_delete_project_release_file(self, monkeypatch, db_request):
user = UserFactory.create()

project = ProjectFactory.create(name="foobar")
Expand All @@ -2834,6 +2834,25 @@ def test_delete_project_release_file(self, db_request):
db_request.user = user
db_request.remote_addr = "1.2.3.4"

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_release_file_email = pretend.call_recorder(
lambda req, user, **k: None
)
monkeypatch.setattr(
views,
"send_removed_project_release_file_email",
send_removed_project_release_file_email,
)

view = views.ManageProjectRelease(release, db_request)

result = view.delete_project_release_file()
Expand Down Expand Up @@ -2865,6 +2884,27 @@ def test_delete_project_release_file(self, db_request):
)
]

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_release_file_email.calls == [
pretend.call(
db_request,
db_request.user,
file=release_file.filename,
release=release,
submitter_name=db_request.user.username,
submitter_role="Owner",
recipient_role="Owner",
)
]

def test_delete_project_release_file_no_confirm(self):
release = pretend.stub(version="1.2.3", project=pretend.stub(name="foobar"))
request = pretend.stub(
Expand Down
18 changes: 18 additions & 0 deletions warehouse/email/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,24 @@ def send_removed_project_release_email(
}


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

return {
"file": file,
"project_name": release.project.name,
"release_version": release.version,
"submitter_name": 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
21 changes: 21 additions & 0 deletions warehouse/manage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
send_primary_email_change_email,
send_removed_project_email,
send_removed_project_release_email,
send_removed_project_release_file_email,
send_two_factor_added_email,
send_two_factor_removed_email,
)
Expand Down Expand Up @@ -1242,6 +1243,26 @@ def _error(message):
},
)

submitter_role = get_user_role_in_project(
project_name, self.request.user.username, self.request
)
contributors = get_project_contributors(project_name, self.request)

for contributor in contributors:
contributor_role = get_user_role_in_project(
project_name, contributor.username, self.request
)

send_removed_project_release_file_email(
self.request,
contributor,
file=release_file.filename,
release=self.release,
submitter_name=self.request.user.username,
submitter_role=submitter_role,
recipient_role=contributor_role,
)

self.request.db.delete(release_file)

self.request.session.flash(
Expand Down
41 changes: 41 additions & 0 deletions warehouse/templates/email/removed-project-release-file/body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "email/_base/body.html" %}

{% block extra_style %}
ul.collaborator-details {
list-style-type: none;
}
{% endblock %}

{% block content %}
<p>
<ul class="removed-project-release-file">
<li>{% trans file=file, release_version=release_version, project_name=project_name %}The file {{ file }} from release {{ release_version }} of the {{ project_name }} project has been deleted.{% endtrans %}</li>
<li>{% trans submitter_name=submitter_name, role=submitter_role %}<strong>Deleted by:</strong> {{ submitter_name }} with a role:
{{ role }}.{% endtrans %}
</li>
</ul>
</p>

<p>{% trans href='mailto:admin@pypi.org', email_address='admin@pypi.org' %}If this was a mistake, you can email <a
href="{{ href }}">{{ email_address }}</a> to communicate with the PyPI administrators.{% endtrans %}</p>
{% endblock %}

{% block reason %}

<p>{% trans recipient_role_descr=recipient_role_descr %}
You are receiving this because you are {{ recipient_role_descr }} of this project.{% endtrans %}</p>

{% endblock %}
Loading

0 comments on commit c66ad74

Please sign in to comment.