From bf56adba9261cfd686d42b0ea93caf7bbbb26a72 Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Wed, 30 Oct 2024 19:22:12 +0100
Subject: [PATCH 1/9] feat: allow marking projects as archived
Signed-off-by: Facundo Tuesca
---
tests/unit/manage/test_views.py | 100 +++++-
tests/unit/test_routes.py | 14 +
warehouse/events/tags.py | 2 +
warehouse/locale/messages.pot | 322 ++++++++++--------
warehouse/manage/views/__init__.py | 68 ++++
...12a43f12cc18_add_new_lifecycle_statuses.py | 55 +++
warehouse/packaging/models.py | 1 +
warehouse/routes.py | 14 +
.../templates/manage/project/settings.html | 35 ++
warehouse/templates/packaging/detail.html | 10 +
10 files changed, 478 insertions(+), 143 deletions(-)
create mode 100644 warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py
diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py
index 07b6c9f0d969..384d7ddc8509 100644
--- a/tests/unit/manage/test_views.py
+++ b/tests/unit/manage/test_views.py
@@ -63,6 +63,7 @@
from warehouse.packaging.models import (
File,
JournalEntry,
+ LifecycleStatus,
Project,
Release,
Role,
@@ -2602,7 +2603,7 @@ class TestManageProjectSettings:
@pytest.mark.parametrize("enabled", [False, True])
def test_manage_project_settings(self, enabled, monkeypatch):
request = pretend.stub(organization_access=enabled)
- project = pretend.stub(organization=None)
+ project = pretend.stub(organization=None, lifecycle_status=None)
view = views.ManageProjectSettingsViews(project, request)
form = pretend.stub()
view.transfer_organization_project_form_class = lambda *a, **kw: form
@@ -2629,7 +2630,7 @@ def test_manage_project_settings_in_organization_managed(self, monkeypatch):
request = pretend.stub(organization_access=True)
organization_managed = pretend.stub(name="managed-org", is_active=True)
organization_owned = pretend.stub(name="owned-org", is_active=True)
- project = pretend.stub(organization=organization_managed)
+ project = pretend.stub(organization=organization_managed, lifecycle_status=None)
view = views.ManageProjectSettingsViews(project, request)
form = pretend.stub()
view.transfer_organization_project_form_class = pretend.call_recorder(
@@ -2661,7 +2662,7 @@ def test_manage_project_settings_in_organization_owned(self, monkeypatch):
request = pretend.stub(organization_access=True)
organization_managed = pretend.stub(name="managed-org", is_active=True)
organization_owned = pretend.stub(name="owned-org", is_active=True)
- project = pretend.stub(organization=organization_owned)
+ project = pretend.stub(organization=organization_owned, lifecycle_status=None)
view = views.ManageProjectSettingsViews(project, request)
form = pretend.stub()
view.transfer_organization_project_form_class = pretend.call_recorder(
@@ -7955,3 +7956,96 @@ def test_delete_oidc_publisher_admin_disabled(self, monkeypatch):
queue="error",
)
]
+
+
+class TestArchiveProject:
+ def test_archive(self, db_request):
+ project = ProjectFactory.create(name="foo")
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.archive_project(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert project.lifecycle_status == LifecycleStatus.Archived
+ assert db_request.route_path.calls == [
+ pretend.call("manage.project.settings", project_name=project.name)
+ ]
+
+ def test_unarchive_project(self, db_request):
+ project = ProjectFactory.create(
+ name="foo", lifecycle_status=LifecycleStatus.Archived
+ )
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.unarchive_project(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.route_path.calls == [
+ pretend.call("manage.project.settings", project_name=project.name)
+ ]
+ assert project.lifecycle_status is None
+
+ def test_disallowed_archive(self, db_request):
+ project = ProjectFactory.create(name="foo", lifecycle_status="quarantine-enter")
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.archive_project(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call(
+ f"Cannot archive project with status {project.lifecycle_status}",
+ queue="error",
+ )
+ ]
+ assert db_request.route_path.calls == [
+ pretend.call("manage.project.settings", project_name="foo")
+ ]
+ assert project.lifecycle_status == "quarantine-enter"
+
+ def test_disallowed_unarchive(self, db_request):
+ project = ProjectFactory.create(name="foo", lifecycle_status="quarantine-enter")
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.unarchive_project(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call("Can only unarchive an archived project", queue="error")
+ ]
+ assert db_request.route_path.calls == [
+ pretend.call("manage.project.settings", project_name="foo")
+ ]
+ assert project.lifecycle_status == "quarantine-enter"
diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py
index 0de9e3f61694..5377dc445eed 100644
--- a/tests/unit/test_routes.py
+++ b/tests/unit/test_routes.py
@@ -493,6 +493,20 @@ def add_redirect_rule(*args, **kwargs):
traverse="/{project_name}",
domain=warehouse,
),
+ pretend.call(
+ "manage.project.archive",
+ "/manage/project/{project_name}/archive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ ),
+ pretend.call(
+ "manage.project.unarchive",
+ "/manage/project/{project_name}/unarchive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ ),
pretend.call(
"manage.project.history",
"/manage/project/{project_name}/history/",
diff --git a/warehouse/events/tags.py b/warehouse/events/tags.py
index 61e3161c8e37..22eaaaec92f9 100644
--- a/warehouse/events/tags.py
+++ b/warehouse/events/tags.py
@@ -124,6 +124,8 @@ class Project(EventTagEnum):
OrganizationProjectRemove = "project:organization_project:remove"
OwnersRequire2FADisabled = "project:owners_require_2fa:disabled"
OwnersRequire2FAEnabled = "project:owners_require_2fa:enabled"
+ ProjectArchiveEnter = "project:archive:enter"
+ ProjectArchiveExit = "project:archive:exit"
ProjectCreate = "project:create"
ProjectQuarantineEnter = "project:quarantine:enter"
ProjectQuarantineExit = "project:quarantine:exit"
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index f8f88a3c4257..f9407e6874b0 100644
--- a/warehouse/locale/messages.pot
+++ b/warehouse/locale/messages.pot
@@ -152,7 +152,7 @@ msgstr ""
msgid "Successful WebAuthn assertion"
msgstr ""
-#: warehouse/accounts/views.py:609 warehouse/manage/views/__init__.py:871
+#: warehouse/accounts/views.py:609 warehouse/manage/views/__init__.py:872
msgid "Recovery code accepted. The supplied code cannot be used again."
msgstr ""
@@ -286,7 +286,7 @@ msgid "You are now ${role} of the '${project_name}' project."
msgstr ""
#: warehouse/accounts/views.py:1588 warehouse/accounts/views.py:1831
-#: warehouse/manage/views/__init__.py:1407
+#: warehouse/manage/views/__init__.py:1408
msgid ""
"Trusted publishing is temporarily disabled. See https://pypi.org/help"
"#admin-intervention for details."
@@ -306,19 +306,19 @@ msgstr ""
msgid "You can't register more than 3 pending trusted publishers at once."
msgstr ""
-#: warehouse/accounts/views.py:1654 warehouse/manage/views/__init__.py:1576
-#: warehouse/manage/views/__init__.py:1689
-#: warehouse/manage/views/__init__.py:1801
-#: warehouse/manage/views/__init__.py:1911
+#: warehouse/accounts/views.py:1654 warehouse/manage/views/__init__.py:1577
+#: warehouse/manage/views/__init__.py:1690
+#: warehouse/manage/views/__init__.py:1802
+#: warehouse/manage/views/__init__.py:1912
msgid ""
"There have been too many attempted trusted publisher registrations. Try "
"again later."
msgstr ""
-#: warehouse/accounts/views.py:1665 warehouse/manage/views/__init__.py:1590
-#: warehouse/manage/views/__init__.py:1703
-#: warehouse/manage/views/__init__.py:1815
-#: warehouse/manage/views/__init__.py:1925
+#: warehouse/accounts/views.py:1665 warehouse/manage/views/__init__.py:1591
+#: warehouse/manage/views/__init__.py:1704
+#: warehouse/manage/views/__init__.py:1816
+#: warehouse/manage/views/__init__.py:1926
msgid "The trusted publisher could not be registered"
msgstr ""
@@ -446,165 +446,169 @@ msgid ""
"less."
msgstr ""
-#: warehouse/manage/views/__init__.py:283
+#: warehouse/manage/views/__init__.py:284
msgid "Account details updated"
msgstr ""
-#: warehouse/manage/views/__init__.py:313
+#: warehouse/manage/views/__init__.py:314
msgid "Email ${email_address} added - check your email for a verification link"
msgstr ""
-#: warehouse/manage/views/__init__.py:819
+#: warehouse/manage/views/__init__.py:820
msgid "Recovery codes already generated"
msgstr ""
-#: warehouse/manage/views/__init__.py:820
+#: warehouse/manage/views/__init__.py:821
msgid "Generating new recovery codes will invalidate your existing codes."
msgstr ""
-#: warehouse/manage/views/__init__.py:929
+#: warehouse/manage/views/__init__.py:930
msgid "Verify your email to create an API token."
msgstr ""
-#: warehouse/manage/views/__init__.py:1029
+#: warehouse/manage/views/__init__.py:1030
msgid "API Token does not exist."
msgstr ""
-#: warehouse/manage/views/__init__.py:1061
+#: warehouse/manage/views/__init__.py:1062
msgid "Invalid credentials. Try again"
msgstr ""
-#: warehouse/manage/views/__init__.py:1180
+#: warehouse/manage/views/__init__.py:1181
msgid "Invalid alternate repository location details"
msgstr ""
-#: warehouse/manage/views/__init__.py:1217
+#: warehouse/manage/views/__init__.py:1218
msgid "Added alternate repository '${name}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:1251
-#: warehouse/manage/views/__init__.py:2258
-#: warehouse/manage/views/__init__.py:2343
-#: warehouse/manage/views/__init__.py:2444
-#: warehouse/manage/views/__init__.py:2544
+#: warehouse/manage/views/__init__.py:1252
+#: warehouse/manage/views/__init__.py:2259
+#: warehouse/manage/views/__init__.py:2344
+#: warehouse/manage/views/__init__.py:2445
+#: warehouse/manage/views/__init__.py:2545
msgid "Confirm the request"
msgstr ""
-#: warehouse/manage/views/__init__.py:1263
+#: warehouse/manage/views/__init__.py:1264
msgid "Invalid alternate repository id"
msgstr ""
-#: warehouse/manage/views/__init__.py:1274
+#: warehouse/manage/views/__init__.py:1275
msgid "Invalid alternate repository for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:1282
+#: warehouse/manage/views/__init__.py:1283
msgid ""
"Could not delete alternate repository - ${confirm} is not the same as "
"${alt_repo_name}"
msgstr ""
-#: warehouse/manage/views/__init__.py:1312
+#: warehouse/manage/views/__init__.py:1313
msgid "Deleted alternate repository '${name}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:1457
+#: warehouse/manage/views/__init__.py:1458
msgid "The trusted publisher could not be constrained"
msgstr ""
-#: warehouse/manage/views/__init__.py:1557
+#: warehouse/manage/views/__init__.py:1558
msgid ""
"GitHub-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1670
+#: warehouse/manage/views/__init__.py:1671
msgid ""
"GitLab-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1782
+#: warehouse/manage/views/__init__.py:1783
msgid ""
"Google-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1891
+#: warehouse/manage/views/__init__.py:1892
msgid ""
"ActiveState-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:2126
-#: warehouse/manage/views/__init__.py:2427
-#: warehouse/manage/views/__init__.py:2535
+#: warehouse/manage/views/__init__.py:2127
+#: warehouse/manage/views/__init__.py:2428
+#: warehouse/manage/views/__init__.py:2536
msgid ""
"Project deletion temporarily disabled. See https://pypi.org/help#admin-"
"intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:2270
+#: warehouse/manage/views/__init__.py:2271
msgid "Could not yank release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2355
+#: warehouse/manage/views/__init__.py:2356
msgid "Could not un-yank release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2456
+#: warehouse/manage/views/__init__.py:2457
msgid "Could not delete release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2556
+#: warehouse/manage/views/__init__.py:2557
msgid "Could not find file"
msgstr ""
-#: warehouse/manage/views/__init__.py:2560
+#: warehouse/manage/views/__init__.py:2561
msgid "Could not delete file - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2710
+#: warehouse/manage/views/__init__.py:2711
msgid "Team '${team_name}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2817
+#: warehouse/manage/views/__init__.py:2818
msgid "User '${username}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2884
+#: warehouse/manage/views/__init__.py:2885
msgid "${username} is now ${role} of the '${project_name}' project."
msgstr ""
-#: warehouse/manage/views/__init__.py:2916
+#: warehouse/manage/views/__init__.py:2917
msgid ""
"User '${username}' does not have a verified primary email address and "
"cannot be added as a ${role_name} for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2929
+#: warehouse/manage/views/__init__.py:2930
#: warehouse/manage/views/organizations.py:889
msgid "User '${username}' already has an active invite. Please try again later."
msgstr ""
-#: warehouse/manage/views/__init__.py:2994
+#: warehouse/manage/views/__init__.py:2995
#: warehouse/manage/views/organizations.py:954
msgid "Invitation sent to '${username}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:3027
+#: warehouse/manage/views/__init__.py:3028
msgid "Could not find role invitation."
msgstr ""
-#: warehouse/manage/views/__init__.py:3038
+#: warehouse/manage/views/__init__.py:3039
msgid "Invitation already expired."
msgstr ""
-#: warehouse/manage/views/__init__.py:3070
+#: warehouse/manage/views/__init__.py:3071
#: warehouse/manage/views/organizations.py:1141
msgid "Invitation revoked from '${username}'."
msgstr ""
+#: warehouse/manage/views/__init__.py:3356
+msgid "Can only unarchive an archived project"
+msgstr ""
+
#: warehouse/manage/views/organizations.py:865
msgid "User '${username}' already has ${role_name} role for organization"
msgstr ""
@@ -914,8 +918,8 @@ msgstr ""
#: warehouse/templates/manage/project/release.html:194
#: warehouse/templates/manage/project/releases.html:140
#: warehouse/templates/manage/project/releases.html:179
-#: warehouse/templates/packaging/detail.html:407
-#: warehouse/templates/packaging/detail.html:427
+#: warehouse/templates/packaging/detail.html:417
+#: warehouse/templates/packaging/detail.html:437
#: warehouse/templates/pages/classifiers.html:25
#: warehouse/templates/pages/help.html:20
#: warehouse/templates/pages/help.html:228
@@ -1148,9 +1152,9 @@ msgstr ""
#: warehouse/templates/manage/organization/settings.html:286
#: warehouse/templates/manage/project/documentation.html:27
#: warehouse/templates/manage/project/release.html:182
-#: warehouse/templates/manage/project/settings.html:87
-#: warehouse/templates/manage/project/settings.html:136
-#: warehouse/templates/manage/project/settings.html:357
+#: warehouse/templates/manage/project/settings.html:122
+#: warehouse/templates/manage/project/settings.html:171
+#: warehouse/templates/manage/project/settings.html:392
#: warehouse/templates/manage/team/settings.html:84
msgid "Warning"
msgstr ""
@@ -1517,9 +1521,9 @@ msgstr ""
#: warehouse/templates/manage/project/roles.html:328
#: warehouse/templates/manage/project/roles.html:359
#: warehouse/templates/manage/project/roles.html:380
-#: warehouse/templates/manage/project/settings.html:287
-#: warehouse/templates/manage/project/settings.html:307
-#: warehouse/templates/manage/project/settings.html:327
+#: warehouse/templates/manage/project/settings.html:322
+#: warehouse/templates/manage/project/settings.html:342
+#: warehouse/templates/manage/project/settings.html:362
#: warehouse/templates/manage/team/roles.html:106
#: warehouse/templates/manage/team/settings.html:35
#: warehouse/templates/packaging/submit-malware-observation.html:58
@@ -1813,9 +1817,9 @@ msgstr ""
#: warehouse/templates/manage/project/history.html:312
#: warehouse/templates/manage/project/history.html:323
#: warehouse/templates/manage/project/history.html:334
-#: warehouse/templates/manage/project/settings.html:224
-#: warehouse/templates/manage/project/settings.html:285
-#: warehouse/templates/manage/project/settings.html:291
+#: warehouse/templates/manage/project/settings.html:259
+#: warehouse/templates/manage/project/settings.html:320
+#: warehouse/templates/manage/project/settings.html:326
#: warehouse/templates/manage/unverified-account.html:112
msgid "Name"
msgstr ""
@@ -2738,7 +2742,7 @@ msgstr ""
#: warehouse/templates/manage/manage_base.html:331
#: warehouse/templates/manage/project/release.html:137
#: warehouse/templates/manage/project/releases.html:178
-#: warehouse/templates/manage/project/settings.html:72
+#: warehouse/templates/manage/project/settings.html:107
#: warehouse/templates/manage/unverified-account.html:172
#: warehouse/templates/manage/unverified-account.html:174
#: warehouse/templates/manage/unverified-account.html:184
@@ -3395,12 +3399,12 @@ msgid "Update password"
msgstr ""
#: warehouse/templates/manage/account.html:472
-#: warehouse/templates/manage/project/settings.html:43
+#: warehouse/templates/manage/project/settings.html:78
msgid "API tokens"
msgstr ""
#: warehouse/templates/manage/account.html:473
-#: warehouse/templates/manage/project/settings.html:44
+#: warehouse/templates/manage/project/settings.html:79
msgid ""
"API tokens provide an alternative way to authenticate when uploading "
"packages to PyPI."
@@ -4607,7 +4611,7 @@ msgstr ""
#: warehouse/templates/manage/project/publishing.html:275
#: warehouse/templates/manage/project/publishing.html:357
#: warehouse/templates/manage/project/roles.html:341
-#: warehouse/templates/manage/project/settings.html:348
+#: warehouse/templates/manage/project/settings.html:383
#: warehouse/templates/manage/team/roles.html:131
msgid "Add"
msgstr ""
@@ -5666,7 +5670,7 @@ msgstr ""
#: warehouse/templates/manage/organization/roles.html:252
#: warehouse/templates/manage/project/release.html:106
#: warehouse/templates/manage/project/releases.html:109
-#: warehouse/templates/manage/project/settings.html:252
+#: warehouse/templates/manage/project/settings.html:287
msgid "Delete"
msgstr ""
@@ -6063,9 +6067,9 @@ msgstr ""
#: warehouse/templates/manage/project/history.html:313
#: warehouse/templates/manage/project/history.html:324
#: warehouse/templates/manage/project/history.html:335
-#: warehouse/templates/manage/project/settings.html:225
-#: warehouse/templates/manage/project/settings.html:305
-#: warehouse/templates/manage/project/settings.html:311
+#: warehouse/templates/manage/project/settings.html:260
+#: warehouse/templates/manage/project/settings.html:340
+#: warehouse/templates/manage/project/settings.html:346
msgid "Url"
msgstr ""
@@ -6273,7 +6277,7 @@ msgstr ""
#: warehouse/templates/manage/project/release.html:137
#: warehouse/templates/manage/project/releases.html:178
-#: warehouse/templates/manage/project/settings.html:72
+#: warehouse/templates/manage/project/settings.html:107
msgid "Dismiss"
msgstr ""
@@ -6616,23 +6620,51 @@ msgstr ""
msgid " (request an increase) "
msgstr ""
+#: warehouse/templates/manage/project/settings.html:43
+#: warehouse/templates/manage/project/settings.html:57
+#: warehouse/templates/manage/project/settings.html:68
+msgid "Archive project"
+msgstr ""
+
#: warehouse/templates/manage/project/settings.html:48
+msgid ""
+"Archiving a project will block any new uploads. Before doing so, we "
+"recommend publishing a final release with an update to the project's "
+"README to warn the users that the project won't receive further updates, "
+"and to mention any alternative projects they may consider as a "
+"replacement."
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:60
+#: warehouse/templates/manage/project/settings.html:74
+msgid "Unarchive project"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:69
+msgid "Archiving a project will block any new file uploads"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:75
+msgid "Unarchiving a project will allow new file uploads"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:83
#, python-format
msgid "Create a token for %(project_name)s"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:53
+#: warehouse/templates/manage/project/settings.html:88
#, python-format
msgid ""
"Verify your primary email address to add an API "
"token for %(project_name)s."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:60
+#: warehouse/templates/manage/project/settings.html:95
msgid "Project description and sidebar"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:62
+#: warehouse/templates/manage/project/settings.html:97
#, python-format
msgid ""
"To set the '%(project_name)s' description, author, links, classifiers, "
@@ -6648,147 +6680,147 @@ msgid ""
"Python Packaging User Guide for more help."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:85
+#: warehouse/templates/manage/project/settings.html:120
msgid "Remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:88
+#: warehouse/templates/manage/project/settings.html:123
msgid "Removing this project from the organization will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:92
-#: warehouse/templates/manage/project/settings.html:142
+#: warehouse/templates/manage/project/settings.html:127
+#: warehouse/templates/manage/project/settings.html:177
#, python-format
msgid "Remove this project from the '%(organization_name)s' organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:95
-#: warehouse/templates/manage/project/settings.html:145
+#: warehouse/templates/manage/project/settings.html:130
+#: warehouse/templates/manage/project/settings.html:180
#, python-format
msgid ""
"Revoke project permissions for teams in the '%(organization_name)s' "
"organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:99
-#: warehouse/templates/manage/project/settings.html:105
+#: warehouse/templates/manage/project/settings.html:134
+#: warehouse/templates/manage/project/settings.html:140
msgid ""
"Individual owners and maintainers of the project will retain their "
"project permissions."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:104
+#: warehouse/templates/manage/project/settings.html:139
#, python-format
msgid ""
"This will remove the project from the '%(organization_name)s' "
"organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:108
+#: warehouse/templates/manage/project/settings.html:143
msgid "Remove project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:108
-#: warehouse/templates/manage/project/settings.html:179
-#: warehouse/templates/manage/project/settings.html:395
+#: warehouse/templates/manage/project/settings.html:143
+#: warehouse/templates/manage/project/settings.html:214
+#: warehouse/templates/manage/project/settings.html:430
msgid "Project Name"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:112
+#: warehouse/templates/manage/project/settings.html:147
msgid "Cannot remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:114
+#: warehouse/templates/manage/project/settings.html:149
msgid ""
"Your organization is currently the sole owner of the "
"project. You must add an individual owner to the project before you can "
"remove the project from your organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:130
+#: warehouse/templates/manage/project/settings.html:165
msgid "Transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:132
+#: warehouse/templates/manage/project/settings.html:167
msgid "Transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:137
+#: warehouse/templates/manage/project/settings.html:172
msgid "Transferring this project will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:149
+#: warehouse/templates/manage/project/settings.html:184
msgid "Revoke your direct Owner role on the project."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:152
+#: warehouse/templates/manage/project/settings.html:187
msgid ""
"You will retain Owner permissions on the project through your "
"organization role."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:157
+#: warehouse/templates/manage/project/settings.html:192
msgid "Add the project to another organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:159
+#: warehouse/templates/manage/project/settings.html:194
msgid "Add the project to an organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:163
+#: warehouse/templates/manage/project/settings.html:198
msgid "Grant full project permissions to owners of the organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:167
+#: warehouse/templates/manage/project/settings.html:202
msgid ""
"All other individual owners and maintainers of the project will retain "
"their project permissions."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:179
+#: warehouse/templates/manage/project/settings.html:214
msgid "Transfer project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:185
+#: warehouse/templates/manage/project/settings.html:220
msgid "Cannot transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:187
+#: warehouse/templates/manage/project/settings.html:222
msgid "Cannot transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:192
+#: warehouse/templates/manage/project/settings.html:227
msgid ""
"Organization owners can transfer the project to organizations that they "
"own or manage."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:193
+#: warehouse/templates/manage/project/settings.html:228
msgid "You are not an owner or manager of any other organizations."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:195
+#: warehouse/templates/manage/project/settings.html:230
msgid ""
"Project owners can transfer the project to organizations that they own or"
" manage."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:196
+#: warehouse/templates/manage/project/settings.html:231
msgid "You are not an owner or manager of any organizations."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:205
+#: warehouse/templates/manage/project/settings.html:240
msgid "Alternate repository locations"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:209
+#: warehouse/templates/manage/project/settings.html:244
#, python-format
msgid ""
"Provisional support for PEP 708 \"Alternate "
"Locations\" Metadata."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:213
+#: warehouse/templates/manage/project/settings.html:248
#, python-format
msgid ""
"Implementation may change, consider subscribing to %(count)s"
@@ -6855,15 +6887,15 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: warehouse/templates/manage/project/settings.html:369
+#: warehouse/templates/manage/project/settings.html:404
msgid "Irreversibly delete the project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:373
+#: warehouse/templates/manage/project/settings.html:408
msgid "Make the project name available to any other PyPI user"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:375
+#: warehouse/templates/manage/project/settings.html:410
msgid ""
"This user will be able to make new releases under this project name, so "
"long as the distribution filenames do not match filenames from a "
@@ -7053,7 +7085,7 @@ msgstr ""
#: warehouse/templates/packaging/detail.html:238
#: warehouse/templates/packaging/detail.html:272
-#: warehouse/templates/packaging/detail.html:322
+#: warehouse/templates/packaging/detail.html:332
msgid "Project description"
msgstr ""
@@ -7064,7 +7096,7 @@ msgstr ""
#: warehouse/templates/packaging/detail.html:244
#: warehouse/templates/packaging/detail.html:284
-#: warehouse/templates/packaging/detail.html:344
+#: warehouse/templates/packaging/detail.html:354
msgid "Release history"
msgstr ""
@@ -7075,7 +7107,7 @@ msgstr ""
#: warehouse/templates/packaging/detail.html:251
#: warehouse/templates/packaging/detail.html:291
-#: warehouse/templates/packaging/detail.html:406
+#: warehouse/templates/packaging/detail.html:416
msgid "Download files"
msgstr ""
@@ -7084,40 +7116,50 @@ msgid "Project details. Focus will be moved to the project details."
msgstr ""
#: warehouse/templates/packaging/detail.html:278
-#: warehouse/templates/packaging/detail.html:336
+#: warehouse/templates/packaging/detail.html:346
msgid "Project details"
msgstr ""
#: warehouse/templates/packaging/detail.html:318
-#: warehouse/templates/packaging/detail.html:393
+msgid "This project has been archived."
+msgstr ""
+
+#: warehouse/templates/packaging/detail.html:320
+msgid ""
+"The maintainers of this project have marked this project as archived. No "
+"new releases are expected."
+msgstr ""
+
+#: warehouse/templates/packaging/detail.html:328
+#: warehouse/templates/packaging/detail.html:403
msgid "Reason this release was yanked:"
msgstr ""
-#: warehouse/templates/packaging/detail.html:329
+#: warehouse/templates/packaging/detail.html:339
msgid "The author of this package has not provided a project description"
msgstr ""
-#: warehouse/templates/packaging/detail.html:346
+#: warehouse/templates/packaging/detail.html:356
msgid "Release notifications"
msgstr ""
-#: warehouse/templates/packaging/detail.html:347
+#: warehouse/templates/packaging/detail.html:357
msgid "RSS feed"
msgstr ""
-#: warehouse/templates/packaging/detail.html:359
+#: warehouse/templates/packaging/detail.html:369
msgid "This version"
msgstr ""
-#: warehouse/templates/packaging/detail.html:379
+#: warehouse/templates/packaging/detail.html:389
msgid "pre-release"
msgstr ""
-#: warehouse/templates/packaging/detail.html:384
+#: warehouse/templates/packaging/detail.html:394
msgid "yanked"
msgstr ""
-#: warehouse/templates/packaging/detail.html:407
+#: warehouse/templates/packaging/detail.html:417
#, python-format
msgid ""
"Download the file for your platform. If you're not sure which to choose, "
@@ -7125,24 +7167,24 @@ msgid ""
"target=\"_blank\" rel=\"noopener\">installing packages ."
msgstr ""
-#: warehouse/templates/packaging/detail.html:410
+#: warehouse/templates/packaging/detail.html:420
msgid "Source Distribution"
msgid_plural "Source Distributions"
msgstr[0] ""
msgstr[1] ""
-#: warehouse/templates/packaging/detail.html:426
+#: warehouse/templates/packaging/detail.html:436
msgid "No source distribution files available for this release."
msgstr ""
-#: warehouse/templates/packaging/detail.html:427
+#: warehouse/templates/packaging/detail.html:437
#, python-format
msgid ""
"See tutorial on generating distribution archives ."
msgstr ""
-#: warehouse/templates/packaging/detail.html:434
+#: warehouse/templates/packaging/detail.html:444
msgid "Built Distribution"
msgid_plural "Built Distributions"
msgstr[0] ""
diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py
index 0023fc0d785f..ffd8d3aa0560 100644
--- a/warehouse/manage/views/__init__.py
+++ b/warehouse/manage/views/__init__.py
@@ -132,6 +132,7 @@
AlternateRepository,
File,
JournalEntry,
+ LifecycleStatus,
Project,
Release,
Role,
@@ -3292,3 +3293,70 @@ def manage_project_history(project, request):
)
def manage_project_documentation(project, request):
return {"project": project}
+
+
+@view_config(
+ route_name="manage.project.archive",
+ context=Project,
+ uses_session=True,
+ require_methods=["POST"],
+ permission=Permissions.ProjectsWrite,
+ has_translations=True,
+)
+def archive_project(project, request) -> HTTPSeeOther:
+ """
+ Archive a Project. Reversible action.
+ """
+ if (
+ project.lifecycle_status is None
+ or project.lifecycle_status == LifecycleStatus.QuarantineExit
+ ):
+ project.lifecycle_status = LifecycleStatus.Archived
+ project.record_event(
+ tag=EventTag.Project.ProjectArchiveEnter,
+ request=request,
+ additional={
+ "submitted_by": request.user.username,
+ },
+ )
+ else:
+ request.session.flash(
+ request._(f"Cannot archive project with status {project.lifecycle_status}"),
+ queue="error",
+ )
+
+ return HTTPSeeOther(
+ request.route_path("manage.project.settings", project_name=project.name)
+ )
+
+
+@view_config(
+ route_name="manage.project.unarchive",
+ context=Project,
+ uses_session=True,
+ require_methods=["POST"],
+ permission=Permissions.ProjectsWrite,
+ has_translations=True,
+)
+def unarchive_project(project, request) -> HTTPSeeOther:
+ """
+ Unarchive a Project. Reversible action.
+ """
+ if project.lifecycle_status == LifecycleStatus.Archived:
+ project.lifecycle_status = None
+ project.record_event(
+ tag=EventTag.Project.ProjectArchiveExit,
+ request=request,
+ additional={
+ "submitted_by": request.user.username,
+ },
+ )
+ else:
+ request.session.flash(
+ request._("Can only unarchive an archived project"),
+ queue="error",
+ )
+
+ return HTTPSeeOther(
+ request.route_path("manage.project.settings", project_name=project.name)
+ )
diff --git a/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py b/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py
new file mode 100644
index 000000000000..3b622cfd7a26
--- /dev/null
+++ b/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py
@@ -0,0 +1,55 @@
+# 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.
+"""
+Add new lifecycle statuses
+
+Revision ID: 12a43f12cc18
+Revises: e24aa37164e72
+"""
+
+from alembic import op
+from alembic_postgresql_enum import TableReference
+
+revision = "12a43f12cc18"
+down_revision = "24aa37164e72"
+
+
+def upgrade():
+ op.sync_enum_values(
+ "public",
+ "lifecyclestatus",
+ ["quarantine-enter", "quarantine-exit", "archived"],
+ [
+ TableReference(
+ table_schema="public",
+ table_name="projects",
+ column_name="lifecycle_status",
+ )
+ ],
+ enum_values_to_rename=[],
+ )
+
+
+def downgrade():
+ op.sync_enum_values(
+ "public",
+ "lifecyclestatus",
+ ["quarantine-enter", "quarantine-exit"],
+ [
+ TableReference(
+ table_schema="public",
+ table_name="projects",
+ column_name="lifecycle_status",
+ )
+ ],
+ enum_values_to_rename=[],
+ )
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
index 0b7782741efe..ab318c5aa0d9 100644
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -166,6 +166,7 @@ def __contains__(self, project):
class LifecycleStatus(enum.StrEnum):
QuarantineEnter = "quarantine-enter"
QuarantineExit = "quarantine-exit"
+ Archived = "archived"
class Project(SitemapMixin, HasEvents, HasObservations, db.Model):
diff --git a/warehouse/routes.py b/warehouse/routes.py
index c31158dbab82..e8566619705c 100644
--- a/warehouse/routes.py
+++ b/warehouse/routes.py
@@ -493,6 +493,20 @@ def includeme(config):
traverse="/{project_name}",
domain=warehouse,
)
+ config.add_route(
+ "manage.project.archive",
+ "/manage/project/{project_name}/archive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ )
+ config.add_route(
+ "manage.project.unarchive",
+ "/manage/project/{project_name}/unarchive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ )
config.add_route(
"manage.project.history",
"/manage/project/{project_name}/history/",
diff --git a/warehouse/templates/manage/project/settings.html b/warehouse/templates/manage/project/settings.html
index 609b684067f6..f84717dad358 100644
--- a/warehouse/templates/manage/project/settings.html
+++ b/warehouse/templates/manage/project/settings.html
@@ -40,6 +40,41 @@ {% trans %}Project settings{% endtrans %}
(request an increase) {% endtrans %}
+ {% trans %}Archive project{% endtrans %}
+ {% set can_be_archived = not project.lifecycle_status or project.lifecycle_status == "quarantine-exit" %}
+ {% set can_be_unarchived = project.lifecycle_status == "archived" %}
+
+
+ {% trans %}
+ Archiving a project will block any new uploads. Before doing so, we
+ recommend publishing a final release with an update to the project's
+ README to warn the users that the project won't receive further updates,
+ and to mention any alternative projects they may consider as a replacement.
+ {% endtrans %}
+
+
+
+ {% trans %}Archive project{% endtrans %}
+
+
+ {% trans %}Unarchive project{% endtrans %}
+
+ {% if not project.lifecycle_status %}
+ {% elif project.lifecycle_status == "archived" %}
+ {% endif %}
+
+ {% set action = request.route_path('manage.project.archive', project_name=project.name) %}
+ {% set slug = "archive-project" %}
+ {% set title = gettext("Archive project") %}
+ {% set extra_description = gettext("Archiving a project will block any new file uploads") %}
+ {{ confirm_modal(title=title, label=project.name, slug=slug, extra_description=extra_description, action=action, warning=False) }}
+
+ {% set action = request.route_path('manage.project.unarchive', project_name=project.name) %}
+ {% set slug = "unarchive-project" %}
+ {% set title = gettext("Unarchive project") %}
+ {% set extra_description = gettext("Unarchiving a project will allow new file uploads") %}
+ {{ confirm_modal(title=title, label=project.name, slug=slug, extra_description=extra_description, action=action, warning=False) }}
+
{% trans %}API tokens{% endtrans %}
{% trans %}API tokens provide an alternative way to authenticate when uploading packages to PyPI.{% endtrans %}
{% if user.has_primary_verified_email %}
diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html
index fef56c293cda..a789e0897261 100644
--- a/warehouse/templates/packaging/detail.html
+++ b/warehouse/templates/packaging/detail.html
@@ -313,6 +313,16 @@
{% endtrans %}
+ {% elif project.lifecycle_status == "archived" %}
+
+
{% trans %}This project has been archived.{% endtrans %}
+
+ {% trans %}
+ The maintainers of this project have marked this project as archived.
+ No new releases are expected.
+ {% endtrans %}
+
+
{% elif release.yanked and release.yanked_reason %}
{% trans %}Reason this release was yanked:{% endtrans %}
From fb05541c80583bc05bc92740deecf12aba6a3f3e Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Tue, 26 Nov 2024 17:04:48 +0100
Subject: [PATCH 2/9] disallow uploads on archived projects
Signed-off-by: Facundo Tuesca
---
tests/unit/packaging/test_models.py | 94 +++++++++++++++++++++++++++++
warehouse/packaging/models.py | 44 ++++++++------
2 files changed, 118 insertions(+), 20 deletions(-)
diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py
index aac3cc37433b..89aa1366169f 100644
--- a/tests/unit/packaging/test_models.py
+++ b/tests/unit/packaging/test_models.py
@@ -365,6 +365,100 @@ def test_acl_for_quarantined_project(self, db_session):
key=lambda x: x[1],
)
+ def test_acl_for_archived_project(self, db_session):
+ """
+ If a Project is archived, the Project ACL should disallow uploads.
+ """
+ project = DBProjectFactory.create(lifecycle_status="archived")
+ owner1 = DBRoleFactory.create(project=project)
+ owner2 = DBRoleFactory.create(project=project)
+
+ # Maintainers should not appear in the ACLs, since they only have
+ # upload permissions, and anchived projects don't allow upload
+ DBRoleFactory.create(project=project, role_name="Maintainer")
+ DBRoleFactory.create(project=project, role_name="Maintainer")
+
+ organization = DBOrganizationFactory.create()
+ owner3 = DBOrganizationRoleFactory.create(organization=organization)
+ DBOrganizationProjectFactory.create(organization=organization, project=project)
+
+ team = DBTeamFactory.create()
+ owner4 = DBTeamRoleFactory.create(team=team)
+ DBTeamProjectRoleFactory.create(
+ team=team, project=project, role_name=TeamProjectRoleType.Owner
+ )
+
+ # Publishers should not appear in the ACLs, since they only have upload
+ # permissions, and archived projects don't allow upload
+ GitHubPublisherFactory.create(projects=[project])
+
+ acls = []
+ for location in lineage(project):
+ try:
+ acl = location.__acl__
+ except AttributeError:
+ continue
+
+ if acl and callable(acl):
+ acl = acl()
+
+ acls.extend(acl)
+
+ _perms_read_and_write = [
+ Permissions.ProjectsRead,
+ Permissions.ProjectsWrite,
+ ]
+ assert acls == [
+ (
+ Allow,
+ "group:admins",
+ (
+ Permissions.AdminDashboardSidebarRead,
+ Permissions.AdminObservationsRead,
+ Permissions.AdminObservationsWrite,
+ Permissions.AdminProhibitedProjectsWrite,
+ Permissions.AdminProhibitedUsernameWrite,
+ Permissions.AdminProjectsDelete,
+ Permissions.AdminProjectsRead,
+ Permissions.AdminProjectsSetLimit,
+ Permissions.AdminProjectsWrite,
+ Permissions.AdminRoleAdd,
+ Permissions.AdminRoleDelete,
+ ),
+ ),
+ (
+ Allow,
+ "group:moderators",
+ (
+ Permissions.AdminDashboardSidebarRead,
+ Permissions.AdminObservationsRead,
+ Permissions.AdminObservationsWrite,
+ Permissions.AdminProjectsRead,
+ Permissions.AdminProjectsSetLimit,
+ Permissions.AdminRoleAdd,
+ Permissions.AdminRoleDelete,
+ ),
+ ),
+ (
+ Allow,
+ "group:observers",
+ Permissions.APIObservationsAdd,
+ ),
+ (
+ Allow,
+ Authenticated,
+ Permissions.SubmitMalwareObservation,
+ ),
+ ] + sorted(
+ [
+ (Allow, f"user:{owner1.user.id}", _perms_read_and_write),
+ (Allow, f"user:{owner2.user.id}", _perms_read_and_write),
+ (Allow, f"user:{owner3.user.id}", _perms_read_and_write),
+ (Allow, f"user:{owner4.user.id}", _perms_read_and_write),
+ ],
+ key=lambda x: x[1],
+ )
+
def test_repr(self, db_request):
project = DBProjectFactory()
assert isinstance(repr(project), str)
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
index ab318c5aa0d9..5f218f02e088 100644
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -331,7 +331,11 @@ def __acl__(self):
# The project has zero or more OIDC publishers registered to it,
# each of which serves as an identity with the ability to upload releases.
for publisher in self.oidc_publishers:
- acls.append((Allow, f"oidc:{publisher.id}", [Permissions.ProjectsUpload]))
+ if self.lifecycle_status != LifecycleStatus.Archived:
+ # Only allow uploads in non-archived projects
+ acls.append(
+ (Allow, f"oidc:{publisher.id}", [Permissions.ProjectsUpload])
+ )
# Get all of the users for this project.
user_query = (
@@ -377,27 +381,27 @@ def __acl__(self):
for user_id, permission_name in sorted(permissions, key=lambda x: (x[1], x[0])):
# Disallow Write permissions for Projects in quarantine, allow Upload
if self.lifecycle_status == LifecycleStatus.QuarantineEnter:
- acls.append(
- (
- Allow,
- f"user:{user_id}",
- [Permissions.ProjectsRead, Permissions.ProjectsUpload],
- )
- )
+ current_permissions = [
+ Permissions.ProjectsRead,
+ Permissions.ProjectsUpload,
+ ]
elif permission_name == "Administer":
- acls.append(
- (
- Allow,
- f"user:{user_id}",
- [
- Permissions.ProjectsRead,
- Permissions.ProjectsUpload,
- Permissions.ProjectsWrite,
- ],
- )
- )
+ current_permissions = [
+ Permissions.ProjectsRead,
+ Permissions.ProjectsUpload,
+ Permissions.ProjectsWrite,
+ ]
else:
- acls.append((Allow, f"user:{user_id}", [Permissions.ProjectsUpload]))
+ current_permissions = [Permissions.ProjectsUpload]
+
+ if self.lifecycle_status == LifecycleStatus.Archived:
+ # Disallow upload permissions for archived projects
+ current_permissions = [
+ p for p in current_permissions if p != Permissions.ProjectsUpload
+ ]
+
+ if current_permissions:
+ acls.append((Allow, f"user:{user_id}", current_permissions))
return acls
@property
From 24c57a3ddffcfc40e8ce93500690474c8109d48b Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Tue, 26 Nov 2024 22:09:16 +0100
Subject: [PATCH 3/9] remove dead code
Signed-off-by: Facundo Tuesca
---
warehouse/templates/manage/project/settings.html | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/warehouse/templates/manage/project/settings.html b/warehouse/templates/manage/project/settings.html
index f84717dad358..79351180ecb3 100644
--- a/warehouse/templates/manage/project/settings.html
+++ b/warehouse/templates/manage/project/settings.html
@@ -59,10 +59,7 @@ {% trans %}Archive project{% endtrans %}
{% trans %}Unarchive project{% endtrans %}
- {% if not project.lifecycle_status %}
- {% elif project.lifecycle_status == "archived" %}
- {% endif %}
-
+
{% set action = request.route_path('manage.project.archive', project_name=project.name) %}
{% set slug = "archive-project" %}
{% set title = gettext("Archive project") %}
From 66df8bdcddde09c2582662c24c29433bdc60aec6 Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Tue, 26 Nov 2024 22:12:14 +0100
Subject: [PATCH 4/9] move archive functions
Signed-off-by: Facundo Tuesca
---
tests/unit/manage/test_views.py | 8 ++---
warehouse/manage/views/__init__.py | 50 +++++++-----------------------
warehouse/utils/project.py | 39 +++++++++++++++++++++++
3 files changed, 54 insertions(+), 43 deletions(-)
diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py
index 384d7ddc8509..b7ebeaa3a40f 100644
--- a/tests/unit/manage/test_views.py
+++ b/tests/unit/manage/test_views.py
@@ -7970,7 +7970,7 @@ def test_archive(self, db_request):
flash=pretend.call_recorder(lambda *a, **kw: None)
)
- result = views.archive_project(project, db_request)
+ result = views.archive_project_view(project, db_request)
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/the-redirect"
@@ -7992,7 +7992,7 @@ def test_unarchive_project(self, db_request):
flash=pretend.call_recorder(lambda *a, **kw: None)
)
- result = views.unarchive_project(project, db_request)
+ result = views.unarchive_project_view(project, db_request)
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/the-redirect"
@@ -8012,7 +8012,7 @@ def test_disallowed_archive(self, db_request):
flash=pretend.call_recorder(lambda *a, **kw: None)
)
- result = views.archive_project(project, db_request)
+ result = views.archive_project_view(project, db_request)
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/the-redirect"
@@ -8038,7 +8038,7 @@ def test_disallowed_unarchive(self, db_request):
flash=pretend.call_recorder(lambda *a, **kw: None)
)
- result = views.unarchive_project(project, db_request)
+ result = views.unarchive_project_view(project, db_request)
assert isinstance(result, HTTPSeeOther)
assert result.headers["Location"] == "/the-redirect"
diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py
index ffd8d3aa0560..a619bc1e6ab5 100644
--- a/warehouse/manage/views/__init__.py
+++ b/warehouse/manage/views/__init__.py
@@ -132,7 +132,6 @@
AlternateRepository,
File,
JournalEntry,
- LifecycleStatus,
Project,
Release,
Role,
@@ -142,7 +141,13 @@
from warehouse.rate_limiting import IRateLimiter
from warehouse.utils.http import is_safe_url
from warehouse.utils.paginate import paginate_url_factory
-from warehouse.utils.project import confirm_project, destroy_docs, remove_project
+from warehouse.utils.project import (
+ archive_project,
+ confirm_project,
+ destroy_docs,
+ remove_project,
+ unarchive_project,
+)
class ManageAccountMixin:
@@ -3301,30 +3306,12 @@ def manage_project_documentation(project, request):
uses_session=True,
require_methods=["POST"],
permission=Permissions.ProjectsWrite,
- has_translations=True,
)
-def archive_project(project, request) -> HTTPSeeOther:
+def archive_project_view(project, request) -> HTTPSeeOther:
"""
Archive a Project. Reversible action.
"""
- if (
- project.lifecycle_status is None
- or project.lifecycle_status == LifecycleStatus.QuarantineExit
- ):
- project.lifecycle_status = LifecycleStatus.Archived
- project.record_event(
- tag=EventTag.Project.ProjectArchiveEnter,
- request=request,
- additional={
- "submitted_by": request.user.username,
- },
- )
- else:
- request.session.flash(
- request._(f"Cannot archive project with status {project.lifecycle_status}"),
- queue="error",
- )
-
+ archive_project(project, request)
return HTTPSeeOther(
request.route_path("manage.project.settings", project_name=project.name)
)
@@ -3336,27 +3323,12 @@ def archive_project(project, request) -> HTTPSeeOther:
uses_session=True,
require_methods=["POST"],
permission=Permissions.ProjectsWrite,
- has_translations=True,
)
-def unarchive_project(project, request) -> HTTPSeeOther:
+def unarchive_project_view(project, request) -> HTTPSeeOther:
"""
Unarchive a Project. Reversible action.
"""
- if project.lifecycle_status == LifecycleStatus.Archived:
- project.lifecycle_status = None
- project.record_event(
- tag=EventTag.Project.ProjectArchiveExit,
- request=request,
- additional={
- "submitted_by": request.user.username,
- },
- )
- else:
- request.session.flash(
- request._("Can only unarchive an archived project"),
- queue="error",
- )
-
+ unarchive_project(project, request)
return HTTPSeeOther(
request.route_path("manage.project.settings", project_name=project.name)
)
diff --git a/warehouse/utils/project.py b/warehouse/utils/project.py
index e6ff187e770e..9b6f0fcf4edf 100644
--- a/warehouse/utils/project.py
+++ b/warehouse/utils/project.py
@@ -196,3 +196,42 @@ def destroy_docs(project, request, flash=True):
request.session.flash(
f"Deleted docs for project {project.name!r}", queue="success"
)
+
+
+def archive_project(project: Project, request) -> None:
+ if (
+ project.lifecycle_status is None
+ or project.lifecycle_status == LifecycleStatus.QuarantineExit
+ ):
+ project.lifecycle_status = LifecycleStatus.Archived
+ project.record_event(
+ tag=EventTag.Project.ProjectArchiveEnter,
+ request=request,
+ additional={
+ "submitted_by": request.user.username,
+ },
+ )
+ request.session.flash("Project archived", queue="success")
+ else:
+ request.session.flash(
+ f"Cannot archive project with status {project.lifecycle_status}",
+ queue="error",
+ )
+
+
+def unarchive_project(project: Project, request) -> None:
+ if project.lifecycle_status == LifecycleStatus.Archived:
+ project.lifecycle_status = None
+ project.record_event(
+ tag=EventTag.Project.ProjectArchiveExit,
+ request=request,
+ additional={
+ "submitted_by": request.user.username,
+ },
+ )
+ request.session.flash("Project unarchived", queue="success")
+ else:
+ request.session.flash(
+ "Can only unarchive an archived project",
+ queue="error",
+ )
From e849c32cfb5ada5532943e53de451b9a722daa2e Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Tue, 26 Nov 2024 22:12:27 +0100
Subject: [PATCH 5/9] add admin UI to archive/unarchive projects
Signed-off-by: Facundo Tuesca
---
tests/unit/admin/test_routes.py | 14 +
tests/unit/admin/views/test_projects.py | 95 ++++++-
warehouse/admin/routes.py | 14 +
.../templates/admin/projects/detail.html | 27 ++
warehouse/admin/views/projects.py | 36 +++
warehouse/locale/messages.pot | 258 +++++++++---------
6 files changed, 312 insertions(+), 132 deletions(-)
diff --git a/tests/unit/admin/test_routes.py b/tests/unit/admin/test_routes.py
index ab481bf4cd75..1b1fa30ebcbd 100644
--- a/tests/unit/admin/test_routes.py
+++ b/tests/unit/admin/test_routes.py
@@ -248,6 +248,20 @@ def test_includeme():
traverse="/{project_name}",
domain=warehouse,
),
+ pretend.call(
+ "admin.project.archive",
+ "/admin/projects/{project_name}/archive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ ),
+ pretend.call(
+ "admin.project.unarchive",
+ "/admin/projects/{project_name}/unarchive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ ),
pretend.call("admin.journals.list", "/admin/journals/", domain=warehouse),
pretend.call(
"admin.prohibited_project_names.list",
diff --git a/tests/unit/admin/views/test_projects.py b/tests/unit/admin/views/test_projects.py
index 47c17b064b50..918fe09ef33e 100644
--- a/tests/unit/admin/views/test_projects.py
+++ b/tests/unit/admin/views/test_projects.py
@@ -26,7 +26,7 @@
from tests.common.db.oidc import GitHubPublisherFactory
from warehouse.admin.views import projects as views
from warehouse.observations.models import ObservationKind
-from warehouse.packaging.models import Project, Role
+from warehouse.packaging.models import LifecycleStatus, Project, Role
from warehouse.packaging.tasks import update_release_description
from warehouse.search.tasks import reindex_project
from warehouse.utils.paginate import paginate_url_factory
@@ -952,3 +952,96 @@ def test_reindexes_project(self, db_request):
assert db_request.session.flash.calls == [
pretend.call("Task sent to reindex the project 'foo'", queue="success")
]
+
+
+class TestProjectArchival:
+ def test_archive(self, db_request):
+ project = ProjectFactory.create(name="foo")
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.archive_project_view(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert project.lifecycle_status == LifecycleStatus.Archived
+ assert db_request.route_path.calls == [
+ pretend.call("admin.project.detail", project_name=project.name)
+ ]
+
+ def test_unarchive_project(self, db_request):
+ project = ProjectFactory.create(
+ name="foo", lifecycle_status=LifecycleStatus.Archived
+ )
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.unarchive_project_view(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.route_path.calls == [
+ pretend.call("admin.project.detail", project_name=project.name)
+ ]
+ assert project.lifecycle_status is None
+
+ def test_disallowed_archive(self, db_request):
+ project = ProjectFactory.create(name="foo", lifecycle_status="quarantine-enter")
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.archive_project_view(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call(
+ f"Cannot archive project with status {project.lifecycle_status}",
+ queue="error",
+ )
+ ]
+ assert db_request.route_path.calls == [
+ pretend.call("admin.project.detail", project_name="foo")
+ ]
+ assert project.lifecycle_status == "quarantine-enter"
+
+ def test_disallowed_unarchive(self, db_request):
+ project = ProjectFactory.create(name="foo", lifecycle_status="quarantine-enter")
+ user = UserFactory.create(username="testuser")
+
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.method = "POST"
+ db_request.user = user
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ result = views.unarchive_project_view(project, db_request)
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call("Can only unarchive an archived project", queue="error")
+ ]
+ assert db_request.route_path.calls == [
+ pretend.call("admin.project.detail", project_name="foo")
+ ]
+ assert project.lifecycle_status == "quarantine-enter"
diff --git a/warehouse/admin/routes.py b/warehouse/admin/routes.py
index 0f8b941d7599..fe7bdae93635 100644
--- a/warehouse/admin/routes.py
+++ b/warehouse/admin/routes.py
@@ -252,6 +252,20 @@ def includeme(config):
traverse="/{project_name}",
domain=warehouse,
)
+ config.add_route(
+ "admin.project.archive",
+ "/admin/projects/{project_name}/archive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ )
+ config.add_route(
+ "admin.project.unarchive",
+ "/admin/projects/{project_name}/unarchive/",
+ factory="warehouse.packaging.models:ProjectFactory",
+ traverse="/{project_name}",
+ domain=warehouse,
+ )
# Journal related Admin pages
config.add_route("admin.journals.list", "/admin/journals/", domain=warehouse)
diff --git a/warehouse/admin/templates/admin/projects/detail.html b/warehouse/admin/templates/admin/projects/detail.html
index 77e6d2b035a8..b74ae332cab5 100644
--- a/warehouse/admin/templates/admin/projects/detail.html
+++ b/warehouse/admin/templates/admin/projects/detail.html
@@ -582,6 +582,33 @@ Prohibit Project Name
+
+
+
+
+
+
{% include 'delete.html' %}
diff --git a/warehouse/admin/views/projects.py b/warehouse/admin/views/projects.py
index 07f5ada3ede2..dfa1f797df38 100644
--- a/warehouse/admin/views/projects.py
+++ b/warehouse/admin/views/projects.py
@@ -30,9 +30,11 @@
from warehouse.search.tasks import reindex_project as _reindex_project
from warehouse.utils.paginate import paginate_url_factory
from warehouse.utils.project import (
+ archive_project,
clear_project_quarantine,
confirm_project,
remove_project,
+ unarchive_project,
)
UPLOAD_LIMIT_CAP = ONE_GIB
@@ -737,3 +739,37 @@ def reindex_project(project, request):
return HTTPSeeOther(
request.route_path("admin.project.detail", project_name=project.normalized_name)
)
+
+
+@view_config(
+ route_name="admin.project.archive",
+ permission=Permissions.AdminProjectsWrite,
+ context=Project,
+ uses_session=True,
+ require_methods=["POST"],
+)
+def archive_project_view(project, request) -> HTTPSeeOther:
+ """
+ Archive a Project. Reversible action.
+ """
+ archive_project(project, request)
+ return HTTPSeeOther(
+ request.route_path("admin.project.detail", project_name=project.name)
+ )
+
+
+@view_config(
+ route_name="admin.project.unarchive",
+ permission=Permissions.AdminProjectsWrite,
+ context=Project,
+ uses_session=True,
+ require_methods=["POST"],
+)
+def unarchive_project_view(project, request) -> HTTPSeeOther:
+ """
+ Unarchive a Project. Reversible action.
+ """
+ unarchive_project(project, request)
+ return HTTPSeeOther(
+ request.route_path("admin.project.detail", project_name=project.name)
+ )
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index f9407e6874b0..0a6289efbcc7 100644
--- a/warehouse/locale/messages.pot
+++ b/warehouse/locale/messages.pot
@@ -152,7 +152,7 @@ msgstr ""
msgid "Successful WebAuthn assertion"
msgstr ""
-#: warehouse/accounts/views.py:609 warehouse/manage/views/__init__.py:872
+#: warehouse/accounts/views.py:609 warehouse/manage/views/__init__.py:877
msgid "Recovery code accepted. The supplied code cannot be used again."
msgstr ""
@@ -286,7 +286,7 @@ msgid "You are now ${role} of the '${project_name}' project."
msgstr ""
#: warehouse/accounts/views.py:1588 warehouse/accounts/views.py:1831
-#: warehouse/manage/views/__init__.py:1408
+#: warehouse/manage/views/__init__.py:1413
msgid ""
"Trusted publishing is temporarily disabled. See https://pypi.org/help"
"#admin-intervention for details."
@@ -306,19 +306,19 @@ msgstr ""
msgid "You can't register more than 3 pending trusted publishers at once."
msgstr ""
-#: warehouse/accounts/views.py:1654 warehouse/manage/views/__init__.py:1577
-#: warehouse/manage/views/__init__.py:1690
-#: warehouse/manage/views/__init__.py:1802
-#: warehouse/manage/views/__init__.py:1912
+#: warehouse/accounts/views.py:1654 warehouse/manage/views/__init__.py:1582
+#: warehouse/manage/views/__init__.py:1695
+#: warehouse/manage/views/__init__.py:1807
+#: warehouse/manage/views/__init__.py:1917
msgid ""
"There have been too many attempted trusted publisher registrations. Try "
"again later."
msgstr ""
-#: warehouse/accounts/views.py:1665 warehouse/manage/views/__init__.py:1591
-#: warehouse/manage/views/__init__.py:1704
-#: warehouse/manage/views/__init__.py:1816
-#: warehouse/manage/views/__init__.py:1926
+#: warehouse/accounts/views.py:1665 warehouse/manage/views/__init__.py:1596
+#: warehouse/manage/views/__init__.py:1709
+#: warehouse/manage/views/__init__.py:1821
+#: warehouse/manage/views/__init__.py:1931
msgid "The trusted publisher could not be registered"
msgstr ""
@@ -446,169 +446,165 @@ msgid ""
"less."
msgstr ""
-#: warehouse/manage/views/__init__.py:284
+#: warehouse/manage/views/__init__.py:289
msgid "Account details updated"
msgstr ""
-#: warehouse/manage/views/__init__.py:314
+#: warehouse/manage/views/__init__.py:319
msgid "Email ${email_address} added - check your email for a verification link"
msgstr ""
-#: warehouse/manage/views/__init__.py:820
+#: warehouse/manage/views/__init__.py:825
msgid "Recovery codes already generated"
msgstr ""
-#: warehouse/manage/views/__init__.py:821
+#: warehouse/manage/views/__init__.py:826
msgid "Generating new recovery codes will invalidate your existing codes."
msgstr ""
-#: warehouse/manage/views/__init__.py:930
+#: warehouse/manage/views/__init__.py:935
msgid "Verify your email to create an API token."
msgstr ""
-#: warehouse/manage/views/__init__.py:1030
+#: warehouse/manage/views/__init__.py:1035
msgid "API Token does not exist."
msgstr ""
-#: warehouse/manage/views/__init__.py:1062
+#: warehouse/manage/views/__init__.py:1067
msgid "Invalid credentials. Try again"
msgstr ""
-#: warehouse/manage/views/__init__.py:1181
+#: warehouse/manage/views/__init__.py:1186
msgid "Invalid alternate repository location details"
msgstr ""
-#: warehouse/manage/views/__init__.py:1218
+#: warehouse/manage/views/__init__.py:1223
msgid "Added alternate repository '${name}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:1252
-#: warehouse/manage/views/__init__.py:2259
-#: warehouse/manage/views/__init__.py:2344
-#: warehouse/manage/views/__init__.py:2445
-#: warehouse/manage/views/__init__.py:2545
+#: warehouse/manage/views/__init__.py:1257
+#: warehouse/manage/views/__init__.py:2264
+#: warehouse/manage/views/__init__.py:2349
+#: warehouse/manage/views/__init__.py:2450
+#: warehouse/manage/views/__init__.py:2550
msgid "Confirm the request"
msgstr ""
-#: warehouse/manage/views/__init__.py:1264
+#: warehouse/manage/views/__init__.py:1269
msgid "Invalid alternate repository id"
msgstr ""
-#: warehouse/manage/views/__init__.py:1275
+#: warehouse/manage/views/__init__.py:1280
msgid "Invalid alternate repository for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:1283
+#: warehouse/manage/views/__init__.py:1288
msgid ""
"Could not delete alternate repository - ${confirm} is not the same as "
"${alt_repo_name}"
msgstr ""
-#: warehouse/manage/views/__init__.py:1313
+#: warehouse/manage/views/__init__.py:1318
msgid "Deleted alternate repository '${name}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:1458
+#: warehouse/manage/views/__init__.py:1463
msgid "The trusted publisher could not be constrained"
msgstr ""
-#: warehouse/manage/views/__init__.py:1558
+#: warehouse/manage/views/__init__.py:1563
msgid ""
"GitHub-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1671
+#: warehouse/manage/views/__init__.py:1676
msgid ""
"GitLab-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1783
+#: warehouse/manage/views/__init__.py:1788
msgid ""
"Google-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1892
+#: warehouse/manage/views/__init__.py:1897
msgid ""
"ActiveState-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:2127
-#: warehouse/manage/views/__init__.py:2428
-#: warehouse/manage/views/__init__.py:2536
+#: warehouse/manage/views/__init__.py:2132
+#: warehouse/manage/views/__init__.py:2433
+#: warehouse/manage/views/__init__.py:2541
msgid ""
"Project deletion temporarily disabled. See https://pypi.org/help#admin-"
"intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:2271
+#: warehouse/manage/views/__init__.py:2276
msgid "Could not yank release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2356
+#: warehouse/manage/views/__init__.py:2361
msgid "Could not un-yank release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2457
+#: warehouse/manage/views/__init__.py:2462
msgid "Could not delete release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2557
+#: warehouse/manage/views/__init__.py:2562
msgid "Could not find file"
msgstr ""
-#: warehouse/manage/views/__init__.py:2561
+#: warehouse/manage/views/__init__.py:2566
msgid "Could not delete file - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2711
+#: warehouse/manage/views/__init__.py:2716
msgid "Team '${team_name}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2818
+#: warehouse/manage/views/__init__.py:2823
msgid "User '${username}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2885
+#: warehouse/manage/views/__init__.py:2890
msgid "${username} is now ${role} of the '${project_name}' project."
msgstr ""
-#: warehouse/manage/views/__init__.py:2917
+#: warehouse/manage/views/__init__.py:2922
msgid ""
"User '${username}' does not have a verified primary email address and "
"cannot be added as a ${role_name} for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2930
+#: warehouse/manage/views/__init__.py:2935
#: warehouse/manage/views/organizations.py:889
msgid "User '${username}' already has an active invite. Please try again later."
msgstr ""
-#: warehouse/manage/views/__init__.py:2995
+#: warehouse/manage/views/__init__.py:3000
#: warehouse/manage/views/organizations.py:954
msgid "Invitation sent to '${username}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:3028
+#: warehouse/manage/views/__init__.py:3033
msgid "Could not find role invitation."
msgstr ""
-#: warehouse/manage/views/__init__.py:3039
+#: warehouse/manage/views/__init__.py:3044
msgid "Invitation already expired."
msgstr ""
-#: warehouse/manage/views/__init__.py:3071
+#: warehouse/manage/views/__init__.py:3076
#: warehouse/manage/views/organizations.py:1141
msgid "Invitation revoked from '${username}'."
msgstr ""
-#: warehouse/manage/views/__init__.py:3356
-msgid "Can only unarchive an archived project"
-msgstr ""
-
#: warehouse/manage/views/organizations.py:865
msgid "User '${username}' already has ${role_name} role for organization"
msgstr ""
@@ -1152,9 +1148,9 @@ msgstr ""
#: warehouse/templates/manage/organization/settings.html:286
#: warehouse/templates/manage/project/documentation.html:27
#: warehouse/templates/manage/project/release.html:182
-#: warehouse/templates/manage/project/settings.html:122
-#: warehouse/templates/manage/project/settings.html:171
-#: warehouse/templates/manage/project/settings.html:392
+#: warehouse/templates/manage/project/settings.html:119
+#: warehouse/templates/manage/project/settings.html:168
+#: warehouse/templates/manage/project/settings.html:389
#: warehouse/templates/manage/team/settings.html:84
msgid "Warning"
msgstr ""
@@ -1521,9 +1517,9 @@ msgstr ""
#: warehouse/templates/manage/project/roles.html:328
#: warehouse/templates/manage/project/roles.html:359
#: warehouse/templates/manage/project/roles.html:380
-#: warehouse/templates/manage/project/settings.html:322
-#: warehouse/templates/manage/project/settings.html:342
-#: warehouse/templates/manage/project/settings.html:362
+#: warehouse/templates/manage/project/settings.html:319
+#: warehouse/templates/manage/project/settings.html:339
+#: warehouse/templates/manage/project/settings.html:359
#: warehouse/templates/manage/team/roles.html:106
#: warehouse/templates/manage/team/settings.html:35
#: warehouse/templates/packaging/submit-malware-observation.html:58
@@ -1817,9 +1813,9 @@ msgstr ""
#: warehouse/templates/manage/project/history.html:312
#: warehouse/templates/manage/project/history.html:323
#: warehouse/templates/manage/project/history.html:334
-#: warehouse/templates/manage/project/settings.html:259
-#: warehouse/templates/manage/project/settings.html:320
-#: warehouse/templates/manage/project/settings.html:326
+#: warehouse/templates/manage/project/settings.html:256
+#: warehouse/templates/manage/project/settings.html:317
+#: warehouse/templates/manage/project/settings.html:323
#: warehouse/templates/manage/unverified-account.html:112
msgid "Name"
msgstr ""
@@ -2742,7 +2738,7 @@ msgstr ""
#: warehouse/templates/manage/manage_base.html:331
#: warehouse/templates/manage/project/release.html:137
#: warehouse/templates/manage/project/releases.html:178
-#: warehouse/templates/manage/project/settings.html:107
+#: warehouse/templates/manage/project/settings.html:104
#: warehouse/templates/manage/unverified-account.html:172
#: warehouse/templates/manage/unverified-account.html:174
#: warehouse/templates/manage/unverified-account.html:184
@@ -3399,12 +3395,12 @@ msgid "Update password"
msgstr ""
#: warehouse/templates/manage/account.html:472
-#: warehouse/templates/manage/project/settings.html:78
+#: warehouse/templates/manage/project/settings.html:75
msgid "API tokens"
msgstr ""
#: warehouse/templates/manage/account.html:473
-#: warehouse/templates/manage/project/settings.html:79
+#: warehouse/templates/manage/project/settings.html:76
msgid ""
"API tokens provide an alternative way to authenticate when uploading "
"packages to PyPI."
@@ -4611,7 +4607,7 @@ msgstr ""
#: warehouse/templates/manage/project/publishing.html:275
#: warehouse/templates/manage/project/publishing.html:357
#: warehouse/templates/manage/project/roles.html:341
-#: warehouse/templates/manage/project/settings.html:383
+#: warehouse/templates/manage/project/settings.html:380
#: warehouse/templates/manage/team/roles.html:131
msgid "Add"
msgstr ""
@@ -5670,7 +5666,7 @@ msgstr ""
#: warehouse/templates/manage/organization/roles.html:252
#: warehouse/templates/manage/project/release.html:106
#: warehouse/templates/manage/project/releases.html:109
-#: warehouse/templates/manage/project/settings.html:287
+#: warehouse/templates/manage/project/settings.html:284
msgid "Delete"
msgstr ""
@@ -6067,9 +6063,9 @@ msgstr ""
#: warehouse/templates/manage/project/history.html:313
#: warehouse/templates/manage/project/history.html:324
#: warehouse/templates/manage/project/history.html:335
-#: warehouse/templates/manage/project/settings.html:260
-#: warehouse/templates/manage/project/settings.html:340
-#: warehouse/templates/manage/project/settings.html:346
+#: warehouse/templates/manage/project/settings.html:257
+#: warehouse/templates/manage/project/settings.html:337
+#: warehouse/templates/manage/project/settings.html:343
msgid "Url"
msgstr ""
@@ -6277,7 +6273,7 @@ msgstr ""
#: warehouse/templates/manage/project/release.html:137
#: warehouse/templates/manage/project/releases.html:178
-#: warehouse/templates/manage/project/settings.html:107
+#: warehouse/templates/manage/project/settings.html:104
msgid "Dismiss"
msgstr ""
@@ -6622,7 +6618,7 @@ msgstr ""
#: warehouse/templates/manage/project/settings.html:43
#: warehouse/templates/manage/project/settings.html:57
-#: warehouse/templates/manage/project/settings.html:68
+#: warehouse/templates/manage/project/settings.html:65
msgid "Archive project"
msgstr ""
@@ -6636,35 +6632,35 @@ msgid ""
msgstr ""
#: warehouse/templates/manage/project/settings.html:60
-#: warehouse/templates/manage/project/settings.html:74
+#: warehouse/templates/manage/project/settings.html:71
msgid "Unarchive project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:69
+#: warehouse/templates/manage/project/settings.html:66
msgid "Archiving a project will block any new file uploads"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:75
+#: warehouse/templates/manage/project/settings.html:72
msgid "Unarchiving a project will allow new file uploads"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:83
+#: warehouse/templates/manage/project/settings.html:80
#, python-format
msgid "Create a token for %(project_name)s"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:88
+#: warehouse/templates/manage/project/settings.html:85
#, python-format
msgid ""
"Verify your primary email address to add an API "
"token for %(project_name)s."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:95
+#: warehouse/templates/manage/project/settings.html:92
msgid "Project description and sidebar"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:97
+#: warehouse/templates/manage/project/settings.html:94
#, python-format
msgid ""
"To set the '%(project_name)s' description, author, links, classifiers, "
@@ -6680,147 +6676,147 @@ msgid ""
"Python Packaging User Guide for more help."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:120
+#: warehouse/templates/manage/project/settings.html:117
msgid "Remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:123
+#: warehouse/templates/manage/project/settings.html:120
msgid "Removing this project from the organization will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:127
-#: warehouse/templates/manage/project/settings.html:177
+#: warehouse/templates/manage/project/settings.html:124
+#: warehouse/templates/manage/project/settings.html:174
#, python-format
msgid "Remove this project from the '%(organization_name)s' organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:130
-#: warehouse/templates/manage/project/settings.html:180
+#: warehouse/templates/manage/project/settings.html:127
+#: warehouse/templates/manage/project/settings.html:177
#, python-format
msgid ""
"Revoke project permissions for teams in the '%(organization_name)s' "
"organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:134
-#: warehouse/templates/manage/project/settings.html:140
+#: warehouse/templates/manage/project/settings.html:131
+#: warehouse/templates/manage/project/settings.html:137
msgid ""
"Individual owners and maintainers of the project will retain their "
"project permissions."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:139
+#: warehouse/templates/manage/project/settings.html:136
#, python-format
msgid ""
"This will remove the project from the '%(organization_name)s' "
"organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:143
+#: warehouse/templates/manage/project/settings.html:140
msgid "Remove project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:143
-#: warehouse/templates/manage/project/settings.html:214
-#: warehouse/templates/manage/project/settings.html:430
+#: warehouse/templates/manage/project/settings.html:140
+#: warehouse/templates/manage/project/settings.html:211
+#: warehouse/templates/manage/project/settings.html:427
msgid "Project Name"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:147
+#: warehouse/templates/manage/project/settings.html:144
msgid "Cannot remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:149
+#: warehouse/templates/manage/project/settings.html:146
msgid ""
"Your organization is currently the sole owner of the "
"project. You must add an individual owner to the project before you can "
"remove the project from your organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:165
+#: warehouse/templates/manage/project/settings.html:162
msgid "Transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:167
+#: warehouse/templates/manage/project/settings.html:164
msgid "Transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:172
+#: warehouse/templates/manage/project/settings.html:169
msgid "Transferring this project will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:184
+#: warehouse/templates/manage/project/settings.html:181
msgid "Revoke your direct Owner role on the project."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:187
+#: warehouse/templates/manage/project/settings.html:184
msgid ""
"You will retain Owner permissions on the project through your "
"organization role."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:192
+#: warehouse/templates/manage/project/settings.html:189
msgid "Add the project to another organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:194
+#: warehouse/templates/manage/project/settings.html:191
msgid "Add the project to an organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:198
+#: warehouse/templates/manage/project/settings.html:195
msgid "Grant full project permissions to owners of the organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:202
+#: warehouse/templates/manage/project/settings.html:199
msgid ""
"All other individual owners and maintainers of the project will retain "
"their project permissions."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:214
+#: warehouse/templates/manage/project/settings.html:211
msgid "Transfer project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:220
+#: warehouse/templates/manage/project/settings.html:217
msgid "Cannot transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:222
+#: warehouse/templates/manage/project/settings.html:219
msgid "Cannot transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:227
+#: warehouse/templates/manage/project/settings.html:224
msgid ""
"Organization owners can transfer the project to organizations that they "
"own or manage."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:228
+#: warehouse/templates/manage/project/settings.html:225
msgid "You are not an owner or manager of any other organizations."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:230
+#: warehouse/templates/manage/project/settings.html:227
msgid ""
"Project owners can transfer the project to organizations that they own or"
" manage."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:231
+#: warehouse/templates/manage/project/settings.html:228
msgid "You are not an owner or manager of any organizations."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:240
+#: warehouse/templates/manage/project/settings.html:237
msgid "Alternate repository locations"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:244
+#: warehouse/templates/manage/project/settings.html:241
#, python-format
msgid ""
"Provisional support for PEP 708 \"Alternate "
"Locations\" Metadata."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:248
+#: warehouse/templates/manage/project/settings.html:245
#, python-format
msgid ""
"Implementation may change, consider subscribing to %(count)s"
@@ -6887,15 +6883,15 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: warehouse/templates/manage/project/settings.html:404
+#: warehouse/templates/manage/project/settings.html:401
msgid "Irreversibly delete the project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:408
+#: warehouse/templates/manage/project/settings.html:405
msgid "Make the project name available to any other PyPI user"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:410
+#: warehouse/templates/manage/project/settings.html:407
msgid ""
"This user will be able to make new releases under this project name, so "
"long as the distribution filenames do not match filenames from a "
From 73c2eba429b53789fd7a694b01e185d58f9eeabb Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Mon, 6 Jan 2025 16:19:36 +0100
Subject: [PATCH 6/9] Apply suggestions from code review
Co-authored-by: Mike Fiedler
---
tests/unit/packaging/test_models.py | 5 ++---
warehouse/packaging/models.py | 4 +---
2 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py
index 89aa1366169f..8a80d58129e2 100644
--- a/tests/unit/packaging/test_models.py
+++ b/tests/unit/packaging/test_models.py
@@ -374,9 +374,8 @@ def test_acl_for_archived_project(self, db_session):
owner2 = DBRoleFactory.create(project=project)
# Maintainers should not appear in the ACLs, since they only have
- # upload permissions, and anchived projects don't allow upload
- DBRoleFactory.create(project=project, role_name="Maintainer")
- DBRoleFactory.create(project=project, role_name="Maintainer")
+ # upload permissions, and archived projects don't allow upload
+ DBRoleFactory.create_batch(2, project=project, role_name="Maintainer")
organization = DBOrganizationFactory.create()
owner3 = DBOrganizationRoleFactory.create(organization=organization)
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
index 5f218f02e088..31a42fe2db3b 100644
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -396,9 +396,7 @@ def __acl__(self):
if self.lifecycle_status == LifecycleStatus.Archived:
# Disallow upload permissions for archived projects
- current_permissions = [
- p for p in current_permissions if p != Permissions.ProjectsUpload
- ]
+ current_permissions.remove(Permissions.ProjectsUpload)
if current_permissions:
acls.append((Allow, f"user:{user_id}", current_permissions))
From 4183812f490d8f1a3d86ca5a45212ca435d2ddfd Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Mon, 6 Jan 2025 16:38:55 +0100
Subject: [PATCH 7/9] Improve code structure
Signed-off-by: Facundo Tuesca
---
warehouse/packaging/models.py | 10 ++++----
warehouse/utils/project.py | 48 ++++++++++++++++++-----------------
2 files changed, 30 insertions(+), 28 deletions(-)
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
index 31a42fe2db3b..476638de019b 100644
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -328,11 +328,11 @@ def __acl__(self):
(Allow, Authenticated, Permissions.SubmitMalwareObservation),
]
- # The project has zero or more OIDC publishers registered to it,
- # each of which serves as an identity with the ability to upload releases.
- for publisher in self.oidc_publishers:
- if self.lifecycle_status != LifecycleStatus.Archived:
- # Only allow uploads in non-archived projects
+ if self.lifecycle_status != LifecycleStatus.Archived:
+ # The project has zero or more OIDC publishers registered to it,
+ # each of which serves as an identity with the ability to upload releases
+ # (only if the project is not archived)
+ for publisher in self.oidc_publishers:
acls.append(
(Allow, f"oidc:{publisher.id}", [Permissions.ProjectsUpload])
)
diff --git a/warehouse/utils/project.py b/warehouse/utils/project.py
index 9b6f0fcf4edf..4ddefc027f09 100644
--- a/warehouse/utils/project.py
+++ b/warehouse/utils/project.py
@@ -200,38 +200,40 @@ def destroy_docs(project, request, flash=True):
def archive_project(project: Project, request) -> None:
if (
- project.lifecycle_status is None
- or project.lifecycle_status == LifecycleStatus.QuarantineExit
+ project.lifecycle_status is not None
+ and project.lifecycle_status != LifecycleStatus.QuarantineExit
):
- project.lifecycle_status = LifecycleStatus.Archived
- project.record_event(
- tag=EventTag.Project.ProjectArchiveEnter,
- request=request,
- additional={
- "submitted_by": request.user.username,
- },
- )
- request.session.flash("Project archived", queue="success")
- else:
request.session.flash(
f"Cannot archive project with status {project.lifecycle_status}",
queue="error",
)
+ return
+
+ project.lifecycle_status = LifecycleStatus.Archived
+ project.record_event(
+ tag=EventTag.Project.ProjectArchiveEnter,
+ request=request,
+ additional={
+ "submitted_by": request.user.username,
+ },
+ )
+ request.session.flash("Project archived", queue="success")
def unarchive_project(project: Project, request) -> None:
- if project.lifecycle_status == LifecycleStatus.Archived:
- project.lifecycle_status = None
- project.record_event(
- tag=EventTag.Project.ProjectArchiveExit,
- request=request,
- additional={
- "submitted_by": request.user.username,
- },
- )
- request.session.flash("Project unarchived", queue="success")
- else:
+ if project.lifecycle_status != LifecycleStatus.Archived:
request.session.flash(
"Can only unarchive an archived project",
queue="error",
)
+ return
+
+ project.lifecycle_status = None
+ project.record_event(
+ tag=EventTag.Project.ProjectArchiveExit,
+ request=request,
+ additional={
+ "submitted_by": request.user.username,
+ },
+ )
+ request.session.flash("Project unarchived", queue="success")
From f1a793067bbfba161049358386eb36e35debd515 Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Thu, 16 Jan 2025 16:51:00 +0100
Subject: [PATCH 8/9] Address review comments
Signed-off-by: Facundo Tuesca
---
.../templates/admin/projects/detail.html | 25 ++-
warehouse/locale/messages.pot | 203 +++++++++---------
.../templates/manage/project/settings.html | 69 +++---
3 files changed, 163 insertions(+), 134 deletions(-)
diff --git a/warehouse/admin/templates/admin/projects/detail.html b/warehouse/admin/templates/admin/projects/detail.html
index b74ae332cab5..89778d0004ad 100644
--- a/warehouse/admin/templates/admin/projects/detail.html
+++ b/warehouse/admin/templates/admin/projects/detail.html
@@ -96,6 +96,27 @@ Remove Project from Quarantine
+ {% elif project.lifecycle_status == 'archived' %}
+
+
+
+
+ This project is archived.
+ It should not allow any new uploads unless it's unarchived.
+
+
+
+
+
{% endif %}
@@ -599,13 +620,13 @@
Archive project
Unarchive
-
+
+
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index 0a6289efbcc7..1a6081502f95 100644
--- a/warehouse/locale/messages.pot
+++ b/warehouse/locale/messages.pot
@@ -1148,9 +1148,9 @@ msgstr ""
#: warehouse/templates/manage/organization/settings.html:286
#: warehouse/templates/manage/project/documentation.html:27
#: warehouse/templates/manage/project/release.html:182
-#: warehouse/templates/manage/project/settings.html:119
-#: warehouse/templates/manage/project/settings.html:168
-#: warehouse/templates/manage/project/settings.html:389
+#: warehouse/templates/manage/project/settings.html:87
+#: warehouse/templates/manage/project/settings.html:136
+#: warehouse/templates/manage/project/settings.html:394
#: warehouse/templates/manage/team/settings.html:84
msgid "Warning"
msgstr ""
@@ -1517,9 +1517,9 @@ msgstr ""
#: warehouse/templates/manage/project/roles.html:328
#: warehouse/templates/manage/project/roles.html:359
#: warehouse/templates/manage/project/roles.html:380
-#: warehouse/templates/manage/project/settings.html:319
-#: warehouse/templates/manage/project/settings.html:339
-#: warehouse/templates/manage/project/settings.html:359
+#: warehouse/templates/manage/project/settings.html:287
+#: warehouse/templates/manage/project/settings.html:307
+#: warehouse/templates/manage/project/settings.html:327
#: warehouse/templates/manage/team/roles.html:106
#: warehouse/templates/manage/team/settings.html:35
#: warehouse/templates/packaging/submit-malware-observation.html:58
@@ -1813,9 +1813,9 @@ msgstr ""
#: warehouse/templates/manage/project/history.html:312
#: warehouse/templates/manage/project/history.html:323
#: warehouse/templates/manage/project/history.html:334
-#: warehouse/templates/manage/project/settings.html:256
-#: warehouse/templates/manage/project/settings.html:317
-#: warehouse/templates/manage/project/settings.html:323
+#: warehouse/templates/manage/project/settings.html:224
+#: warehouse/templates/manage/project/settings.html:285
+#: warehouse/templates/manage/project/settings.html:291
#: warehouse/templates/manage/unverified-account.html:112
msgid "Name"
msgstr ""
@@ -2738,7 +2738,7 @@ msgstr ""
#: warehouse/templates/manage/manage_base.html:331
#: warehouse/templates/manage/project/release.html:137
#: warehouse/templates/manage/project/releases.html:178
-#: warehouse/templates/manage/project/settings.html:104
+#: warehouse/templates/manage/project/settings.html:72
#: warehouse/templates/manage/unverified-account.html:172
#: warehouse/templates/manage/unverified-account.html:174
#: warehouse/templates/manage/unverified-account.html:184
@@ -3395,12 +3395,12 @@ msgid "Update password"
msgstr ""
#: warehouse/templates/manage/account.html:472
-#: warehouse/templates/manage/project/settings.html:75
+#: warehouse/templates/manage/project/settings.html:43
msgid "API tokens"
msgstr ""
#: warehouse/templates/manage/account.html:473
-#: warehouse/templates/manage/project/settings.html:76
+#: warehouse/templates/manage/project/settings.html:44
msgid ""
"API tokens provide an alternative way to authenticate when uploading "
"packages to PyPI."
@@ -4607,7 +4607,7 @@ msgstr ""
#: warehouse/templates/manage/project/publishing.html:275
#: warehouse/templates/manage/project/publishing.html:357
#: warehouse/templates/manage/project/roles.html:341
-#: warehouse/templates/manage/project/settings.html:380
+#: warehouse/templates/manage/project/settings.html:348
#: warehouse/templates/manage/team/roles.html:131
msgid "Add"
msgstr ""
@@ -5666,7 +5666,7 @@ msgstr ""
#: warehouse/templates/manage/organization/roles.html:252
#: warehouse/templates/manage/project/release.html:106
#: warehouse/templates/manage/project/releases.html:109
-#: warehouse/templates/manage/project/settings.html:284
+#: warehouse/templates/manage/project/settings.html:252
msgid "Delete"
msgstr ""
@@ -6063,9 +6063,9 @@ msgstr ""
#: warehouse/templates/manage/project/history.html:313
#: warehouse/templates/manage/project/history.html:324
#: warehouse/templates/manage/project/history.html:335
-#: warehouse/templates/manage/project/settings.html:257
-#: warehouse/templates/manage/project/settings.html:337
-#: warehouse/templates/manage/project/settings.html:343
+#: warehouse/templates/manage/project/settings.html:225
+#: warehouse/templates/manage/project/settings.html:305
+#: warehouse/templates/manage/project/settings.html:311
msgid "Url"
msgstr ""
@@ -6273,7 +6273,7 @@ msgstr ""
#: warehouse/templates/manage/project/release.html:137
#: warehouse/templates/manage/project/releases.html:178
-#: warehouse/templates/manage/project/settings.html:104
+#: warehouse/templates/manage/project/settings.html:72
msgid "Dismiss"
msgstr ""
@@ -6616,51 +6616,23 @@ msgstr ""
msgid " (request an increase) "
msgstr ""
-#: warehouse/templates/manage/project/settings.html:43
-#: warehouse/templates/manage/project/settings.html:57
-#: warehouse/templates/manage/project/settings.html:65
-msgid "Archive project"
-msgstr ""
-
#: warehouse/templates/manage/project/settings.html:48
-msgid ""
-"Archiving a project will block any new uploads. Before doing so, we "
-"recommend publishing a final release with an update to the project's "
-"README to warn the users that the project won't receive further updates, "
-"and to mention any alternative projects they may consider as a "
-"replacement."
-msgstr ""
-
-#: warehouse/templates/manage/project/settings.html:60
-#: warehouse/templates/manage/project/settings.html:71
-msgid "Unarchive project"
-msgstr ""
-
-#: warehouse/templates/manage/project/settings.html:66
-msgid "Archiving a project will block any new file uploads"
-msgstr ""
-
-#: warehouse/templates/manage/project/settings.html:72
-msgid "Unarchiving a project will allow new file uploads"
-msgstr ""
-
-#: warehouse/templates/manage/project/settings.html:80
#, python-format
msgid "Create a token for %(project_name)s"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:85
+#: warehouse/templates/manage/project/settings.html:53
#, python-format
msgid ""
"Verify your primary email address to add an API "
"token for %(project_name)s."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:92
+#: warehouse/templates/manage/project/settings.html:60
msgid "Project description and sidebar"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:94
+#: warehouse/templates/manage/project/settings.html:62
#, python-format
msgid ""
"To set the '%(project_name)s' description, author, links, classifiers, "
@@ -6676,147 +6648,147 @@ msgid ""
"Python Packaging User Guide for more help."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:117
+#: warehouse/templates/manage/project/settings.html:85
msgid "Remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:120
+#: warehouse/templates/manage/project/settings.html:88
msgid "Removing this project from the organization will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:124
-#: warehouse/templates/manage/project/settings.html:174
+#: warehouse/templates/manage/project/settings.html:92
+#: warehouse/templates/manage/project/settings.html:142
#, python-format
msgid "Remove this project from the '%(organization_name)s' organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:127
-#: warehouse/templates/manage/project/settings.html:177
+#: warehouse/templates/manage/project/settings.html:95
+#: warehouse/templates/manage/project/settings.html:145
#, python-format
msgid ""
"Revoke project permissions for teams in the '%(organization_name)s' "
"organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:131
-#: warehouse/templates/manage/project/settings.html:137
+#: warehouse/templates/manage/project/settings.html:99
+#: warehouse/templates/manage/project/settings.html:105
msgid ""
"Individual owners and maintainers of the project will retain their "
"project permissions."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:136
+#: warehouse/templates/manage/project/settings.html:104
#, python-format
msgid ""
"This will remove the project from the '%(organization_name)s' "
"organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:140
+#: warehouse/templates/manage/project/settings.html:108
msgid "Remove project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:140
-#: warehouse/templates/manage/project/settings.html:211
-#: warehouse/templates/manage/project/settings.html:427
+#: warehouse/templates/manage/project/settings.html:108
+#: warehouse/templates/manage/project/settings.html:179
+#: warehouse/templates/manage/project/settings.html:432
msgid "Project Name"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:144
+#: warehouse/templates/manage/project/settings.html:112
msgid "Cannot remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:146
+#: warehouse/templates/manage/project/settings.html:114
msgid ""
"Your organization is currently the sole owner of the "
"project. You must add an individual owner to the project before you can "
"remove the project from your organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:162
+#: warehouse/templates/manage/project/settings.html:130
msgid "Transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:164
+#: warehouse/templates/manage/project/settings.html:132
msgid "Transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:169
+#: warehouse/templates/manage/project/settings.html:137
msgid "Transferring this project will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:181
+#: warehouse/templates/manage/project/settings.html:149
msgid "Revoke your direct Owner role on the project."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:184
+#: warehouse/templates/manage/project/settings.html:152
msgid ""
"You will retain Owner permissions on the project through your "
"organization role."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:189
+#: warehouse/templates/manage/project/settings.html:157
msgid "Add the project to another organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:191
+#: warehouse/templates/manage/project/settings.html:159
msgid "Add the project to an organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:195
+#: warehouse/templates/manage/project/settings.html:163
msgid "Grant full project permissions to owners of the organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:199
+#: warehouse/templates/manage/project/settings.html:167
msgid ""
"All other individual owners and maintainers of the project will retain "
"their project permissions."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:211
+#: warehouse/templates/manage/project/settings.html:179
msgid "Transfer project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:217
+#: warehouse/templates/manage/project/settings.html:185
msgid "Cannot transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:219
+#: warehouse/templates/manage/project/settings.html:187
msgid "Cannot transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:224
+#: warehouse/templates/manage/project/settings.html:192
msgid ""
"Organization owners can transfer the project to organizations that they "
"own or manage."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:225
+#: warehouse/templates/manage/project/settings.html:193
msgid "You are not an owner or manager of any other organizations."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:227
+#: warehouse/templates/manage/project/settings.html:195
msgid ""
"Project owners can transfer the project to organizations that they own or"
" manage."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:228
+#: warehouse/templates/manage/project/settings.html:196
msgid "You are not an owner or manager of any organizations."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:237
+#: warehouse/templates/manage/project/settings.html:205
msgid "Alternate repository locations"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:241
+#: warehouse/templates/manage/project/settings.html:209
#, python-format
msgid ""
"Provisional support for PEP 708 \"Alternate "
"Locations\" Metadata."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:245
+#: warehouse/templates/manage/project/settings.html:213
#, python-format
msgid ""
"Implementation may change, consider subscribing to configured to do so , you can "
+"update the project's description by editing the README file."
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:374
+#: warehouse/templates/manage/project/settings.html:385
+msgid "Unarchive project"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:380
+msgid "Archiving a project will block any new file uploads"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:386
+msgid "Unarchiving a project will allow new file uploads"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:392
+#: warehouse/templates/manage/project/settings.html:432
msgid "Delete project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:390
+#: warehouse/templates/manage/project/settings.html:395
msgid "Deleting this project will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:395
+#: warehouse/templates/manage/project/settings.html:400
#, python-format
msgid ""
"Irreversibly delete the project along with %(count)s"
@@ -6883,15 +6886,15 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: warehouse/templates/manage/project/settings.html:401
+#: warehouse/templates/manage/project/settings.html:406
msgid "Irreversibly delete the project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:405
+#: warehouse/templates/manage/project/settings.html:410
msgid "Make the project name available to any other PyPI user"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:407
+#: warehouse/templates/manage/project/settings.html:412
msgid ""
"This user will be able to make new releases under this project name, so "
"long as the distribution filenames do not match filenames from a "
diff --git a/warehouse/templates/manage/project/settings.html b/warehouse/templates/manage/project/settings.html
index 79351180ecb3..00fa67bb1283 100644
--- a/warehouse/templates/manage/project/settings.html
+++ b/warehouse/templates/manage/project/settings.html
@@ -40,38 +40,6 @@ {% trans %}Project settings{% endtrans %}
(request an increase) {% endtrans %}
- {% trans %}Archive project{% endtrans %}
- {% set can_be_archived = not project.lifecycle_status or project.lifecycle_status == "quarantine-exit" %}
- {% set can_be_unarchived = project.lifecycle_status == "archived" %}
-
-
- {% trans %}
- Archiving a project will block any new uploads. Before doing so, we
- recommend publishing a final release with an update to the project's
- README to warn the users that the project won't receive further updates,
- and to mention any alternative projects they may consider as a replacement.
- {% endtrans %}
-
-
-
- {% trans %}Archive project{% endtrans %}
-
-
- {% trans %}Unarchive project{% endtrans %}
-
-
- {% set action = request.route_path('manage.project.archive', project_name=project.name) %}
- {% set slug = "archive-project" %}
- {% set title = gettext("Archive project") %}
- {% set extra_description = gettext("Archiving a project will block any new file uploads") %}
- {{ confirm_modal(title=title, label=project.name, slug=slug, extra_description=extra_description, action=action, warning=False) }}
-
- {% set action = request.route_path('manage.project.unarchive', project_name=project.name) %}
- {% set slug = "unarchive-project" %}
- {% set title = gettext("Unarchive project") %}
- {% set extra_description = gettext("Unarchiving a project will allow new file uploads") %}
- {{ confirm_modal(title=title, label=project.name, slug=slug, extra_description=extra_description, action=action, warning=False) }}
-
{% trans %}API tokens{% endtrans %}
{% trans %}API tokens provide an alternative way to authenticate when uploading packages to PyPI.{% endtrans %}
{% if user.has_primary_verified_email %}
@@ -383,6 +351,43 @@
+ {% trans %}Archive project{% endtrans %}
+ {% set can_be_archived = not project.lifecycle_status or project.lifecycle_status == "quarantine-exit" %}
+ {% set can_be_unarchived = project.lifecycle_status == "archived" %}
+
+
+ {% trans readme_description_href='https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/'%}
+ Archiving a project will prevent any new uploads. Before doing so, we
+ recommend publishing a final release with an update to the project's
+ description to warn the users that the project won't receive further updates,
+ and to mention any alternative projects they may consider as a replacement.
+ If your project is
+ configured
+ to do so , you can update the project's description by editing the README file.
+ {% endtrans %}
+
+
+
+ {% trans %}Archive project{% endtrans %}
+
+
+ {% trans %}Unarchive project{% endtrans %}
+
+
+ {% set action = request.route_path('manage.project.archive', project_name=project.name) %}
+ {% set slug = "archive-project" %}
+ {% set title = gettext("Archive project") %}
+ {% set extra_description = gettext("Archiving a project will block any new file uploads") %}
+ {{ confirm_modal(title=title, label=project.name, slug=slug, extra_description=extra_description, action=action, warning=False) }}
+
+ {% set action = request.route_path('manage.project.unarchive', project_name=project.name) %}
+ {% set slug = "unarchive-project" %}
+ {% set title = gettext("Unarchive project") %}
+ {% set extra_description = gettext("Unarchiving a project will allow new file uploads") %}
+ {{ confirm_modal(title=title, label=project.name, slug=slug, extra_description=extra_description, action=action, warning=False) }}
+
+
+
{% trans %}Delete project{% endtrans %}
From c100cec4ceca41cddac6e265aa8a632520275409 Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Thu, 16 Jan 2025 18:44:44 +0100
Subject: [PATCH 9/9] Apply suggestions from code review
Co-authored-by: Mike Fiedler
---
.../versions/12a43f12cc18_add_new_lifecycle_statuses.py | 2 +-
warehouse/templates/manage/project/settings.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py b/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py
index 3b622cfd7a26..b1601a763c2d 100644
--- a/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py
+++ b/warehouse/migrations/versions/12a43f12cc18_add_new_lifecycle_statuses.py
@@ -13,7 +13,7 @@
Add new lifecycle statuses
Revision ID: 12a43f12cc18
-Revises: e24aa37164e72
+Revises: 24aa37164e72
"""
from alembic import op
diff --git a/warehouse/templates/manage/project/settings.html b/warehouse/templates/manage/project/settings.html
index 00fa67bb1283..40d6201a500b 100644
--- a/warehouse/templates/manage/project/settings.html
+++ b/warehouse/templates/manage/project/settings.html
@@ -356,7 +356,7 @@ {% trans %}Archive project{% endtrans %}
{% set can_be_unarchived = project.lifecycle_status == "archived" %}
- {% trans readme_description_href='https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/'%}
+ {% trans readme_description_href='https://packaging.python.org/guides/making-a-pypi-friendly-readme/'%}
Archiving a project will prevent any new uploads. Before doing so, we
recommend publishing a final release with an update to the project's
description to warn the users that the project won't receive further updates,