diff --git a/tests/unit/email/test_init.py b/tests/unit/email/test_init.py
index cbe230b597a4..759fbde61243 100644
--- a/tests/unit/email/test_init.py
+++ b/tests/unit/email/test_init.py
@@ -10,6 +10,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import datetime
+
import attr
import celery.exceptions
import pretend
@@ -1092,6 +1094,402 @@ def test_added_as_collaborator_email_unverified(
assert send_email.delay.calls == []
+class TestRemovedPackageEmail:
+ def test_removed_project_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/subject.txt"
+ )
+ subject_renderer.string_response = "Email Subject"
+ body_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project/body.txt"
+ )
+ body_renderer.string_response = "Email Body"
+ html_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project/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)
+
+ result = email.send_removed_project_email(
+ pyramid_request,
+ [stub_user, stub_submitter_user],
+ project_name="test_project",
+ submitter_name=stub_submitter_user.username,
+ submitter_role="Owner",
+ recipient_role="Maintainer",
+ )
+
+ assert result == {
+ "project": "test_project",
+ "submitter": stub_submitter_user.username,
+ "submitter_role": "owner",
+ "recipient_role_descr": "a maintainer",
+ }
+
+ subject_renderer.assert_(project="test_project")
+ body_renderer.assert_(project="test_project")
+ body_renderer.assert_(submitter=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"
+ ),
+ )
+ ),
+ ),
+ ]
+
+ def test_removed_project_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/subject.txt"
+ )
+ subject_renderer.string_response = "Email Subject"
+ body_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project/body.txt"
+ )
+ body_renderer.string_response = "Email Body"
+ html_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project/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)
+
+ result = email.send_removed_project_email(
+ pyramid_request,
+ [stub_user, stub_submitter_user],
+ project_name="test_project",
+ submitter_name=stub_submitter_user.username,
+ submitter_role="Owner",
+ recipient_role="Owner",
+ )
+
+ assert result == {
+ "project": "test_project",
+ "submitter": stub_submitter_user.username,
+ "submitter_role": "owner",
+ "recipient_role_descr": "an owner",
+ }
+
+ subject_renderer.assert_(project="test_project")
+ body_renderer.assert_(project="test_project")
+ body_renderer.assert_(submitter=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"
+ ),
+ )
+ ),
+ ),
+ ]
+
+
+class TestRemovedReleaseEmail:
+ def test_send_removed_project_release_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/subject.txt"
+ )
+ subject_renderer.string_response = "Email Subject"
+ body_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project-release/body.txt"
+ )
+ body_renderer.string_response = "Email Body"
+ html_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project-release/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_email(
+ pyramid_request,
+ [stub_user, stub_submitter_user],
+ release=release,
+ submitter_name=stub_submitter_user.username,
+ submitter_role="Owner",
+ recipient_role="Maintainer",
+ )
+
+ assert result == {
+ "project": release.project.name,
+ "release": release.version,
+ "release_date": release.created.strftime("%Y-%m-%d"),
+ "submitter": stub_submitter_user.username,
+ "submitter_role": "owner",
+ "recipient_role_descr": "a maintainer",
+ }
+
+ subject_renderer.assert_(project="test_project")
+ subject_renderer.assert_(release="0.0.0")
+ body_renderer.assert_(project="test_project")
+ body_renderer.assert_(release="0.0.0")
+ body_renderer.assert_(release_date=release.created.strftime("%Y-%m-%d"))
+ body_renderer.assert_(submitter=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"
+ ),
+ )
+ ),
+ ),
+ ]
+
+ def test_send_removed_project_release_emai_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/subject.txt"
+ )
+ subject_renderer.string_response = "Email Subject"
+ body_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project-release/body.txt"
+ )
+ body_renderer.string_response = "Email Body"
+ html_renderer = pyramid_config.testing_add_renderer(
+ "email/removed-project-release/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_email(
+ pyramid_request,
+ [stub_user, stub_submitter_user],
+ release=release,
+ submitter_name=stub_submitter_user.username,
+ submitter_role="Owner",
+ recipient_role="Owner",
+ )
+
+ assert result == {
+ "project": release.project.name,
+ "release": release.version,
+ "release_date": release.created.strftime("%Y-%m-%d"),
+ "submitter": stub_submitter_user.username,
+ "submitter_role": "owner",
+ "recipient_role_descr": "an owner",
+ }
+
+ subject_renderer.assert_(project="test_project")
+ subject_renderer.assert_(release="0.0.0")
+ body_renderer.assert_(project="test_project")
+ body_renderer.assert_(release="0.0.0")
+ body_renderer.assert_(release_date=release.created.strftime("%Y-%m-%d"))
+ body_renderer.assert_(submitter=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"
+ ),
+ )
+ ),
+ ),
+ ]
+
+
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 b3b04869931c..870d650f2f95 100644
--- a/tests/unit/manage/test_views.py
+++ b/tests/unit/manage/test_views.py
@@ -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")
@@ -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)
@@ -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())
@@ -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},
@@ -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)
@@ -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 == [
diff --git a/warehouse/email/__init__.py b/warehouse/email/__init__.py
index 8b6b170498b7..9ff6b2118b9a 100644
--- a/warehouse/email/__init__.py
+++ b/warehouse/email/__init__.py
@@ -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)
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index d200685160da..f6a609521a49 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: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 ""
@@ -1255,6 +1255,48 @@ msgid ""
"%(new_email)s
"
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 ""
+"Deleted by: %(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 %(email_address)s 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 ""
diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py
index a8f807243961..a37ee2c1e5f4 100644
--- a/warehouse/manage/views.py
+++ b/warehouse/manage/views.py
@@ -38,6 +38,8 @@
send_email_verification_email,
send_password_change_email,
send_primary_email_change_email,
+ send_removed_project_email,
+ send_removed_project_release_email,
send_two_factor_added_email,
send_two_factor_removed_email,
)
@@ -899,6 +901,46 @@ def manage_project_settings(project, request):
return {"project": project}
+def get_project_contributors(project_name, request):
+ query_res = (
+ request.db.query(Project)
+ .join(User, Project.users)
+ .filter(Project.name == project_name)
+ .one()
+ )
+ return query_res.users
+
+
+def get_user_role_in_project(project_name, username, request):
+ raw_res = (
+ request.db.query(Project)
+ .join(User, Project.users)
+ .filter(User.username == username, Project.name == project_name)
+ .with_entities(Role.role_name)
+ .distinct(Role.role_name)
+ .all()
+ )
+
+ query_res = []
+ for el in raw_res:
+ if el.role_name is not None:
+ query_res.append(el)
+
+ user_role = ""
+ # This check is needed because of
+ # issue https://github.com/pypa/warehouse/issues/2745
+ # which is not yet resolved and a user could be an owner
+ # and a maintainer at the same time
+ if len(query_res) == 2 and (
+ query_res[0].role_name == "Owner" or query_res[1].role_name == "Owner"
+ ):
+ user_role = "Owner"
+ if len(query_res) == 1:
+ user_role = query_res[0].role_name
+
+ return user_role
+
+
@view_config(
route_name="manage.project.delete_project",
context=Project,
@@ -921,6 +963,26 @@ def delete_project(project, request):
)
confirm_project(project, request, fail_route="manage.project.settings")
+
+ submitter_role = get_user_role_in_project(
+ project.name, request.user.username, request
+ )
+ contributors = get_project_contributors(project.name, request)
+
+ for contributor in contributors:
+ contributor_role = get_user_role_in_project(
+ project.name, contributor.username, request
+ )
+
+ send_removed_project_email(
+ request,
+ contributor,
+ project_name=project.name,
+ submitter_name=request.user.username,
+ submitter_role=submitter_role,
+ recipient_role=contributor_role,
+ )
+
remove_project(project, request)
return HTTPSeeOther(request.route_path("manage.projects"))
@@ -1053,6 +1115,11 @@ def delete_project_release(self):
)
)
+ submitter_role = get_user_role_in_project(
+ self.release.project.name, self.request.user.username, self.request
+ )
+ contributors = get_project_contributors(self.release.project.name, self.request)
+
self.request.db.add(
JournalEntry(
name=self.release.project.name,
@@ -1078,6 +1145,20 @@ def delete_project_release(self):
f"Deleted release {self.release.version!r}", queue="success"
)
+ for contributor in contributors:
+ contributor_role = get_user_role_in_project(
+ self.release.project.name, contributor.username, self.request
+ )
+
+ send_removed_project_release_email(
+ self.request,
+ contributor,
+ release=self.release,
+ submitter_name=self.request.user.username,
+ submitter_role=submitter_role,
+ recipient_role=contributor_role,
+ )
+
return HTTPSeeOther(
self.request.route_path(
"manage.project.releases", project_name=self.release.project.name
diff --git a/warehouse/templates/email/removed-project-release/body.html b/warehouse/templates/email/removed-project-release/body.html
new file mode 100644
index 000000000000..476fcdfc014e
--- /dev/null
+++ b/warehouse/templates/email/removed-project-release/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 project=project, release=release, date=release_date %}The {{ project }} release {{ release }} released on {{ date }} has been deleted.{% endtrans %}
+ - {% trans submitter=submitter, role=submitter_role %}Deleted by: {{ submitter }} 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/body.txt b/warehouse/templates/email/removed-project-release/body.txt
new file mode 100644
index 000000000000..364d2c4c9555
--- /dev/null
+++ b/warehouse/templates/email/removed-project-release/body.txt
@@ -0,0 +1,29 @@
+{#
+ # 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 project=project, release=release, date=release_date %}The {{ project }} release {{ release }} released on {{ date }} has been deleted.{% endtrans %}
+
+ {% trans submitter=submitter, role=submitter_role %}Deleted by: {{ submitter }} 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/subject.txt b/warehouse/templates/email/removed-project-release/subject.txt
new file mode 100644
index 000000000000..1fc56452dd25
--- /dev/null
+++ b/warehouse/templates/email/removed-project-release/subject.txt
@@ -0,0 +1,18 @@
+{#
+ # 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 content %}
+{% trans project=project, release=release %}The {{ project }} release {{ release }} has been deleted.{% endtrans %}{% endblock %}
diff --git a/warehouse/templates/email/removed-project/body.html b/warehouse/templates/email/removed-project/body.html
new file mode 100644
index 000000000000..278f0554f5e3
--- /dev/null
+++ b/warehouse/templates/email/removed-project/body.html
@@ -0,0 +1,39 @@
+{#
+ # 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 project=project %}The project {{ project }} has been deleted.{% endtrans %}
+ - {% trans submitter=submitter, role=submitter_role %}Deleted by: {{ submitter }} 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/body.txt b/warehouse/templates/email/removed-project/body.txt
new file mode 100644
index 000000000000..8a02f386eaff
--- /dev/null
+++ b/warehouse/templates/email/removed-project/body.txt
@@ -0,0 +1,29 @@
+{#
+ # 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 project=project %}The project {{ project }} has been deleted.{% endtrans %}
+
+ {% trans submitter=submitter, role=submitter_role %}Deleted by: {{ submitter }} 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/subject.txt b/warehouse/templates/email/removed-project/subject.txt
new file mode 100644
index 000000000000..3738aa2c4f78
--- /dev/null
+++ b/warehouse/templates/email/removed-project/subject.txt
@@ -0,0 +1,19 @@
+{#
+ # 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 content %}
+ {% trans project=project %}The project {{ project }} has been deleted.{% endtrans %}
+{% endblock %}