From f22daf04a2d1022e19546bad431a2c8e0d2c3c89 Mon Sep 17 00:00:00 2001
From: Facundo Tuesca
Date: Wed, 30 Oct 2024 19:22:12 +0100
Subject: [PATCH] feat: allow marking projects as archived
Signed-off-by: Facundo Tuesca
---
tests/unit/manage/test_views.py | 82 +++-
warehouse/locale/messages.pot | 356 ++++++++++--------
warehouse/manage/forms.py | 16 +
warehouse/manage/views/__init__.py | 45 +++
...12a43f12cc18_add_new_lifecycle_statuses.py | 55 +++
warehouse/packaging/models.py | 1 +
.../templates/manage/project/settings.html | 18 +
warehouse/templates/packaging/detail.html | 10 +
8 files changed, 417 insertions(+), 166 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 fb16a5a325ec..e8857f1e2d36 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,
@@ -96,7 +97,6 @@
class TestManageUnverifiedAccount:
-
def test_manage_account(self, monkeypatch):
user_service = pretend.stub()
name = pretend.stub()
@@ -2603,11 +2603,12 @@ 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
view.add_alternate_repository_form_class = lambda *a, **kw: form
+ view.set_project_status_form_class = lambda *a, **kw: form
user_organizations = pretend.call_recorder(
lambda *a, **kw: {
@@ -2622,21 +2623,91 @@ def test_manage_project_settings(self, enabled, monkeypatch):
"project": project,
"MAX_FILESIZE": MAX_FILESIZE,
"MAX_PROJECT_SIZE": MAX_PROJECT_SIZE,
+ "set_project_status_form": form,
"transfer_organization_project_form": form,
"add_alternate_repository_form_class": form,
}
+ def test_archive_project(self, db_request):
+ project = ProjectFactory.create(name="foo")
+
+ db_request.POST = MultiDict({"project_status": "archived"})
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ settings_views = views.ManageProjectSettingsViews(project, db_request)
+ result = settings_views.set_project_status()
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call("Set project status to 'archived'", queue="success")
+ ]
+ assert db_request.route_path.calls == [
+ pretend.call("manage.project.settings", project_name="foo")
+ ]
+ assert project.lifecycle_status == LifecycleStatus.Archived
+
+ def test_unarchive_project(self, db_request):
+ project = ProjectFactory.create(
+ name="foo", lifecycle_status=LifecycleStatus.Archived
+ )
+
+ db_request.POST = MultiDict({"project_status": ""})
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ settings_views = views.ManageProjectSettingsViews(project, db_request)
+ result = settings_views.set_project_status()
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call("Set project status to 'None'", queue="success")
+ ]
+ assert db_request.route_path.calls == [
+ pretend.call("manage.project.settings", project_name="foo")
+ ]
+ assert project.lifecycle_status is None
+
+ def test_disallowed_lifecycle_status_change(self, db_request):
+ project = ProjectFactory.create(name="foo", lifecycle_status="quarantine-enter")
+
+ db_request.POST = MultiDict({"project_status": "quarantine-exit"})
+ db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/the-redirect")
+ db_request.session = pretend.stub(
+ flash=pretend.call_recorder(lambda *a, **kw: None)
+ )
+
+ settings_views = views.ManageProjectSettingsViews(project, db_request)
+ result = settings_views.set_project_status()
+
+ assert isinstance(result, HTTPSeeOther)
+ assert result.headers["Location"] == "/the-redirect"
+ assert db_request.session.flash.calls == [
+ pretend.call("Invalid project 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_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(
lambda *a, **kw: form
)
view.add_alternate_repository_form_class = lambda *a, **kw: form
+ view.set_project_status_form_class = lambda *a, **kw: form
user_organizations = pretend.call_recorder(
lambda *a, **kw: {
@@ -2651,6 +2722,7 @@ def test_manage_project_settings_in_organization_managed(self, monkeypatch):
"project": project,
"MAX_FILESIZE": MAX_FILESIZE,
"MAX_PROJECT_SIZE": MAX_PROJECT_SIZE,
+ "set_project_status_form": form,
"transfer_organization_project_form": form,
"add_alternate_repository_form_class": form,
}
@@ -2662,13 +2734,14 @@ 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(
lambda *a, **kw: form
)
view.add_alternate_repository_form_class = lambda *a, **kw: form
+ view.set_project_status_form_class = lambda *a, **kw: form
user_organizations = pretend.call_recorder(
lambda *a, **kw: {
@@ -2683,6 +2756,7 @@ def test_manage_project_settings_in_organization_owned(self, monkeypatch):
"project": project,
"MAX_FILESIZE": MAX_FILESIZE,
"MAX_PROJECT_SIZE": MAX_PROJECT_SIZE,
+ "set_project_status_form": form,
"transfer_organization_project_form": form,
"add_alternate_repository_form_class": form,
}
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index 4d8141199bbe..a0581b98916b 100644
--- a/warehouse/locale/messages.pot
+++ b/warehouse/locale/messages.pot
@@ -90,8 +90,8 @@ msgid ""
"different email."
msgstr ""
-#: warehouse/accounts/forms.py:410 warehouse/manage/forms.py:139
-#: warehouse/manage/forms.py:730
+#: warehouse/accounts/forms.py:410 warehouse/manage/forms.py:140
+#: warehouse/manage/forms.py:746
msgid "The name is too long. Choose a name with 100 characters or less."
msgstr ""
@@ -152,7 +152,7 @@ msgstr ""
msgid "Successful WebAuthn assertion"
msgstr ""
-#: warehouse/accounts/views.py:569 warehouse/manage/views/__init__.py:873
+#: warehouse/accounts/views.py:569 warehouse/manage/views/__init__.py:874
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:1548 warehouse/accounts/views.py:1791
-#: warehouse/manage/views/__init__.py:1417
+#: warehouse/manage/views/__init__.py:1462
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:1614 warehouse/manage/views/__init__.py:1472
-#: warehouse/manage/views/__init__.py:1585
-#: warehouse/manage/views/__init__.py:1697
-#: warehouse/manage/views/__init__.py:1807
+#: warehouse/accounts/views.py:1614 warehouse/manage/views/__init__.py:1517
+#: warehouse/manage/views/__init__.py:1630
+#: warehouse/manage/views/__init__.py:1742
+#: warehouse/manage/views/__init__.py:1852
msgid ""
"There have been too many attempted trusted publisher registrations. Try "
"again later."
msgstr ""
-#: warehouse/accounts/views.py:1625 warehouse/manage/views/__init__.py:1486
-#: warehouse/manage/views/__init__.py:1599
-#: warehouse/manage/views/__init__.py:1711
-#: warehouse/manage/views/__init__.py:1821
+#: warehouse/accounts/views.py:1625 warehouse/manage/views/__init__.py:1531
+#: warehouse/manage/views/__init__.py:1644
+#: warehouse/manage/views/__init__.py:1756
+#: warehouse/manage/views/__init__.py:1866
msgid "The trusted publisher could not be registered"
msgstr ""
@@ -345,11 +345,11 @@ msgstr ""
msgid "Banner Preview"
msgstr ""
-#: warehouse/manage/forms.py:408
+#: warehouse/manage/forms.py:409
msgid "Choose an organization account name with 50 characters or less."
msgstr ""
-#: warehouse/manage/forms.py:416
+#: warehouse/manage/forms.py:417
msgid ""
"The organization account name is invalid. Organization account names must"
" be composed of letters, numbers, dots, hyphens and underscores. And must"
@@ -357,246 +357,254 @@ msgid ""
"organization account name."
msgstr ""
-#: warehouse/manage/forms.py:439
+#: warehouse/manage/forms.py:440
msgid ""
"This organization account name has already been used. Choose a different "
"organization account name."
msgstr ""
-#: warehouse/manage/forms.py:454
+#: warehouse/manage/forms.py:455
msgid ""
"You have already submitted an application for that name. Choose a "
"different organization account name."
msgstr ""
-#: warehouse/manage/forms.py:490
+#: warehouse/manage/forms.py:491
msgid "Select project"
msgstr ""
-#: warehouse/manage/forms.py:495 warehouse/oidc/forms/_core.py:22
+#: warehouse/manage/forms.py:496 warehouse/oidc/forms/_core.py:22
#: warehouse/oidc/forms/gitlab.py:43
msgid "Specify project name"
msgstr ""
-#: warehouse/manage/forms.py:498
+#: warehouse/manage/forms.py:499
msgid ""
"Start and end with a letter or numeral containing only ASCII numeric and "
"'.', '_' and '-'."
msgstr ""
-#: warehouse/manage/forms.py:505
+#: warehouse/manage/forms.py:506
msgid "This project name has already been used. Choose a different project name."
msgstr ""
-#: warehouse/manage/forms.py:578
+#: warehouse/manage/forms.py:579
msgid ""
"The organization name is too long. Choose a organization name with 100 "
"characters or less."
msgstr ""
-#: warehouse/manage/forms.py:590
+#: warehouse/manage/forms.py:591
msgid ""
"The organization URL is too long. Choose a organization URL with 400 "
"characters or less."
msgstr ""
-#: warehouse/manage/forms.py:597
+#: warehouse/manage/forms.py:598
msgid "The organization URL must start with http:// or https://"
msgstr ""
-#: warehouse/manage/forms.py:608
+#: warehouse/manage/forms.py:609
msgid ""
"The organization description is too long. Choose a organization "
"description with 400 characters or less."
msgstr ""
-#: warehouse/manage/forms.py:643
+#: warehouse/manage/forms.py:644
msgid "You have already submitted the maximum number of "
msgstr ""
-#: warehouse/manage/forms.py:673
+#: warehouse/manage/forms.py:674
msgid "Choose a team name with 50 characters or less."
msgstr ""
-#: warehouse/manage/forms.py:679
+#: warehouse/manage/forms.py:680
msgid ""
"The team name is invalid. Team names cannot start or end with a space, "
"period, underscore, hyphen, or slash. Choose a different team name."
msgstr ""
-#: warehouse/manage/forms.py:707
+#: warehouse/manage/forms.py:708
msgid "This team name has already been used. Choose a different team name."
msgstr ""
-#: warehouse/manage/forms.py:726
+#: warehouse/manage/forms.py:742
msgid "Specify your alternate repository name"
msgstr ""
-#: warehouse/manage/forms.py:740
+#: warehouse/manage/forms.py:756
msgid "Specify your alternate repository URL"
msgstr ""
-#: warehouse/manage/forms.py:744
+#: warehouse/manage/forms.py:760
msgid "The URL is too long. Choose a URL with 400 characters or less."
msgstr ""
-#: warehouse/manage/forms.py:758
+#: warehouse/manage/forms.py:774
msgid ""
"The description is too long. Choose a description with 400 characters or "
"less."
msgstr ""
-#: warehouse/manage/views/__init__.py:285
+#: warehouse/manage/views/__init__.py:286
msgid "Account details updated"
msgstr ""
-#: warehouse/manage/views/__init__.py:315
+#: warehouse/manage/views/__init__.py:316
msgid "Email ${email_address} added - check your email for a verification link"
msgstr ""
-#: warehouse/manage/views/__init__.py:821
+#: warehouse/manage/views/__init__.py:822
msgid "Recovery codes already generated"
msgstr ""
-#: warehouse/manage/views/__init__.py:822
+#: warehouse/manage/views/__init__.py:823
msgid "Generating new recovery codes will invalidate your existing codes."
msgstr ""
-#: warehouse/manage/views/__init__.py:931
+#: warehouse/manage/views/__init__.py:932
msgid "Verify your email to create an API token."
msgstr ""
-#: warehouse/manage/views/__init__.py:1031
+#: warehouse/manage/views/__init__.py:1032
msgid "API Token does not exist."
msgstr ""
-#: warehouse/manage/views/__init__.py:1063
+#: warehouse/manage/views/__init__.py:1064
msgid "Invalid credentials. Try again"
msgstr ""
-#: warehouse/manage/views/__init__.py:1182
+#: warehouse/manage/views/__init__.py:1188
+msgid "Invalid project status"
+msgstr ""
+
+#: warehouse/manage/views/__init__.py:1201
+msgid "Set project status to '${status_name}'"
+msgstr ""
+
+#: warehouse/manage/views/__init__.py:1227
msgid "Invalid alternate repository location details"
msgstr ""
-#: warehouse/manage/views/__init__.py:1219
+#: warehouse/manage/views/__init__.py:1264
msgid "Added alternate repository '${name}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:1253
-#: warehouse/manage/views/__init__.py:2154
-#: warehouse/manage/views/__init__.py:2239
-#: warehouse/manage/views/__init__.py:2340
-#: warehouse/manage/views/__init__.py:2440
+#: warehouse/manage/views/__init__.py:1298
+#: warehouse/manage/views/__init__.py:2199
+#: warehouse/manage/views/__init__.py:2284
+#: warehouse/manage/views/__init__.py:2385
+#: warehouse/manage/views/__init__.py:2485
msgid "Confirm the request"
msgstr ""
-#: warehouse/manage/views/__init__.py:1265
+#: warehouse/manage/views/__init__.py:1310
msgid "Invalid alternate repository id"
msgstr ""
-#: warehouse/manage/views/__init__.py:1276
+#: warehouse/manage/views/__init__.py:1321
msgid "Invalid alternate repository for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:1284
+#: warehouse/manage/views/__init__.py:1329
msgid ""
"Could not delete alternate repository - ${confirm} is not the same as "
"${alt_repo_name}"
msgstr ""
-#: warehouse/manage/views/__init__.py:1322
+#: warehouse/manage/views/__init__.py:1367
msgid "Deleted alternate repository '${name}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:1453
+#: warehouse/manage/views/__init__.py:1498
msgid ""
"GitHub-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1566
+#: warehouse/manage/views/__init__.py:1611
msgid ""
"GitLab-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1678
+#: warehouse/manage/views/__init__.py:1723
msgid ""
"Google-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:1787
+#: warehouse/manage/views/__init__.py:1832
msgid ""
"ActiveState-based trusted publishing is temporarily disabled. See "
"https://pypi.org/help#admin-intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:2022
-#: warehouse/manage/views/__init__.py:2323
-#: warehouse/manage/views/__init__.py:2431
+#: warehouse/manage/views/__init__.py:2067
+#: warehouse/manage/views/__init__.py:2368
+#: warehouse/manage/views/__init__.py:2476
msgid ""
"Project deletion temporarily disabled. See https://pypi.org/help#admin-"
"intervention for details."
msgstr ""
-#: warehouse/manage/views/__init__.py:2166
+#: warehouse/manage/views/__init__.py:2211
msgid "Could not yank release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2251
+#: warehouse/manage/views/__init__.py:2296
msgid "Could not un-yank release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2352
+#: warehouse/manage/views/__init__.py:2397
msgid "Could not delete release - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2452
+#: warehouse/manage/views/__init__.py:2497
msgid "Could not find file"
msgstr ""
-#: warehouse/manage/views/__init__.py:2456
+#: warehouse/manage/views/__init__.py:2501
msgid "Could not delete file - "
msgstr ""
-#: warehouse/manage/views/__init__.py:2606
+#: warehouse/manage/views/__init__.py:2651
msgid "Team '${team_name}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2713
+#: warehouse/manage/views/__init__.py:2758
msgid "User '${username}' already has ${role_name} role for project"
msgstr ""
-#: warehouse/manage/views/__init__.py:2780
+#: warehouse/manage/views/__init__.py:2825
msgid "${username} is now ${role} of the '${project_name}' project."
msgstr ""
-#: warehouse/manage/views/__init__.py:2812
+#: warehouse/manage/views/__init__.py:2857
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:2825
+#: warehouse/manage/views/__init__.py:2870
#: warehouse/manage/views/organizations.py:878
msgid "User '${username}' already has an active invite. Please try again later."
msgstr ""
-#: warehouse/manage/views/__init__.py:2890
+#: warehouse/manage/views/__init__.py:2935
#: warehouse/manage/views/organizations.py:943
msgid "Invitation sent to '${username}'"
msgstr ""
-#: warehouse/manage/views/__init__.py:2923
+#: warehouse/manage/views/__init__.py:2968
msgid "Could not find role invitation."
msgstr ""
-#: warehouse/manage/views/__init__.py:2934
+#: warehouse/manage/views/__init__.py:2979
msgid "Invitation already expired."
msgstr ""
-#: warehouse/manage/views/__init__.py:2966
+#: warehouse/manage/views/__init__.py:3011
#: warehouse/manage/views/organizations.py:1130
msgid "Invitation revoked from '${username}'."
msgstr ""
@@ -840,8 +848,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
@@ -1078,9 +1086,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:105
+#: warehouse/templates/manage/project/settings.html:154
+#: warehouse/templates/manage/project/settings.html:375
#: warehouse/templates/manage/team/settings.html:84
msgid "Warning"
msgstr ""
@@ -1446,9 +1454,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:305
+#: warehouse/templates/manage/project/settings.html:325
+#: warehouse/templates/manage/project/settings.html:345
#: warehouse/templates/manage/team/roles.html:106
#: warehouse/templates/manage/team/settings.html:35
#: warehouse/templates/packaging/submit-malware-observation.html:58
@@ -1742,9 +1750,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:242
+#: warehouse/templates/manage/project/settings.html:303
+#: warehouse/templates/manage/project/settings.html:309
#: warehouse/templates/manage/unverified-account.html:112
msgid "Name"
msgstr ""
@@ -2667,7 +2675,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:90
#: warehouse/templates/manage/unverified-account.html:172
#: warehouse/templates/manage/unverified-account.html:174
#: warehouse/templates/manage/unverified-account.html:184
@@ -3324,12 +3332,12 @@ msgid "Update password"
msgstr ""
#: warehouse/templates/manage/account.html:472
-#: warehouse/templates/manage/project/settings.html:43
+#: warehouse/templates/manage/project/settings.html:61
msgid "API tokens"
msgstr ""
#: warehouse/templates/manage/account.html:473
-#: warehouse/templates/manage/project/settings.html:44
+#: warehouse/templates/manage/project/settings.html:62
msgid ""
"API tokens provide an alternative way to authenticate when uploading "
"packages to PyPI."
@@ -4536,7 +4544,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:366
#: warehouse/templates/manage/team/roles.html:131
msgid "Add"
msgstr ""
@@ -5584,7 +5592,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:270
msgid "Delete"
msgstr ""
@@ -5981,9 +5989,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:243
+#: warehouse/templates/manage/project/settings.html:323
+#: warehouse/templates/manage/project/settings.html:329
msgid "Url"
msgstr ""
@@ -6162,7 +6170,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:90
msgid "Dismiss"
msgstr ""
@@ -6505,23 +6513,37 @@ msgstr ""
msgid " (request an increase) "
msgstr ""
-#: warehouse/templates/manage/project/settings.html:48
+#: warehouse/templates/manage/project/settings.html:43
+msgid "Project status"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:44
+msgid ""
+"The project status indicates what users should expect in terms of active "
+"development and mainteinance."
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:58
+msgid "Set status"
+msgstr ""
+
+#: warehouse/templates/manage/project/settings.html:66
#, python-format
msgid "Create a token for %(project_name)s"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:53
+#: warehouse/templates/manage/project/settings.html:71
#, 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:78
msgid "Project description and sidebar"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:62
+#: warehouse/templates/manage/project/settings.html:80
#, python-format
msgid ""
"To set the '%(project_name)s' description, author, links, classifiers, "
@@ -6537,147 +6559,147 @@ msgid ""
"Python Packaging User Guide for more help."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:85
+#: warehouse/templates/manage/project/settings.html:103
msgid "Remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:88
+#: warehouse/templates/manage/project/settings.html:106
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:110
+#: warehouse/templates/manage/project/settings.html:160
#, 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:113
+#: warehouse/templates/manage/project/settings.html:163
#, 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:117
+#: warehouse/templates/manage/project/settings.html:123
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:122
#, 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:126
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:126
+#: warehouse/templates/manage/project/settings.html:197
+#: warehouse/templates/manage/project/settings.html:413
msgid "Project Name"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:112
+#: warehouse/templates/manage/project/settings.html:130
msgid "Cannot remove project from organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:114
+#: warehouse/templates/manage/project/settings.html:132
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:148
msgid "Transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:132
+#: warehouse/templates/manage/project/settings.html:150
msgid "Transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:137
+#: warehouse/templates/manage/project/settings.html:155
msgid "Transferring this project will:"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:149
+#: warehouse/templates/manage/project/settings.html:167
msgid "Revoke your direct Owner role on the project."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:152
+#: warehouse/templates/manage/project/settings.html:170
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:175
msgid "Add the project to another organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:159
+#: warehouse/templates/manage/project/settings.html:177
msgid "Add the project to an organization that you own."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:163
+#: warehouse/templates/manage/project/settings.html:181
msgid "Grant full project permissions to owners of the organization."
msgstr ""
-#: warehouse/templates/manage/project/settings.html:167
+#: warehouse/templates/manage/project/settings.html:185
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:197
msgid "Transfer project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:185
+#: warehouse/templates/manage/project/settings.html:203
msgid "Cannot transfer project to another organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:187
+#: warehouse/templates/manage/project/settings.html:205
msgid "Cannot transfer project to an organization"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:192
+#: warehouse/templates/manage/project/settings.html:210
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:211
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:213
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:214
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:223
msgid "Alternate repository locations"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:209
+#: warehouse/templates/manage/project/settings.html:227
#, 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:231
#, python-format
msgid ""
"Implementation may change, consider subscribing to %(count)s"
@@ -6744,15 +6766,15 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: warehouse/templates/manage/project/settings.html:369
+#: warehouse/templates/manage/project/settings.html:387
msgid "Irreversibly delete the project"
msgstr ""
-#: warehouse/templates/manage/project/settings.html:373
+#: warehouse/templates/manage/project/settings.html:391
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:393
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 "
@@ -6942,7 +6964,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 ""
@@ -6953,7 +6975,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 ""
@@ -6964,7 +6986,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 ""
@@ -6973,40 +6995,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, "
@@ -7014,24 +7046,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/forms.py b/warehouse/manage/forms.py
index 58539de2c435..5b90238b3a21 100644
--- a/warehouse/manage/forms.py
+++ b/warehouse/manage/forms.py
@@ -31,6 +31,7 @@
OrganizationType,
TeamProjectRoleType,
)
+from warehouse.packaging.models import LifecycleStatus
from warehouse.utils.project import PROJECT_NAME_RE
# /manage/account/ forms
@@ -715,6 +716,21 @@ class CreateTeamForm(SaveTeamForm):
__params__ = SaveTeamForm.__params__
+class SetProjectStatusForm(wtforms.Form):
+ """Form to set a project's status."""
+
+ __params__ = ["project_status"]
+
+ project_status = wtforms.SelectField(
+ "Select status",
+ choices=[
+ ("", "(no status)"),
+ (LifecycleStatus.Archived, "Archived"),
+ ],
+ coerce=lambda string: LifecycleStatus(string) if string else None,
+ )
+
+
class AddAlternateRepositoryForm(wtforms.Form):
"""Form to add an Alternate Repository Location for a Project."""
diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py
index a5b176807cb8..5d2a88e9e6d8 100644
--- a/warehouse/manage/views/__init__.py
+++ b/warehouse/manage/views/__init__.py
@@ -90,6 +90,7 @@
ProvisionTOTPForm,
ProvisionWebAuthnForm,
SaveAccountForm,
+ SetProjectStatusForm,
TransferOrganizationProjectForm,
)
from warehouse.manage.views.organizations import (
@@ -1125,6 +1126,7 @@ class ManageProjectSettingsViews:
def __init__(self, project, request):
self.project = project
self.request = request
+ self.set_project_status_form_class = SetProjectStatusForm
self.transfer_organization_project_form_class = TransferOrganizationProjectForm
self.add_alternate_repository_form_class = AddAlternateRepositoryForm
@@ -1154,11 +1156,17 @@ def manage_project_settings(self):
) - current_organization
add_alt_repo_form = self.add_alternate_repository_form_class()
+ set_project_status_form = self.set_project_status_form_class(
+ project_status=(
+ self.project.lifecycle_status if self.project.lifecycle_status else ""
+ )
+ )
return {
"project": self.project,
"MAX_FILESIZE": MAX_FILESIZE,
"MAX_PROJECT_SIZE": MAX_PROJECT_SIZE,
+ "set_project_status_form": set_project_status_form,
"transfer_organization_project_form": (
self.transfer_organization_project_form_class(
organization_choices=organization_choices,
@@ -1167,6 +1175,43 @@ def manage_project_settings(self):
"add_alternate_repository_form_class": add_alt_repo_form,
}
+ @view_config(
+ request_method="POST",
+ request_param=SetProjectStatusForm.__params__,
+ require_reauth=True,
+ permission=Permissions.ProjectsWrite,
+ )
+ def set_project_status(self):
+ form = self.set_project_status_form_class(self.request.POST)
+ if not form.validate():
+ self.request.session.flash(
+ self.request._("Invalid project status"),
+ queue="error",
+ )
+ return HTTPSeeOther(
+ self.request.route_path(
+ "manage.project.settings",
+ project_name=self.project.name,
+ )
+ )
+
+ self.project.lifecycle_status = form.project_status.data
+
+ self.request.session.flash(
+ self.request._(
+ "Set project status to '${status_name}'",
+ mapping={"status_name": form.project_status.data},
+ ),
+ queue="success",
+ )
+
+ return HTTPSeeOther(
+ self.request.route_path(
+ "manage.project.settings",
+ project_name=self.project.name,
+ )
+ )
+
@view_config(
request_method="POST",
request_param=AddAlternateRepositoryForm.__params__
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..30415d7f47b3
--- /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: 6ee23f5a6c1b
+"""
+
+from alembic import op
+from alembic_postgresql_enum import TableReference
+
+revision = "12a43f12cc18"
+down_revision = "6ee23f5a6c1b"
+
+
+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 e0c27e9bf0b2..ec37fb38cee1 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/templates/manage/project/settings.html b/warehouse/templates/manage/project/settings.html
index 609b684067f6..9c374e30542c 100644
--- a/warehouse/templates/manage/project/settings.html
+++ b/warehouse/templates/manage/project/settings.html
@@ -40,6 +40,24 @@ {% trans %}Project settings{% endtrans %}
(request an increase) {% endtrans %}
+ {% trans %}Project status {% endtrans %}
+ {% trans %}The project status indicates what users should expect in terms of active development and mainteinance.{% endtrans %}
+
+
{% 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 %}