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

+
+
+

Archive project

+
+ +
+
+ + +
+ {% 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 @@ + {% elif project.lifecycle_status == 'archived' %} +
+
+

+ Archived +

+
+ + + +
+
+
+

+ This project is archived. + It should not allow any new uploads unless it's unarchived. +

+
+
+ + {% endif %}
@@ -599,13 +620,13 @@

Archive project

- +
-
+
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,