diff --git a/tests/unit/email/test_init.py b/tests/unit/email/test_init.py
index 759fbde61243..deacac4c897f 100644
--- a/tests/unit/email/test_init.py
+++ b/tests/unit/email/test_init.py
@@ -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 ",
+ attr.asdict(
+ EmailMessage(
+ subject="Email Subject",
+ body_text="Email Body",
+ body_html=(
+ "\n\n"
+ "Email HTML Body
\n\n"
+ ),
+ ),
+ ),
+ ),
+ pretend.call(
+ "submitterusername ",
+ attr.asdict(
+ EmailMessage(
+ subject="Email Subject",
+ body_text="Email Body",
+ body_html=(
+ "\n\n"
+ "Email HTML Body
\n\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 ",
+ attr.asdict(
+ EmailMessage(
+ subject="Email Subject",
+ body_text="Email Body",
+ body_html=(
+ "\n\n"
+ "Email HTML Body
\n\n"
+ ),
+ ),
+ ),
+ ),
+ pretend.call(
+ "submitterusername ",
+ attr.asdict(
+ EmailMessage(
+ subject="Email Subject",
+ body_text="Email Body",
+ body_html=(
+ "\n\n"
+ "Email HTML Body
\n\n"
+ ),
+ )
+ ),
+ ),
+ ]
+
+
class TestTwoFactorEmail:
@pytest.mark.parametrize(
("action", "method", "pretty_method"),
diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py
index 5bb3f585a44f..cbc3a777517d 100644
--- a/tests/unit/manage/test_views.py
+++ b/tests/unit/manage/test_views.py
@@ -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")
@@ -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()
@@ -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(
diff --git a/warehouse/email/__init__.py b/warehouse/email/__init__.py
index 9ff6b2118b9a..00caabfb60ca 100644
--- a/warehouse/email/__init__.py
+++ b/warehouse/email/__init__.py
@@ -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)
diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py
index 578b37de883e..0c158440163f 100644
--- a/warehouse/manage/views.py
+++ b/warehouse/manage/views.py
@@ -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,
)
@@ -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(
diff --git a/warehouse/templates/email/removed-project-release-file/body.html b/warehouse/templates/email/removed-project-release-file/body.html
new file mode 100644
index 000000000000..c3cc67fef724
--- /dev/null
+++ b/warehouse/templates/email/removed-project-release-file/body.html
@@ -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 %}
+
+
+ - {% 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 %}
+ - {% trans submitter_name=submitter_name, role=submitter_role %}Deleted by: {{ submitter_name }} with a role:
+ {{ role }}.{% endtrans %}
+
+
+
+
+{% trans href='mailto:admin@pypi.org', email_address='admin@pypi.org' %}If this was a mistake, you can email {{ email_address }} to communicate with the PyPI administrators.{% endtrans %}
+{% endblock %}
+
+{% block reason %}
+
+{% trans recipient_role_descr=recipient_role_descr %}
+You are receiving this because you are {{ recipient_role_descr }} of this project.{% endtrans %}
+
+{% endblock %}
diff --git a/warehouse/templates/email/removed-project-release-file/body.txt b/warehouse/templates/email/removed-project-release-file/body.txt
new file mode 100644
index 000000000000..7c3fa38fc890
--- /dev/null
+++ b/warehouse/templates/email/removed-project-release-file/body.txt
@@ -0,0 +1,30 @@
+{#
+ # 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.txt" %}
+
+{% block content %}
+ {% 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 %}
+
+ {% trans submitter_name=submitter_name, role=submitter_role %}Deleted by: {{ submitter_name }} with a role: {{ role }}.{% endtrans %}
+
+ {% trans email_address='admin@pypi.org' %}If this was a mistake, you can email {{ email_address }} to communicate with the PyPI administrators.{% endtrans %}
+{% endblock %}
+
+{% block reason %}
+ {% trans recipient_role_descr=recipient_role_descr %}
+ You are receiving this because you are {{ recipient_role_descr }} of this project.
+ {% endtrans %}
+{% endblock %}
diff --git a/warehouse/templates/email/removed-project-release-file/subject.txt b/warehouse/templates/email/removed-project-release-file/subject.txt
new file mode 100644
index 000000000000..80243ac026fb
--- /dev/null
+++ b/warehouse/templates/email/removed-project-release-file/subject.txt
@@ -0,0 +1,17 @@
+{#
+ # 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/subject.txt" %}
+
+{% block subject %}{% trans project_name=project_name, release_version=release_version %}File deleted for {{ project_name }} {{ release_version }}{% endtrans %}{% endblock %}