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/locale/messages.pot b/warehouse/locale/messages.pot index 6424aab11372..4f111cd58416 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -169,25 +169,25 @@ msgstr "" msgid "Email address ${email_address} verified. ${confirm_message}." msgstr "" -#: warehouse/manage/views.py:187 +#: warehouse/manage/views.py:188 msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views.py:668 warehouse/manage/views.py:704 +#: warehouse/manage/views.py:669 warehouse/manage/views.py:705 msgid "" "You must provision a two factor method before recovery codes can be " "generated" msgstr "" -#: warehouse/manage/views.py:679 +#: warehouse/manage/views.py:680 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views.py:680 +#: warehouse/manage/views.py:681 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views.py:730 +#: warehouse/manage/views.py:731 msgid "Invalid credentials. Try again" msgstr "" 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..af5b39d23a48 --- /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 %} +

+

    +
  • The file {{ file }} from release {{ release_version }} of the {{ project_name }} project has been deleted.
  • +
  • Deleted by: {{ submitter_name }} with a role: + {{ submitter_role }}. +
  • +
+

+ +

If this was a mistake, you can email admin@pypi.org to communicate with the PyPI administrators.

+{% endblock %} + +{% block reason %} + +

+You are receiving this because you are {{ recipient_role_descr }} of this project.

+ +{% 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..51557606142c --- /dev/null +++ b/warehouse/templates/email/removed-project-release-file/body.txt @@ -0,0 +1,27 @@ +{# + # 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 %} + The file {{ file }} from release {{ release_version }} of the {{ project_name }} project has been deleted. + + Deleted by: {{ submitter_name }} with a role: {{ submitter_role }}. + + If this was a mistake, you can email admin@pypi.org to communicate with the PyPI administrators. +{% endblock %} + +{% block reason %} + You are receiving this because you are {{ recipient_role_descr }} of this project. +{% 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..ff5e46475706 --- /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 %}File deleted for {{ project_name }} {{ release_version }}{% endblock %}