diff --git a/docs/api-reference/json.rst b/docs/api-reference/json.rst index 1a0564f15331..56c6f5ebb322 100644 --- a/docs/api-reference/json.rst +++ b/docs/api-reference/json.rst @@ -72,7 +72,8 @@ Project "requires_dist": null, "requires_python": null, "summary": "A sample Python project", - "version": "1.2.0" + "version": "1.2.0", + "yanked": false }, "last_serial": 1591652, "releases": { @@ -92,7 +93,8 @@ Project "python_version": "2.7", "size": 3795, "upload_time_iso_8601": "2015-06-14T14:38:05.093750Z", - "url": "https://files.pythonhosted.org/packages/30/52/547eb3719d0e872bdd6fe3ab60cef92596f95262e925e1943f68f840df88/sampleproject-1.2.0-py2.py3-none-any.whl" + "url": "https://files.pythonhosted.org/packages/30/52/547eb3719d0e872bdd6fe3ab60cef92596f95262e925e1943f68f840df88/sampleproject-1.2.0-py2.py3-none-any.whl", + "yanked": false }, { "comment_text": "", @@ -108,7 +110,8 @@ Project "python_version": "source", "size": 3148, "upload_time_iso_8601": "2015-06-14T14:37:56Z", - "url": "https://files.pythonhosted.org/packages/eb/45/79be82bdeafcecb9dca474cad4003e32ef8e4a0dec6abbd4145ccb02abe1/sampleproject-1.2.0.tar.gz" + "url": "https://files.pythonhosted.org/packages/eb/45/79be82bdeafcecb9dca474cad4003e32ef8e4a0dec6abbd4145ccb02abe1/sampleproject-1.2.0.tar.gz", + "yanked": false } ] }, @@ -127,7 +130,8 @@ Project "python_version": "2.7", "size": 3795, "upload_time_iso_8601": "2015-06-14T14:38:05.234526", - "url": "https://files.pythonhosted.org/packages/30/52/547eb3719d0e872bdd6fe3ab60cef92596f95262e925e1943f68f840df88/sampleproject-1.2.0-py2.py3-none-any.whl" + "url": "https://files.pythonhosted.org/packages/30/52/547eb3719d0e872bdd6fe3ab60cef92596f95262e925e1943f68f840df88/sampleproject-1.2.0-py2.py3-none-any.whl", + "yanked": false }, { "comment_text": "", @@ -143,7 +147,8 @@ Project "python_version": "source", "size": 3148, "upload_time_iso_8601": "2015-06-14T14:37:56.000001Z", - "url": "https://files.pythonhosted.org/packages/eb/45/79be82bdeafcecb9dca474cad4003e32ef8e4a0dec6abbd4145ccb02abe1/sampleproject-1.2.0.tar.gz" + "url": "https://files.pythonhosted.org/packages/eb/45/79be82bdeafcecb9dca474cad4003e32ef8e4a0dec6abbd4145ccb02abe1/sampleproject-1.2.0.tar.gz", + "yanked": false } ] } @@ -201,7 +206,8 @@ Release "requires_dist": null, "requires_python": null, "summary": "", - "version": "1.0" + "version": "1.0", + "yanked": false }, "last_serial": 1591652, "releases": { @@ -221,7 +227,8 @@ Release "python_version": "2.7", "size": 3795, "upload_time_iso_8601": "2015-06-14T14:38:05.869374Z", - "url": "https://files.pythonhosted.org/packages/30/52/547eb3719d0e872bdd6fe3ab60cef92596f95262e925e1943f68f840df88/sampleproject-1.2.0-py2.py3-none-any.whl" + "url": "https://files.pythonhosted.org/packages/30/52/547eb3719d0e872bdd6fe3ab60cef92596f95262e925e1943f68f840df88/sampleproject-1.2.0-py2.py3-none-any.whl", + "yanked": false }, { "comment_text": "", @@ -237,7 +244,8 @@ Release "python_version": "source", "size": 3148, "upload_time_iso_8601": "2015-06-14T14:37:56.394783Z", - "url": "https://files.pythonhosted.org/packages/eb/45/79be82bdeafcecb9dca474cad4003e32ef8e4a0dec6abbd4145ccb02abe1/sampleproject-1.2.0.tar.gz" + "url": "https://files.pythonhosted.org/packages/eb/45/79be82bdeafcecb9dca474cad4003e32ef8e4a0dec6abbd4145ccb02abe1/sampleproject-1.2.0.tar.gz", + "yanked": false } ] }, diff --git a/tests/unit/email/test_init.py b/tests/unit/email/test_init.py index 069293b9ac2d..4a7b6921d390 100644 --- a/tests/unit/email/test_init.py +++ b/tests/unit/email/test_init.py @@ -1280,6 +1280,426 @@ def test_removed_project_email_to_owner( ] +class TestYankedReleaseEmail: + def test_send_yanked_project_release_email_to_maintainer( + self, pyramid_request, pyramid_config, monkeypatch + ): + stub_user = pretend.stub( + username="username", + name="", + email="email@example.com", + primary_email=pretend.stub(email="email@example.com", verified=True), + ) + stub_submitter_user = pretend.stub( + username="submitterusername", + name="", + email="submiteremail@example.com", + primary_email=pretend.stub( + email="submiteremail@example.com", verified=True + ), + ) + + subject_renderer = pyramid_config.testing_add_renderer( + "email/yanked-project-release/subject.txt" + ) + subject_renderer.string_response = "Email Subject" + body_renderer = pyramid_config.testing_add_renderer( + "email/yanked-project-release/body.txt" + ) + body_renderer.string_response = "Email Body" + html_renderer = pyramid_config.testing_add_renderer( + "email/yanked-project-release/body.html" + ) + html_renderer.string_response = "Email HTML Body" + + send_email = pretend.stub( + delay=pretend.call_recorder(lambda *args, **kwargs: None) + ) + pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email) + monkeypatch.setattr(email, "send_email", send_email) + + release = pretend.stub( + version="0.0.0", + project=pretend.stub(name="test_project"), + created=datetime.datetime(2017, 2, 5, 0, 0, 0, 0), + ) + + result = email.send_yanked_project_release_email( + pyramid_request, + [stub_user, stub_submitter_user], + release=release, + submitter_name=stub_submitter_user.username, + submitter_role="Owner", + recipient_role="Maintainer", + ) + + assert result == { + "project": release.project.name, + "release": release.version, + "release_date": release.created.strftime("%Y-%m-%d"), + "submitter": stub_submitter_user.username, + "submitter_role": "owner", + "recipient_role_descr": "a maintainer", + } + + subject_renderer.assert_(project="test_project") + subject_renderer.assert_(release="0.0.0") + body_renderer.assert_(project="test_project") + body_renderer.assert_(release="0.0.0") + body_renderer.assert_(release_date=release.created.strftime("%Y-%m-%d")) + body_renderer.assert_(submitter=stub_submitter_user.username) + body_renderer.assert_(submitter_role="owner") + body_renderer.assert_(recipient_role_descr="a maintainer") + + assert pyramid_request.task.calls == [ + pretend.call(send_email), + pretend.call(send_email), + ] + + assert send_email.delay.calls == [ + pretend.call( + "username ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ), + ), + ), + pretend.call( + "submitterusername ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ) + ), + ), + ] + + def test_send_yanked_project_release_email_to_owner( + self, pyramid_request, pyramid_config, monkeypatch + ): + stub_user = pretend.stub( + username="username", + name="", + email="email@example.com", + primary_email=pretend.stub(email="email@example.com", verified=True), + ) + stub_submitter_user = pretend.stub( + username="submitterusername", + name="", + email="submiteremail@example.com", + primary_email=pretend.stub( + email="submiteremail@example.com", verified=True + ), + ) + + subject_renderer = pyramid_config.testing_add_renderer( + "email/yanked-project-release/subject.txt" + ) + subject_renderer.string_response = "Email Subject" + body_renderer = pyramid_config.testing_add_renderer( + "email/yanked-project-release/body.txt" + ) + body_renderer.string_response = "Email Body" + html_renderer = pyramid_config.testing_add_renderer( + "email/yanked-project-release/body.html" + ) + html_renderer.string_response = "Email HTML Body" + + send_email = pretend.stub( + delay=pretend.call_recorder(lambda *args, **kwargs: None) + ) + pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email) + monkeypatch.setattr(email, "send_email", send_email) + + release = pretend.stub( + version="0.0.0", + project=pretend.stub(name="test_project"), + created=datetime.datetime(2017, 2, 5, 0, 0, 0, 0), + ) + + result = email.send_yanked_project_release_email( + pyramid_request, + [stub_user, stub_submitter_user], + release=release, + submitter_name=stub_submitter_user.username, + submitter_role="Owner", + recipient_role="Owner", + ) + + assert result == { + "project": release.project.name, + "release": release.version, + "release_date": release.created.strftime("%Y-%m-%d"), + "submitter": stub_submitter_user.username, + "submitter_role": "owner", + "recipient_role_descr": "an owner", + } + + subject_renderer.assert_(project="test_project") + subject_renderer.assert_(release="0.0.0") + body_renderer.assert_(project="test_project") + body_renderer.assert_(release="0.0.0") + body_renderer.assert_(release_date=release.created.strftime("%Y-%m-%d")) + body_renderer.assert_(submitter=stub_submitter_user.username) + body_renderer.assert_(submitter_role="owner") + body_renderer.assert_(recipient_role_descr="an owner") + + assert pyramid_request.task.calls == [ + pretend.call(send_email), + pretend.call(send_email), + ] + + assert send_email.delay.calls == [ + pretend.call( + "username ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ), + ), + ), + pretend.call( + "submitterusername ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ) + ), + ), + ] + + +class TestUnyankedReleaseEmail: + def test_send_unyanked_project_release_email_to_maintainer( + self, pyramid_request, pyramid_config, monkeypatch + ): + stub_user = pretend.stub( + username="username", + name="", + email="email@example.com", + primary_email=pretend.stub(email="email@example.com", verified=True), + ) + stub_submitter_user = pretend.stub( + username="submitterusername", + name="", + email="submiteremail@example.com", + primary_email=pretend.stub( + email="submiteremail@example.com", verified=True + ), + ) + + subject_renderer = pyramid_config.testing_add_renderer( + "email/unyanked-project-release/subject.txt" + ) + subject_renderer.string_response = "Email Subject" + body_renderer = pyramid_config.testing_add_renderer( + "email/unyanked-project-release/body.txt" + ) + body_renderer.string_response = "Email Body" + html_renderer = pyramid_config.testing_add_renderer( + "email/unyanked-project-release/body.html" + ) + html_renderer.string_response = "Email HTML Body" + + send_email = pretend.stub( + delay=pretend.call_recorder(lambda *args, **kwargs: None) + ) + pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email) + monkeypatch.setattr(email, "send_email", send_email) + + release = pretend.stub( + version="0.0.0", + project=pretend.stub(name="test_project"), + created=datetime.datetime(2017, 2, 5, 0, 0, 0, 0), + ) + + result = email.send_unyanked_project_release_email( + pyramid_request, + [stub_user, stub_submitter_user], + release=release, + submitter_name=stub_submitter_user.username, + submitter_role="Owner", + recipient_role="Maintainer", + ) + + assert result == { + "project": release.project.name, + "release": release.version, + "release_date": release.created.strftime("%Y-%m-%d"), + "submitter": stub_submitter_user.username, + "submitter_role": "owner", + "recipient_role_descr": "a maintainer", + } + + subject_renderer.assert_(project="test_project") + subject_renderer.assert_(release="0.0.0") + body_renderer.assert_(project="test_project") + body_renderer.assert_(release="0.0.0") + body_renderer.assert_(release_date=release.created.strftime("%Y-%m-%d")) + body_renderer.assert_(submitter=stub_submitter_user.username) + body_renderer.assert_(submitter_role="owner") + body_renderer.assert_(recipient_role_descr="a maintainer") + + assert pyramid_request.task.calls == [ + pretend.call(send_email), + pretend.call(send_email), + ] + + assert send_email.delay.calls == [ + pretend.call( + "username ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ), + ), + ), + pretend.call( + "submitterusername ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ) + ), + ), + ] + + def test_send_unyanked_project_release_email_to_owner( + self, pyramid_request, pyramid_config, monkeypatch + ): + stub_user = pretend.stub( + username="username", + name="", + email="email@example.com", + primary_email=pretend.stub(email="email@example.com", verified=True), + ) + stub_submitter_user = pretend.stub( + username="submitterusername", + name="", + email="submiteremail@example.com", + primary_email=pretend.stub( + email="submiteremail@example.com", verified=True + ), + ) + + subject_renderer = pyramid_config.testing_add_renderer( + "email/unyanked-project-release/subject.txt" + ) + subject_renderer.string_response = "Email Subject" + body_renderer = pyramid_config.testing_add_renderer( + "email/unyanked-project-release/body.txt" + ) + body_renderer.string_response = "Email Body" + html_renderer = pyramid_config.testing_add_renderer( + "email/unyanked-project-release/body.html" + ) + html_renderer.string_response = "Email HTML Body" + + send_email = pretend.stub( + delay=pretend.call_recorder(lambda *args, **kwargs: None) + ) + pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email) + monkeypatch.setattr(email, "send_email", send_email) + + release = pretend.stub( + version="0.0.0", + project=pretend.stub(name="test_project"), + created=datetime.datetime(2017, 2, 5, 0, 0, 0, 0), + ) + + result = email.send_unyanked_project_release_email( + pyramid_request, + [stub_user, stub_submitter_user], + release=release, + submitter_name=stub_submitter_user.username, + submitter_role="Owner", + recipient_role="Owner", + ) + + assert result == { + "project": release.project.name, + "release": release.version, + "release_date": release.created.strftime("%Y-%m-%d"), + "submitter": stub_submitter_user.username, + "submitter_role": "owner", + "recipient_role_descr": "an owner", + } + + subject_renderer.assert_(project="test_project") + subject_renderer.assert_(release="0.0.0") + body_renderer.assert_(project="test_project") + body_renderer.assert_(release="0.0.0") + body_renderer.assert_(release_date=release.created.strftime("%Y-%m-%d")) + body_renderer.assert_(submitter=stub_submitter_user.username) + body_renderer.assert_(submitter_role="owner") + body_renderer.assert_(recipient_role_descr="an owner") + + assert pyramid_request.task.calls == [ + pretend.call(send_email), + pretend.call(send_email), + ] + + assert send_email.delay.calls == [ + pretend.call( + "username ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ), + ), + ), + pretend.call( + "submitterusername ", + attr.asdict( + EmailMessage( + subject="Email Subject", + body_text="Email Body", + body_html=( + "\n\n" + "

Email HTML Body

\n\n" + ), + ) + ), + ), + ] + + class TestRemovedReleaseEmail: def test_send_removed_project_release_email_to_maintainer( self, pyramid_request, pyramid_config, monkeypatch diff --git a/tests/unit/legacy/api/test_json.py b/tests/unit/legacy/api/test_json.py index 62cf9b13ff58..1058a154fae7 100644 --- a/tests/unit/legacy/api/test_json.py +++ b/tests/unit/legacy/api/test_json.py @@ -260,6 +260,7 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): "requires_dist": None, "requires_python": None, "summary": None, + "yanked": False, "version": "3.0", }, "releases": { @@ -284,6 +285,7 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): "upload_time_iso_8601": files[0].upload_time.isoformat() + "Z", "url": "/the/fake/url/", "requires_python": None, + "yanked": False, } ], "2.0": [ @@ -306,6 +308,7 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): "upload_time_iso_8601": files[1].upload_time.isoformat() + "Z", "url": "/the/fake/url/", "requires_python": None, + "yanked": False, } ], "3.0": [ @@ -328,6 +331,7 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): "upload_time_iso_8601": files[2].upload_time.isoformat() + "Z", "url": "/the/fake/url/", "requires_python": None, + "yanked": False, } ], }, @@ -349,6 +353,7 @@ def test_detail_renders(self, pyramid_config, db_request, db_session): "upload_time_iso_8601": files[2].upload_time.isoformat() + "Z", "url": "/the/fake/url/", "requires_python": None, + "yanked": False, } ], "last_serial": je.id, @@ -410,6 +415,7 @@ def test_minimal_renders(self, pyramid_config, db_request): "requires_dist": None, "requires_python": None, "summary": None, + "yanked": False, "version": "0.1", }, "releases": { @@ -431,6 +437,7 @@ def test_minimal_renders(self, pyramid_config, db_request): "upload_time_iso_8601": file.upload_time.isoformat() + "Z", "url": "/the/fake/url/", "requires_python": None, + "yanked": False, } ] }, @@ -449,6 +456,7 @@ def test_minimal_renders(self, pyramid_config, db_request): "upload_time_iso_8601": file.upload_time.isoformat() + "Z", "url": "/the/fake/url/", "requires_python": None, + "yanked": False, } ], "last_serial": je.id, diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index 7e4208e02ed5..ecad89e9bea4 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -2541,7 +2541,9 @@ class TestManageProjectRelease: def test_manage_project_release(self): files = pretend.stub() project = pretend.stub() - release = pretend.stub(project=project, files=pretend.stub(all=lambda: files)) + release = pretend.stub( + project=project, files=pretend.stub(all=lambda: files), yanked=False + ) request = pretend.stub() view = views.ManageProjectRelease(release, request) @@ -2593,6 +2595,324 @@ def test_delete_project_release_disallow_deletion(self, monkeypatch): ) ] + def test_yank_project_release(self, monkeypatch): + user = pretend.stub(username=pretend.stub()) + release = pretend.stub( + version="1.2.3", + canonical_version="1.2.3", + project=pretend.stub( + name="foobar", + record_event=pretend.call_recorder(lambda *a, **kw: None), + users=[user], + ), + created=datetime.datetime(2017, 2, 5, 17, 18, 18, 462_634), + yanked=False, + ) + request = pretend.stub( + POST={"confirm_yank_version": release.version}, + method="POST", + db=pretend.stub(add=pretend.call_recorder(lambda a: None),), + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"), + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + user=user, + remote_addr=pretend.stub(), + ) + journal_obj = pretend.stub() + journal_cls = pretend.call_recorder(lambda **kw: journal_obj) + + get_user_role_in_project = pretend.call_recorder( + lambda project, user, req: "Owner" + ) + monkeypatch.setattr(views, "get_user_role_in_project", get_user_role_in_project) + + monkeypatch.setattr(views, "JournalEntry", journal_cls) + send_yanked_project_release_email = pretend.call_recorder( + lambda req, contrib, **k: None + ) + monkeypatch.setattr( + views, + "send_yanked_project_release_email", + send_yanked_project_release_email, + ) + + view = views.ManageProjectRelease(release, request) + + result = view.yank_project_release() + + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + assert release.yanked + + assert get_user_role_in_project.calls == [ + pretend.call(release.project, request.user, request,), + pretend.call(release.project, request.user, request,), + ] + + assert send_yanked_project_release_email.calls == [ + pretend.call( + request, + request.user, + release=release, + submitter_name=request.user.username, + submitter_role="Owner", + recipient_role="Owner", + ) + ] + + assert request.db.add.calls == [pretend.call(journal_obj)] + assert journal_cls.calls == [ + pretend.call( + name=release.project.name, + action="yank release", + version=release.version, + submitted_by=request.user, + submitted_from=request.remote_addr, + ) + ] + assert request.session.flash.calls == [ + pretend.call(f"Yanked release {release.version!r}", queue="success") + ] + assert request.route_path.calls == [ + pretend.call("manage.project.releases", project_name=release.project.name) + ] + assert release.project.record_event.calls == [ + pretend.call( + tag="project:release:yank", + ip_address=request.remote_addr, + additional={ + "submitted_by": request.user.username, + "canonical_version": release.canonical_version, + }, + ) + ] + + def test_yank_project_release_no_confirm(self): + release = pretend.stub( + version="1.2.3", project=pretend.stub(name="foobar"), yanked=False + ) + request = pretend.stub( + POST={"confirm_yank_version": ""}, + method="POST", + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"), + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + ) + view = views.ManageProjectRelease(release, request) + + result = view.yank_project_release() + + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + assert not release.yanked + + assert request.session.flash.calls == [ + pretend.call("Confirm the request", queue="error") + ] + assert request.route_path.calls == [ + pretend.call( + "manage.project.release", + project_name=release.project.name, + version=release.version, + ) + ] + + def test_yank_project_release_bad_confirm(self): + release = pretend.stub( + version="1.2.3", project=pretend.stub(name="foobar"), yanked=False + ) + request = pretend.stub( + POST={"confirm_yank_version": "invalid"}, + method="POST", + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"), + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + ) + view = views.ManageProjectRelease(release, request) + + result = view.yank_project_release() + + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + assert not release.yanked + + assert request.session.flash.calls == [ + pretend.call( + "Could not yank release - " + + f"'invalid' is not the same as {release.version!r}", + queue="error", + ) + ] + assert request.route_path.calls == [ + pretend.call( + "manage.project.release", + project_name=release.project.name, + version=release.version, + ) + ] + + def test_unyank_project_release(self, monkeypatch): + user = pretend.stub(username=pretend.stub()) + release = pretend.stub( + version="1.2.3", + canonical_version="1.2.3", + project=pretend.stub( + name="foobar", + record_event=pretend.call_recorder(lambda *a, **kw: None), + users=[user], + ), + created=datetime.datetime(2017, 2, 5, 17, 18, 18, 462_634), + yanked=True, + ) + request = pretend.stub( + POST={"confirm_unyank_version": release.version}, + method="POST", + db=pretend.stub(add=pretend.call_recorder(lambda a: None),), + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"), + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + user=user, + remote_addr=pretend.stub(), + ) + journal_obj = pretend.stub() + journal_cls = pretend.call_recorder(lambda **kw: journal_obj) + + get_user_role_in_project = pretend.call_recorder( + lambda project_name, username, req: "Owner" + ) + monkeypatch.setattr(views, "get_user_role_in_project", get_user_role_in_project) + + monkeypatch.setattr(views, "JournalEntry", journal_cls) + send_unyanked_project_release_email = pretend.call_recorder( + lambda req, contrib, **k: None + ) + monkeypatch.setattr( + views, + "send_unyanked_project_release_email", + send_unyanked_project_release_email, + ) + + view = views.ManageProjectRelease(release, request) + + result = view.unyank_project_release() + + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + assert not release.yanked + + assert get_user_role_in_project.calls == [ + pretend.call(release.project, request.user, request), + pretend.call(release.project, request.user, request), + ] + + assert send_unyanked_project_release_email.calls == [ + pretend.call( + request, + request.user, + release=release, + submitter_name=request.user.username, + submitter_role="Owner", + recipient_role="Owner", + ) + ] + + assert request.db.add.calls == [pretend.call(journal_obj)] + assert journal_cls.calls == [ + pretend.call( + name=release.project.name, + action="unyank release", + version=release.version, + submitted_by=request.user, + submitted_from=request.remote_addr, + ) + ] + assert request.session.flash.calls == [ + pretend.call(f"Un-yanked release {release.version!r}", queue="success") + ] + assert request.route_path.calls == [ + pretend.call("manage.project.releases", project_name=release.project.name) + ] + assert release.project.record_event.calls == [ + pretend.call( + tag="project:release:unyank", + ip_address=request.remote_addr, + additional={ + "submitted_by": request.user.username, + "canonical_version": release.canonical_version, + }, + ) + ] + + def test_unyank_project_release_no_confirm(self): + release = pretend.stub( + version="1.2.3", project=pretend.stub(name="foobar"), yanked=True + ) + request = pretend.stub( + POST={"confirm_unyank_version": ""}, + method="POST", + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"), + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + ) + view = views.ManageProjectRelease(release, request) + + result = view.unyank_project_release() + + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + assert release.yanked + + assert request.session.flash.calls == [ + pretend.call("Confirm the request", queue="error") + ] + assert request.route_path.calls == [ + pretend.call( + "manage.project.release", + project_name=release.project.name, + version=release.version, + ) + ] + + def test_unyank_project_release_bad_confirm(self): + release = pretend.stub( + version="1.2.3", project=pretend.stub(name="foobar"), yanked=True + ) + request = pretend.stub( + POST={"confirm_unyank_version": "invalid"}, + method="POST", + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + route_path=pretend.call_recorder(lambda *a, **kw: "/the-redirect"), + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + ) + view = views.ManageProjectRelease(release, request) + + result = view.unyank_project_release() + + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + assert release.yanked + + assert request.session.flash.calls == [ + pretend.call( + "Could not un-yank release - " + + f"'invalid' is not the same as {release.version!r}", + queue="error", + ) + ] + assert request.route_path.calls == [ + pretend.call( + "manage.project.release", + project_name=release.project.name, + version=release.version, + ) + ] + def test_delete_project_release(self, monkeypatch): user = pretend.stub(username=pretend.stub()) release = pretend.stub( @@ -2606,7 +2926,7 @@ def test_delete_project_release(self, monkeypatch): created=datetime.datetime(2017, 2, 5, 17, 18, 18, 462_634), ) request = pretend.stub( - POST={"confirm_version": release.version}, + POST={"confirm_delete_version": release.version}, method="POST", db=pretend.stub( delete=pretend.call_recorder(lambda a: None), @@ -2693,7 +3013,7 @@ def test_delete_project_release(self, monkeypatch): def test_delete_project_release_no_confirm(self): release = pretend.stub(version="1.2.3", project=pretend.stub(name="foobar")) request = pretend.stub( - POST={"confirm_version": ""}, + POST={"confirm_delete_version": ""}, method="POST", db=pretend.stub(delete=pretend.call_recorder(lambda a: None)), flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), @@ -2725,7 +3045,7 @@ def test_delete_project_release_no_confirm(self): def test_delete_project_release_bad_confirm(self): release = pretend.stub(version="1.2.3", project=pretend.stub(name="foobar")) request = pretend.stub( - POST={"confirm_version": "invalid"}, + POST={"confirm_delete_version": "invalid"}, method="POST", db=pretend.stub(delete=pretend.call_recorder(lambda a: None)), flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), diff --git a/tests/unit/packaging/test_views.py b/tests/unit/packaging/test_views.py index 7b0966d3625b..786c1d9d87ad 100644 --- a/tests/unit/packaging/test_views.py +++ b/tests/unit/packaging/test_views.py @@ -183,7 +183,8 @@ def test_detail_rendered(self, db_request): "description": "rendered description", "latest_version": project.latest_version, "all_versions": [ - (r.version, r.created, r.is_prerelease) for r in reversed(releases) + (r.version, r.created, r.is_prerelease, r.yanked) + for r in reversed(releases) ], "maintainers": sorted(users, key=lambda u: u.username.lower()), "license": None, @@ -230,7 +231,8 @@ def test_detail_renders(self, monkeypatch, db_request): "description": "rendered description", "latest_version": project.latest_version, "all_versions": [ - (r.version, r.created, r.is_prerelease) for r in reversed(releases) + (r.version, r.created, r.is_prerelease, r.yanked) + for r in reversed(releases) ], "maintainers": sorted(users, key=lambda u: u.username.lower()), "license": None, diff --git a/warehouse/email/__init__.py b/warehouse/email/__init__.py index e768e49f032e..5b44af735596 100644 --- a/warehouse/email/__init__.py +++ b/warehouse/email/__init__.py @@ -229,6 +229,42 @@ def send_removed_project_email( } +@_email("yanked-project-release") +def send_yanked_project_release_email( + request, user, *, release, submitter_name, submitter_role, recipient_role +): + recipient_role_descr = "an owner" + if recipient_role == "Maintainer": + recipient_role_descr = "a maintainer" + + return { + "project": release.project.name, + "release": release.version, + "release_date": release.created.strftime("%Y-%m-%d"), + "submitter": submitter_name, + "submitter_role": submitter_role.lower(), + "recipient_role_descr": recipient_role_descr, + } + + +@_email("unyanked-project-release") +def send_unyanked_project_release_email( + request, user, *, release, submitter_name, submitter_role, recipient_role +): + recipient_role_descr = "an owner" + if recipient_role == "Maintainer": + recipient_role_descr = "a maintainer" + + return { + "project": release.project.name, + "release": release.version, + "release_date": release.created.strftime("%Y-%m-%d"), + "submitter": submitter_name, + "submitter_role": submitter_role.lower(), + "recipient_role_descr": recipient_role_descr, + } + + @_email("removed-project-release") def send_removed_project_release_email( request, user, *, release, submitter_name, submitter_role, recipient_role diff --git a/warehouse/legacy/api/json.py b/warehouse/legacy/api/json.py index 8192efc451ed..9a6d09d9cb16 100644 --- a/warehouse/legacy/api/json.py +++ b/warehouse/legacy/api/json.py @@ -65,7 +65,7 @@ def json_project(project, request): try: release = ( request.db.query(Release) - .filter(Release.project == project) + .filter(Release.project == project, Release.yanked.is_(False)) .order_by(Release.is_prerelease.nullslast(), Release._pypi_ordering.desc()) .limit(1) .one() @@ -147,6 +147,7 @@ def json_release(release, request): "upload_time_iso_8601": f.upload_time.isoformat() + "Z", "url": request.route_url("packaging.file", path=f.path), "requires_python": r.requires_python if r.requires_python else None, + "yanked": r.yanked, } for f in fs ] @@ -183,6 +184,7 @@ def json_release(release, request): "bugtrack_url": None, "home_page": release.home_page, "download_url": release.download_url, + "yanked": release.yanked, }, "urls": releases[release.version], "releases": releases, diff --git a/warehouse/legacy/api/simple.py b/warehouse/legacy/api/simple.py index b511f4d4bf05..a3bf32ae5e27 100644 --- a/warehouse/legacy/api/simple.py +++ b/warehouse/legacy/api/simple.py @@ -63,8 +63,6 @@ def simple_index(request): ], ) def simple_detail(project, request): - # TODO: Handle files which are not hosted on PyPI - # Make sure that we're using the normalized version of the URL. if project.normalized_name != request.matchdict.get( "name", project.normalized_name diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index df82bc6b9ab6..0902a97ae6ef 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -175,25 +175,25 @@ msgstr "" msgid "Email address ${email_address} verified. ${confirm_message}." msgstr "" -#: warehouse/manage/views.py:187 +#: warehouse/manage/views.py:189 msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views.py:668 warehouse/manage/views.py:704 +#: warehouse/manage/views.py:670 warehouse/manage/views.py:706 msgid "" "You must provision a two factor method before recovery codes can be " "generated" msgstr "" -#: warehouse/manage/views.py:679 +#: warehouse/manage/views.py:681 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views.py:680 +#: warehouse/manage/views.py:682 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views.py:731 +#: warehouse/manage/views.py:733 msgid "Invalid credentials. Try again" msgstr "" @@ -246,68 +246,69 @@ msgstr "" #: warehouse/templates/manage/account/webauthn-provision.html:53 #: warehouse/templates/manage/account/webauthn-provision.html:74 #: warehouse/templates/manage/release.html:119 -#: warehouse/templates/manage/release.html:137 -#: warehouse/templates/manage/releases.html:93 -#: warehouse/templates/manage/releases.html:111 -#: warehouse/templates/packaging/detail.html:279 +#: warehouse/templates/manage/release.html:161 +#: warehouse/templates/manage/releases.html:123 +#: warehouse/templates/manage/releases.html:156 +#: warehouse/templates/packaging/detail.html:297 #: warehouse/templates/pages/classifiers.html:25 #: warehouse/templates/pages/help.html:20 -#: warehouse/templates/pages/help.html:194 -#: warehouse/templates/pages/help.html:201 -#: warehouse/templates/pages/help.html:224 -#: warehouse/templates/pages/help.html:228 -#: warehouse/templates/pages/help.html:263 -#: warehouse/templates/pages/help.html:290 -#: warehouse/templates/pages/help.html:295 -#: warehouse/templates/pages/help.html:300 -#: warehouse/templates/pages/help.html:302 -#: warehouse/templates/pages/help.html:307 -#: warehouse/templates/pages/help.html:308 +#: warehouse/templates/pages/help.html:196 +#: warehouse/templates/pages/help.html:203 +#: warehouse/templates/pages/help.html:217 +#: warehouse/templates/pages/help.html:233 +#: warehouse/templates/pages/help.html:237 +#: warehouse/templates/pages/help.html:272 +#: warehouse/templates/pages/help.html:299 +#: warehouse/templates/pages/help.html:304 #: warehouse/templates/pages/help.html:309 -#: warehouse/templates/pages/help.html:313 -#: warehouse/templates/pages/help.html:346 -#: warehouse/templates/pages/help.html:348 -#: warehouse/templates/pages/help.html:351 -#: warehouse/templates/pages/help.html:387 -#: warehouse/templates/pages/help.html:392 -#: warehouse/templates/pages/help.html:398 -#: warehouse/templates/pages/help.html:456 -#: warehouse/templates/pages/help.html:468 -#: warehouse/templates/pages/help.html:474 +#: warehouse/templates/pages/help.html:311 +#: warehouse/templates/pages/help.html:316 +#: warehouse/templates/pages/help.html:317 +#: warehouse/templates/pages/help.html:318 +#: warehouse/templates/pages/help.html:322 +#: warehouse/templates/pages/help.html:355 +#: warehouse/templates/pages/help.html:357 +#: warehouse/templates/pages/help.html:360 +#: warehouse/templates/pages/help.html:396 +#: warehouse/templates/pages/help.html:401 +#: warehouse/templates/pages/help.html:407 +#: warehouse/templates/pages/help.html:465 #: warehouse/templates/pages/help.html:477 -#: warehouse/templates/pages/help.html:479 +#: warehouse/templates/pages/help.html:483 +#: warehouse/templates/pages/help.html:486 #: warehouse/templates/pages/help.html:488 -#: warehouse/templates/pages/help.html:500 -#: warehouse/templates/pages/help.html:506 -#: warehouse/templates/pages/help.html:518 -#: warehouse/templates/pages/help.html:519 -#: warehouse/templates/pages/help.html:524 -#: warehouse/templates/pages/help.html:559 -#: warehouse/templates/pages/help.html:580 -#: warehouse/templates/pages/help.html:592 -#: warehouse/templates/pages/help.html:603 -#: warehouse/templates/pages/help.html:608 -#: warehouse/templates/pages/help.html:616 -#: warehouse/templates/pages/help.html:627 -#: warehouse/templates/pages/help.html:644 -#: warehouse/templates/pages/help.html:651 -#: warehouse/templates/pages/help.html:659 -#: warehouse/templates/pages/help.html:675 -#: warehouse/templates/pages/help.html:680 -#: warehouse/templates/pages/help.html:685 -#: warehouse/templates/pages/help.html:695 +#: warehouse/templates/pages/help.html:497 +#: warehouse/templates/pages/help.html:509 +#: warehouse/templates/pages/help.html:515 +#: warehouse/templates/pages/help.html:527 +#: warehouse/templates/pages/help.html:528 +#: warehouse/templates/pages/help.html:533 +#: warehouse/templates/pages/help.html:568 +#: warehouse/templates/pages/help.html:589 +#: warehouse/templates/pages/help.html:601 +#: warehouse/templates/pages/help.html:612 +#: warehouse/templates/pages/help.html:617 +#: warehouse/templates/pages/help.html:625 +#: warehouse/templates/pages/help.html:636 +#: warehouse/templates/pages/help.html:653 +#: warehouse/templates/pages/help.html:660 +#: warehouse/templates/pages/help.html:668 +#: warehouse/templates/pages/help.html:684 +#: warehouse/templates/pages/help.html:689 +#: warehouse/templates/pages/help.html:694 #: warehouse/templates/pages/help.html:704 -#: warehouse/templates/pages/help.html:718 -#: warehouse/templates/pages/help.html:726 -#: warehouse/templates/pages/help.html:734 -#: warehouse/templates/pages/help.html:742 -#: warehouse/templates/pages/help.html:752 -#: warehouse/templates/pages/help.html:767 -#: warehouse/templates/pages/help.html:782 -#: warehouse/templates/pages/help.html:783 -#: warehouse/templates/pages/help.html:784 -#: warehouse/templates/pages/help.html:785 -#: warehouse/templates/pages/help.html:790 +#: warehouse/templates/pages/help.html:713 +#: warehouse/templates/pages/help.html:727 +#: warehouse/templates/pages/help.html:735 +#: warehouse/templates/pages/help.html:743 +#: warehouse/templates/pages/help.html:751 +#: warehouse/templates/pages/help.html:761 +#: warehouse/templates/pages/help.html:776 +#: warehouse/templates/pages/help.html:791 +#: warehouse/templates/pages/help.html:792 +#: warehouse/templates/pages/help.html:793 +#: warehouse/templates/pages/help.html:794 +#: warehouse/templates/pages/help.html:799 #: warehouse/templates/pages/security.html:36 #: warehouse/templates/pages/sponsor.html:27 #: warehouse/templates/pages/sponsor.html:33 @@ -420,7 +421,7 @@ msgstr "" #: warehouse/templates/base.html:41 warehouse/templates/base.html:55 #: warehouse/templates/base.html:257 #: warehouse/templates/includes/current-user-indicator.html:54 -#: warehouse/templates/pages/help.html:98 +#: warehouse/templates/pages/help.html:99 #: warehouse/templates/pages/sitemap.html:27 msgid "Help" msgstr "" @@ -482,9 +483,9 @@ msgstr "" #: warehouse/templates/includes/session-notifications.html:19 #: warehouse/templates/manage/account.html:728 #: warehouse/templates/manage/documentation.html:27 -#: warehouse/templates/manage/manage_base.html:105 -#: warehouse/templates/manage/manage_base.html:157 -#: warehouse/templates/manage/release.html:127 +#: warehouse/templates/manage/manage_base.html:106 +#: warehouse/templates/manage/manage_base.html:158 +#: warehouse/templates/manage/release.html:151 #: warehouse/templates/manage/settings.html:56 msgid "Warning" msgstr "" @@ -812,7 +813,7 @@ msgid "Password" msgstr "" #: warehouse/templates/accounts/login.html:76 -#: warehouse/templates/manage/manage_base.html:169 +#: warehouse/templates/manage/manage_base.html:170 msgid "Show password" msgstr "" @@ -1337,10 +1338,10 @@ msgstr "" #: warehouse/templates/manage/account.html:223 #: warehouse/templates/manage/account.html:225 #: warehouse/templates/manage/account.html:235 -#: warehouse/templates/manage/manage_base.html:93 -#: warehouse/templates/manage/manage_base.html:95 +#: warehouse/templates/manage/manage_base.html:94 +#: warehouse/templates/manage/manage_base.html:96 #: warehouse/templates/manage/release.html:118 -#: warehouse/templates/manage/releases.html:110 +#: warehouse/templates/manage/releases.html:155 #: warehouse/templates/manage/roles.html:24 #: warehouse/templates/manage/settings.html:50 #: warehouse/templates/search/results.html:201 @@ -1551,7 +1552,7 @@ msgstr "" #: warehouse/templates/includes/packaging/project-data.html:84 #: warehouse/templates/includes/packaging/project-data.html:86 -#: warehouse/templates/pages/help.html:510 +#: warehouse/templates/pages/help.html:519 msgid "Maintainer:" msgstr "" @@ -1977,7 +1978,7 @@ msgstr "" #: warehouse/templates/manage/account.html:568 #: warehouse/templates/manage/release.html:58 -#: warehouse/templates/packaging/detail.html:311 +#: warehouse/templates/packaging/detail.html:329 msgid "None" msgstr "" @@ -2360,7 +2361,7 @@ msgstr "" #: warehouse/templates/manage/manage_base.html:16 #: warehouse/templates/manage/projects.html:57 -#: warehouse/templates/manage/releases.html:71 +#: warehouse/templates/manage/releases.html:82 msgid "Manage" msgstr "" @@ -2373,30 +2374,30 @@ msgstr "" msgid "Account navigation" msgstr "" -#: warehouse/templates/manage/manage_base.html:106 -#: warehouse/templates/manage/manage_base.html:158 +#: warehouse/templates/manage/manage_base.html:107 +#: warehouse/templates/manage/manage_base.html:159 msgid "This action cannot be undone!" msgstr "" -#: warehouse/templates/manage/manage_base.html:114 +#: warehouse/templates/manage/manage_base.html:115 msgid "Confirm your username to continue." msgstr "" -#: warehouse/templates/manage/manage_base.html:116 +#: warehouse/templates/manage/manage_base.html:117 #, python-format msgid "Confirm the %(item)s to continue." msgstr "" -#: warehouse/templates/manage/manage_base.html:124 -#: warehouse/templates/manage/manage_base.html:176 +#: warehouse/templates/manage/manage_base.html:125 +#: warehouse/templates/manage/manage_base.html:177 msgid "Cancel" msgstr "" -#: warehouse/templates/manage/manage_base.html:147 +#: warehouse/templates/manage/manage_base.html:148 msgid "close" msgstr "" -#: warehouse/templates/manage/manage_base.html:163 +#: warehouse/templates/manage/manage_base.html:164 msgid "Enter your password to continue." msgstr "" @@ -2441,8 +2442,8 @@ msgid "This project has no releases" msgstr "" #: warehouse/templates/manage/projects.html:67 -#: warehouse/templates/manage/releases.html:77 -#: warehouse/templates/packaging/detail.html:321 +#: warehouse/templates/manage/releases.html:88 +#: warehouse/templates/packaging/detail.html:339 msgid "View" msgstr "" @@ -2485,8 +2486,8 @@ msgstr "" #: warehouse/templates/manage/release.html:37 #: warehouse/templates/manage/release.html:48 -#: warehouse/templates/packaging/detail.html:285 -#: warehouse/templates/packaging/detail.html:296 +#: warehouse/templates/packaging/detail.html:303 +#: warehouse/templates/packaging/detail.html:314 msgid "Filename, size" msgstr "" @@ -2497,15 +2498,15 @@ msgstr "" #: warehouse/templates/manage/release.html:39 #: warehouse/templates/manage/release.html:57 -#: warehouse/templates/packaging/detail.html:287 -#: warehouse/templates/packaging/detail.html:307 +#: warehouse/templates/packaging/detail.html:305 +#: warehouse/templates/packaging/detail.html:325 msgid "Python version" msgstr "" #: warehouse/templates/manage/release.html:40 #: warehouse/templates/manage/release.html:61 -#: warehouse/templates/packaging/detail.html:288 -#: warehouse/templates/packaging/detail.html:315 +#: warehouse/templates/packaging/detail.html:306 +#: warehouse/templates/packaging/detail.html:333 msgid "Upload date" msgstr "" @@ -2534,7 +2535,7 @@ msgid "Delete file" msgstr "" #: warehouse/templates/manage/release.html:93 -#: warehouse/templates/manage/releases.html:86 +#: warehouse/templates/manage/releases.html:103 msgid "Delete" msgstr "" @@ -2547,7 +2548,7 @@ msgid "No files found" msgstr "" #: warehouse/templates/manage/release.html:118 -#: warehouse/templates/manage/releases.html:110 +#: warehouse/templates/manage/releases.html:155 #: warehouse/templates/manage/roles.html:24 #: warehouse/templates/manage/settings.html:50 msgid "Dismiss" @@ -2565,69 +2566,108 @@ msgid "Release settings" msgstr "" #: warehouse/templates/manage/release.html:125 -#: warehouse/templates/manage/release.html:142 -msgid "Delete release" +#: warehouse/templates/manage/release.html:145 +#: warehouse/templates/manage/releases.html:117 +msgid "Yank release" msgstr "" -#: warehouse/templates/manage/release.html:129 +#: warehouse/templates/manage/release.html:128 #, python-format msgid "" "\n" -" Deleting will irreversibly delete this release along with " -"%(count)s file.\n" +" Yanking will mark this release (and %(count)s file within it) to " +"be ignored when installing in most common scenarios.\n" " " msgid_plural "" "\n" -" Deleting will irreversibly delete this release along with " -"%(count)s files.\n" +" Yanking will mark this release (and %(count)s files within it) to" +" be ignored when installing in most common scenarios.\n" " " msgstr[0] "" msgstr[1] "" -#: warehouse/templates/manage/release.html:135 -msgid "Deleting will irreversibly delete this release." +#: warehouse/templates/manage/release.html:134 +msgid "" +"\n" +" Yanking will mark this release to be ignored when installing in " +"most common scenarios.\n" +" " msgstr "" -#: warehouse/templates/manage/release.html:137 -#: warehouse/templates/manage/releases.html:93 +#: warehouse/templates/manage/release.html:138 #, python-format msgid "" -"You will not be able to re-upload a new distribution of the same type " -"with the same version number. Consider making a new release or a post release instead." +"\n" +" This release will still be installable for users pinning to this " +"exact version, e.g. when using " +"%(project_name)s==%(version)s.\n" +" " +msgstr "" + +#: warehouse/templates/manage/release.html:141 +#, python-format +msgid "" +"\n" +" For more information, see PEP 592.\n" +" " msgstr "" -#: warehouse/templates/manage/release.html:142 -#: warehouse/templates/manage/releases.html:27 -#: warehouse/templates/manage/releases.html:96 +#: warehouse/templates/manage/release.html:145 +#: warehouse/templates/manage/release.html:166 +#: warehouse/templates/manage/releases.html:23 +#: warehouse/templates/manage/releases.html:114 +#: warehouse/templates/manage/releases.html:118 +#: warehouse/templates/manage/releases.html:125 msgid "Version" msgstr "" -#: warehouse/templates/manage/releases.html:18 -#, python-format -msgid "Manage '%(project_name)s' releases" +#: warehouse/templates/manage/release.html:149 +#: warehouse/templates/manage/release.html:166 +#: warehouse/templates/manage/releases.html:121 +msgid "Delete release" msgstr "" -#: warehouse/templates/manage/releases.html:21 +#: warehouse/templates/manage/release.html:153 #, python-format -msgid "Releases (%(release_count)s)" +msgid "" +"\n" +" Deleting will irreversibly delete this release along with " +"%(count)s file.\n" +" " +msgid_plural "" +"\n" +" Deleting will irreversibly delete this release along with " +"%(count)s files.\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: warehouse/templates/manage/release.html:159 +msgid "Deleting will irreversibly delete this release." msgstr "" -#: warehouse/templates/manage/releases.html:24 +#: warehouse/templates/manage/release.html:161 +msgid "" +"You will not be able to re-upload a new distribution of the same type " +"with the same version number. Consider yanking this release, making a new" +" release, or a post release instead." +msgstr "" + +#: warehouse/templates/manage/releases.html:20 #, python-format msgid "Releases for %(project_name)s" msgstr "" -#: warehouse/templates/manage/releases.html:28 +#: warehouse/templates/manage/releases.html:24 msgid "Release date" msgstr "" -#: warehouse/templates/manage/releases.html:29 +#: warehouse/templates/manage/releases.html:25 msgid "Files" msgstr "" -#: warehouse/templates/manage/releases.html:37 +#: warehouse/templates/manage/releases.html:34 msgid "Manage version" msgstr "" @@ -2635,12 +2675,12 @@ msgstr "" #, python-format msgid "" "\n" -" %(count)s file\n" -" " +" %(count)s file\n" +" " msgid_plural "" "\n" -" %(count)s files\n" -" " +" %(count)s files\n" +" " msgstr[0] "" msgstr[1] "" @@ -2657,19 +2697,55 @@ msgstr "" msgid "Options for %(version)s" msgstr "" -#: warehouse/templates/manage/releases.html:81 -msgid "Delete Release" +#: warehouse/templates/manage/releases.html:70 +msgid "Un-yank Release" +msgstr "" + +#: warehouse/templates/manage/releases.html:75 +msgid "Un-yank" +msgstr "" + +#: warehouse/templates/manage/releases.html:95 +msgid "Yank" +msgstr "" + +#: warehouse/templates/manage/releases.html:113 +msgid "Un-yank release" msgstr "" -#: warehouse/templates/manage/releases.html:106 +#: warehouse/templates/manage/releases.html:123 +#, python-format +msgid "" +"You will not be able to re-upload a new distribution of the same type " +"with the same version number. Consider yanking this release, making a new" +" release or a post release instead." +msgstr "" + +#: warehouse/templates/manage/releases.html:134 +#, python-format +msgid "Manage '%(project_name)s' releases" +msgstr "" + +#: warehouse/templates/manage/releases.html:139 +#, python-format +msgid "Releases (%(release_count)s)" +msgstr "" + +#: warehouse/templates/manage/releases.html:145 +#, python-format +msgid "Yanked Releases (%(release_count)s)" +msgstr "" + +#: warehouse/templates/manage/releases.html:151 msgid "Creating a new release" msgstr "" -#: warehouse/templates/manage/releases.html:108 +#: warehouse/templates/manage/releases.html:153 msgid "No releases found" msgstr "" -#: warehouse/templates/manage/releases.html:111 +#: warehouse/templates/manage/releases.html:156 #, python-format msgid "" "Learn how to create a new release on the installing packages." msgstr "" -#: warehouse/templates/packaging/detail.html:282 +#: warehouse/templates/packaging/detail.html:300 #, python-format msgid "Files for %(project_name)s, version %(version)s" msgstr "" -#: warehouse/templates/packaging/detail.html:286 -#: warehouse/templates/packaging/detail.html:303 +#: warehouse/templates/packaging/detail.html:304 +#: warehouse/templates/packaging/detail.html:321 msgid "File type" msgstr "" -#: warehouse/templates/packaging/detail.html:289 -#: warehouse/templates/packaging/detail.html:319 +#: warehouse/templates/packaging/detail.html:307 +#: warehouse/templates/packaging/detail.html:337 msgid "Hashes" msgstr "" @@ -3337,201 +3421,205 @@ msgstr "" msgid "What's a trove classifier?" msgstr "" -#: warehouse/templates/pages/help.html:57 -msgid "Why do I need a verified email address?" +#: warehouse/templates/pages/help.html:56 +msgid "What's a \"yanked\" release?" msgstr "" #: warehouse/templates/pages/help.html:58 -msgid "Why is PyPI telling me my password is compromised?" +msgid "Why do I need a verified email address?" msgstr "" #: warehouse/templates/pages/help.html:59 -msgid "What is two factor authentication and how does it work on PyPI?" +msgid "Why is PyPI telling me my password is compromised?" msgstr "" #: warehouse/templates/pages/help.html:60 +msgid "What is two factor authentication and how does it work on PyPI?" +msgstr "" + +#: warehouse/templates/pages/help.html:61 msgid "" "How does two factor authentication with an authentication application " "(TOTP) work? How do I" " set it up on PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:61 +#: warehouse/templates/pages/help.html:62 msgid "" "How does two factor authentication with a security device (e.g. USB key) " "work? How do I set it up on PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:62 +#: warehouse/templates/pages/help.html:63 msgid "What devices (other than a USB key) can I use as a security device?" msgstr "" -#: warehouse/templates/pages/help.html:63 +#: warehouse/templates/pages/help.html:64 msgid "" "How does two factor authentication with a recovery code work? How do I " "set it up on PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:64 +#: warehouse/templates/pages/help.html:65 msgid "How can I use API tokens to authenticate with PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:66 +#: warehouse/templates/pages/help.html:67 msgid "How can I run a mirror of PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:67 +#: warehouse/templates/pages/help.html:68 msgid "Does PyPI have APIs I can use?" msgstr "" -#: warehouse/templates/pages/help.html:68 +#: warehouse/templates/pages/help.html:69 msgid "How do I get notified when a new version of a project is released?" msgstr "" -#: warehouse/templates/pages/help.html:69 +#: warehouse/templates/pages/help.html:70 msgid "" "Where can I see statistics about PyPI, downloads, and project/package " "usage?" msgstr "" -#: warehouse/templates/pages/help.html:71 +#: warehouse/templates/pages/help.html:72 msgid "I forgot my PyPI password. Can you help me?" msgstr "" -#: warehouse/templates/pages/help.html:72 +#: warehouse/templates/pages/help.html:73 msgid "I've lost access to my PyPI account. Can you help me?" msgstr "" -#: warehouse/templates/pages/help.html:73 +#: warehouse/templates/pages/help.html:74 msgid "" "Why am I getting a \"Invalid or non-existent authentication " "information.\" error when uploading files?" msgstr "" -#: warehouse/templates/pages/help.html:74 +#: warehouse/templates/pages/help.html:75 msgid "" "Why am I getting \"No matching distribution found\" or \"Could not fetch " "URL\" errors during pip install?" msgstr "" -#: warehouse/templates/pages/help.html:75 +#: warehouse/templates/pages/help.html:76 msgid "I am having trouble using the PyPI website. Can you help me?" msgstr "" -#: warehouse/templates/pages/help.html:76 +#: warehouse/templates/pages/help.html:77 msgid "Why can't I manually upload files to PyPI, through the browser interface?" msgstr "" -#: warehouse/templates/pages/help.html:77 +#: warehouse/templates/pages/help.html:78 msgid "How can I publish my private packages to PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:78 +#: warehouse/templates/pages/help.html:79 msgid "Why did my package or user registration get blocked?" msgstr "" -#: warehouse/templates/pages/help.html:79 +#: warehouse/templates/pages/help.html:80 msgid "How do I get a file size limit exemption or increase for my project?" msgstr "" -#: warehouse/templates/pages/help.html:81 +#: warehouse/templates/pages/help.html:82 msgid "" "Why am I getting a \"Filename or contents already exists\" or \"Filename " "has been previously used\" error?" msgstr "" -#: warehouse/templates/pages/help.html:82 +#: warehouse/templates/pages/help.html:83 msgid "Why isn't my desired project name available?" msgstr "" -#: warehouse/templates/pages/help.html:83 +#: warehouse/templates/pages/help.html:84 msgid "How do I claim an abandoned or previously registered project name?" msgstr "" -#: warehouse/templates/pages/help.html:84 +#: warehouse/templates/pages/help.html:85 msgid "What collaborator roles are available for a project on PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:85 +#: warehouse/templates/pages/help.html:86 msgid "How do I become an owner/maintainer of a project on PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:86 +#: warehouse/templates/pages/help.html:87 msgid "How can I upload a project description in a different format?" msgstr "" -#: warehouse/templates/pages/help.html:87 +#: warehouse/templates/pages/help.html:88 msgid "How do I request a new trove classifier?" msgstr "" -#: warehouse/templates/pages/help.html:88 +#: warehouse/templates/pages/help.html:89 msgid "Where can I report a bug or provide feedback about PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:90 +#: warehouse/templates/pages/help.html:91 msgid "Who maintains PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:91 +#: warehouse/templates/pages/help.html:92 msgid "What powers PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:92 +#: warehouse/templates/pages/help.html:93 msgid "Can I depend on PyPI being available?" msgstr "" -#: warehouse/templates/pages/help.html:93 +#: warehouse/templates/pages/help.html:94 msgid "How can I contribute to PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:94 +#: warehouse/templates/pages/help.html:95 msgid "How do I keep up with upcoming changes to PyPI?" msgstr "" -#: warehouse/templates/pages/help.html:95 +#: warehouse/templates/pages/help.html:96 msgid "" "What does the \"beta feature\" badge mean? What are Warehouse's current " "beta features?" msgstr "" -#: warehouse/templates/pages/help.html:96 +#: warehouse/templates/pages/help.html:97 msgid "How do I pronounce \"PyPI\"?" msgstr "" -#: warehouse/templates/pages/help.html:103 +#: warehouse/templates/pages/help.html:104 msgid "Common questions" msgstr "" -#: warehouse/templates/pages/help.html:106 -#: warehouse/templates/pages/help.html:182 +#: warehouse/templates/pages/help.html:107 +#: warehouse/templates/pages/help.html:184 msgid "Basics" msgstr "" -#: warehouse/templates/pages/help.html:116 +#: warehouse/templates/pages/help.html:118 msgid "My Account" msgstr "" -#: warehouse/templates/pages/help.html:130 -#: warehouse/templates/pages/help.html:465 +#: warehouse/templates/pages/help.html:132 +#: warehouse/templates/pages/help.html:474 msgid "Integrating" msgstr "" -#: warehouse/templates/pages/help.html:140 -#: warehouse/templates/pages/help.html:492 +#: warehouse/templates/pages/help.html:142 +#: warehouse/templates/pages/help.html:501 msgid "Administration of projects on PyPI" msgstr "" -#: warehouse/templates/pages/help.html:153 -#: warehouse/templates/pages/help.html:541 +#: warehouse/templates/pages/help.html:155 +#: warehouse/templates/pages/help.html:550 msgid "Troubleshooting" msgstr "" -#: warehouse/templates/pages/help.html:169 -#: warehouse/templates/pages/help.html:671 +#: warehouse/templates/pages/help.html:171 +#: warehouse/templates/pages/help.html:680 msgid "About" msgstr "" -#: warehouse/templates/pages/help.html:185 +#: warehouse/templates/pages/help.html:187 #, python-format msgid "" "\n" @@ -3555,7 +3643,7 @@ msgid "" " " msgstr "" -#: warehouse/templates/pages/help.html:194 +#: warehouse/templates/pages/help.html:196 #, python-format msgid "" "To learn how to install a file from PyPI, visit the Python Packaging User Guide." msgstr "" -#: warehouse/templates/pages/help.html:201 +#: warehouse/templates/pages/help.html:203 #, python-format msgid "" "For full instructions on configuring, packaging and distributing your " @@ -3575,7 +3663,7 @@ msgid "" "target=\"_blank\" rel=\"noopener\">Python Packaging User Guide." msgstr "" -#: warehouse/templates/pages/help.html:208 +#: warehouse/templates/pages/help.html:210 #, python-format msgid "" "Classifiers are used to categorize projects on PyPI. See == or ===. See PEP 592 for more " +"information." +msgstr "" + +#: warehouse/templates/pages/help.html:224 msgid "My account" msgstr "" -#: warehouse/templates/pages/help.html:218 +#: warehouse/templates/pages/help.html:227 msgid "" "Currently, PyPI requires a verified email address to perform the " "following operations:" msgstr "" -#: warehouse/templates/pages/help.html:220 +#: warehouse/templates/pages/help.html:229 msgid "Register a new project." msgstr "" -#: warehouse/templates/pages/help.html:221 +#: warehouse/templates/pages/help.html:230 msgid "Upload a new version or file." msgstr "" -#: warehouse/templates/pages/help.html:223 +#: warehouse/templates/pages/help.html:232 msgid "" "The list of activities that require a verified email address is likely to" " grow over time." msgstr "" -#: warehouse/templates/pages/help.html:224 +#: warehouse/templates/pages/help.html:233 #, python-format msgid "" "This policy will allow us to enforce a key policy of PyPI itself has not suffered a breach. This is a protective measure " @@ -3652,7 +3751,7 @@ msgid "" "href=\"%(reset_pwd_href)s\">reset your password.

" msgstr "" -#: warehouse/templates/pages/help.html:263 +#: warehouse/templates/pages/help.html:272 #, python-format msgid "" "

Two factor authentication (2FA) makes your account more secure by " @@ -3671,7 +3770,7 @@ msgid "" "rel=\"noopener\">discuss.python.org.

" msgstr "" -#: warehouse/templates/pages/help.html:290 +#: warehouse/templates/pages/help.html:299 #, python-format msgid "" "PyPI users can set up two-factor authentication using any authentication " @@ -3680,21 +3779,21 @@ msgid "" "password\">TOTP standard." msgstr "" -#: warehouse/templates/pages/help.html:291 +#: warehouse/templates/pages/help.html:300 msgid "" "TOTP authentication " "applications generate a regularly changing authentication code to use " "when logging into your account." msgstr "" -#: warehouse/templates/pages/help.html:292 +#: warehouse/templates/pages/help.html:301 msgid "" "Because TOTP is an " "open standard, there are many applications that are compatible with your " "PyPI account. Popular applications include:" msgstr "" -#: warehouse/templates/pages/help.html:295 +#: warehouse/templates/pages/help.html:304 #, python-format msgid "" "Google Authenticator for iOS" msgstr "" -#: warehouse/templates/pages/help.html:298 -#: warehouse/templates/pages/help.html:300 -#: warehouse/templates/pages/help.html:305 #: warehouse/templates/pages/help.html:307 +#: warehouse/templates/pages/help.html:309 +#: warehouse/templates/pages/help.html:314 +#: warehouse/templates/pages/help.html:316 msgid "(proprietary)" msgstr "" -#: warehouse/templates/pages/help.html:302 +#: warehouse/templates/pages/help.html:311 #, python-format msgid "" "Duo Mobile for iOS" msgstr "" -#: warehouse/templates/pages/help.html:308 -#: warehouse/templates/pages/help.html:309 +#: warehouse/templates/pages/help.html:317 +#: warehouse/templates/pages/help.html:318 msgid "(open source)" msgstr "" -#: warehouse/templates/pages/help.html:313 +#: warehouse/templates/pages/help.html:322 #, python-format msgid "" "Some password managers (e.g. 2FA with an " "authentication application:" msgstr "" -#: warehouse/templates/pages/help.html:323 +#: warehouse/templates/pages/help.html:332 msgid "" "Open an authentication (TOTP) application" msgstr "" -#: warehouse/templates/pages/help.html:324 +#: warehouse/templates/pages/help.html:333 msgid "" "Log in to your PyPI account, go to your account settings, and choose " "\"Add 2FA with " "authentication application\"" msgstr "" -#: warehouse/templates/pages/help.html:325 +#: warehouse/templates/pages/help.html:334 msgid "" "PyPI will generate a secret key, specific to your account. This is " "displayed as a QR code, and as a text code." msgstr "" -#: warehouse/templates/pages/help.html:326 +#: warehouse/templates/pages/help.html:335 msgid "" "Scan the QR code with your authentication application, or type it in " "manually. The method of input will depend on the application you have " "chosen." msgstr "" -#: warehouse/templates/pages/help.html:327 +#: warehouse/templates/pages/help.html:336 msgid "" "Your application will generate an authentication code - use this to " "verify your set up on PyPI" msgstr "" -#: warehouse/templates/pages/help.html:330 +#: warehouse/templates/pages/help.html:339 msgid "" "The PyPI server and your application now share your PyPI secret key, " "allowing your application to generate valid authentication codes for your" " PyPI account." msgstr "" -#: warehouse/templates/pages/help.html:332 -#: warehouse/templates/pages/help.html:374 +#: warehouse/templates/pages/help.html:341 +#: warehouse/templates/pages/help.html:383 msgid "Next time you log in to PyPI you'll need to:" msgstr "" -#: warehouse/templates/pages/help.html:334 -#: warehouse/templates/pages/help.html:426 +#: warehouse/templates/pages/help.html:343 +#: warehouse/templates/pages/help.html:435 msgid "Provide your username and password, as normal" msgstr "" -#: warehouse/templates/pages/help.html:335 +#: warehouse/templates/pages/help.html:344 msgid "Open your authentication application to generate an authentication code" msgstr "" -#: warehouse/templates/pages/help.html:336 +#: warehouse/templates/pages/help.html:345 msgid "Use this code to finish logging into PyPI" msgstr "" -#: warehouse/templates/pages/help.html:342 +#: warehouse/templates/pages/help.html:351 msgid "" "A security device is a USB key or other " "device that generates a one-time password and sends that password to " @@ -3804,11 +3903,11 @@ msgid "" "user." msgstr "" -#: warehouse/templates/pages/help.html:344 +#: warehouse/templates/pages/help.html:353 msgid "To set up two factor authentication with a USB key, you'll need:" msgstr "" -#: warehouse/templates/pages/help.html:346 +#: warehouse/templates/pages/help.html:355 #, python-format msgid "" "To use a :" msgstr "" -#: warehouse/templates/pages/help.html:351 +#: warehouse/templates/pages/help.html:360 #, python-format msgid "" "Popular keys include Thetis." msgstr "" -#: warehouse/templates/pages/help.html:358 +#: warehouse/templates/pages/help.html:367 msgid "" "Note that some older Yubico USB keys do not follow the FIDO " "specification, and will therefore not work with PyPI" msgstr "" -#: warehouse/templates/pages/help.html:363 +#: warehouse/templates/pages/help.html:372 msgid "Follow these steps:" msgstr "" -#: warehouse/templates/pages/help.html:365 +#: warehouse/templates/pages/help.html:374 msgid "" "\n" "
  • Log in to your PyPI account, go to your account settings, " @@ -3863,13 +3962,13 @@ msgid "" " " msgstr "" -#: warehouse/templates/pages/help.html:372 +#: warehouse/templates/pages/help.html:381 msgid "" "Once complete, your USB key will be registered to your PyPI account and " "can be used during the log in process." msgstr "" -#: warehouse/templates/pages/help.html:376 +#: warehouse/templates/pages/help.html:385 msgid "" "\n" "
  • Provide your username and password, as normal
  • \n" @@ -3878,7 +3977,7 @@ msgid "" " " msgstr "" -#: warehouse/templates/pages/help.html:387 +#: warehouse/templates/pages/help.html:396 #, python-format msgid "" "There is a growing ecosystem of mobile phones to act as security devices." msgstr "" -#: warehouse/templates/pages/help.html:398 +#: warehouse/templates/pages/help.html:407 #, python-format msgid "" "As PyPI's two factor implementation follows the authentication " "application or security device, you can use " "these codes to sign into PyPI." msgstr "" -#: warehouse/templates/pages/help.html:410 +#: warehouse/templates/pages/help.html:419 msgid "" "Recovery codes are one time use. They are not a " "substitute for a authentication application or API tokens provide an alternative way (instead of username " @@ -3980,41 +4079,41 @@ msgid "" " " msgstr "" -#: warehouse/templates/pages/help.html:441 +#: warehouse/templates/pages/help.html:450 msgid "To make an API token:" msgstr "" -#: warehouse/templates/pages/help.html:444 +#: warehouse/templates/pages/help.html:453 msgid "Verify your email address" msgstr "" -#: warehouse/templates/pages/help.html:444 +#: warehouse/templates/pages/help.html:453 #, python-format msgid "(check your account settings)" msgstr "" -#: warehouse/templates/pages/help.html:445 +#: warehouse/templates/pages/help.html:454 #, python-format msgid "" "In your account settings, go to the API tokens " "section and select \"Add API token\"" msgstr "" -#: warehouse/templates/pages/help.html:448 +#: warehouse/templates/pages/help.html:457 msgid "To use an API token:" msgstr "" -#: warehouse/templates/pages/help.html:451 +#: warehouse/templates/pages/help.html:460 msgid "Set your username to __token__" msgstr "" -#: warehouse/templates/pages/help.html:452 +#: warehouse/templates/pages/help.html:461 msgid "" "Set your password to the token value, including the pypi- " "prefix" msgstr "" -#: warehouse/templates/pages/help.html:456 +#: warehouse/templates/pages/help.html:465 #, python-format msgid "" "Where you edit or add these values will depend on your individual use " @@ -4026,22 +4125,22 @@ msgid "" "rel=\"noopener\">.travis.yml if you are using Travis)." msgstr "" -#: warehouse/templates/pages/help.html:460 +#: warehouse/templates/pages/help.html:469 msgid "" "Advanced users may wish to inspect their token by decoding it with " "base64, and checking the output against the unique identifier displayed " "on PyPI." msgstr "" -#: warehouse/templates/pages/help.html:468 +#: warehouse/templates/pages/help.html:477 msgid "Yes, including RSS feeds of new packages and new releases." msgstr "" -#: warehouse/templates/pages/help.html:468 +#: warehouse/templates/pages/help.html:477 msgid "See the API reference." msgstr "" -#: warehouse/templates/pages/help.html:471 +#: warehouse/templates/pages/help.html:480 #, python-format msgid "" "If you need to run your own mirror of PyPI, the GitHub apps." msgstr "" -#: warehouse/templates/pages/help.html:477 +#: warehouse/templates/pages/help.html:486 #, python-format msgid "" "You can ." msgstr "" -#: warehouse/templates/pages/help.html:479 +#: warehouse/templates/pages/help.html:488 #, python-format msgid "" "other relevant factors." msgstr "" -#: warehouse/templates/pages/help.html:488 +#: warehouse/templates/pages/help.html:497 #, python-format msgid "" "For recent statistics on uptime and performance, see ." msgstr "" -#: warehouse/templates/pages/help.html:495 +#: warehouse/templates/pages/help.html:504 #, python-format msgid "" "PyPI does not support publishing private packages. If you need to publish" @@ -4099,7 +4198,7 @@ msgid "" "run your own deployment of the devpi project." msgstr "" -#: warehouse/templates/pages/help.html:498 +#: warehouse/templates/pages/help.html:507 msgid "" "Your publishing tool may return an error that your new project can't be " "created with your desired name, despite no evidence of a project or " @@ -4107,7 +4206,7 @@ msgid "" "reasons this may occur:" msgstr "" -#: warehouse/templates/pages/help.html:500 +#: warehouse/templates/pages/help.html:509 #, python-format msgid "" "The project name conflicts with a module from any major version from 2.5 to present." msgstr "" -#: warehouse/templates/pages/help.html:501 +#: warehouse/templates/pages/help.html:510 #, python-format msgid "" "The project name has been explicitly prohibited by the PyPI " @@ -4124,13 +4223,13 @@ msgid "" "with a malicious package." msgstr "" -#: warehouse/templates/pages/help.html:502 +#: warehouse/templates/pages/help.html:511 msgid "" "The project name has been registered by another user, but no releases " "have been created." msgstr "" -#: warehouse/templates/pages/help.html:506 +#: warehouse/templates/pages/help.html:515 #, python-format msgid "" "Follow the PEP 541." msgstr "" -#: warehouse/templates/pages/help.html:511 +#: warehouse/templates/pages/help.html:520 msgid "Owner:" msgstr "" -#: warehouse/templates/pages/help.html:514 +#: warehouse/templates/pages/help.html:523 msgid "" "Only the current owners of a project have the ability to add new owners " "or maintainers. If you need to request ownership, you should contact the " @@ -4151,12 +4250,12 @@ msgid "" "project page." msgstr "" -#: warehouse/templates/pages/help.html:515 +#: warehouse/templates/pages/help.html:524 #, python-format msgid "If the owner is unresponsive, see %(anchor_text)s" msgstr "" -#: warehouse/templates/pages/help.html:518 +#: warehouse/templates/pages/help.html:527 #, python-format msgid "" "By default, an upload's description will render with file an issue and tell us:

    " msgstr "" -#: warehouse/templates/pages/help.html:532 +#: warehouse/templates/pages/help.html:541 msgid "A link to your project on PyPI (or Test PyPI)" msgstr "" -#: warehouse/templates/pages/help.html:533 +#: warehouse/templates/pages/help.html:542 msgid "The size of your release, in megabytes" msgstr "" -#: warehouse/templates/pages/help.html:534 +#: warehouse/templates/pages/help.html:543 msgid "Which index/indexes you need the increase for (PyPI, Test PyPI, or both)" msgstr "" -#: warehouse/templates/pages/help.html:535 +#: warehouse/templates/pages/help.html:544 msgid "" "A brief description of your project, including the reason for the " "additional size." msgstr "" -#: warehouse/templates/pages/help.html:544 +#: warehouse/templates/pages/help.html:553 msgid "" "If you've forgotten your PyPI password but you remember your email " "address or username, follow these steps to reset your password:" msgstr "" -#: warehouse/templates/pages/help.html:546 +#: warehouse/templates/pages/help.html:555 #, python-format msgid "Go to reset your password." msgstr "" -#: warehouse/templates/pages/help.html:547 +#: warehouse/templates/pages/help.html:556 msgid "Enter the email address or username you used for PyPI and submit the form." msgstr "" -#: warehouse/templates/pages/help.html:548 +#: warehouse/templates/pages/help.html:557 msgid "You'll receive an email with a password reset link." msgstr "" -#: warehouse/templates/pages/help.html:553 +#: warehouse/templates/pages/help.html:562 msgid "If you've lost access to your PyPI account due to:" msgstr "" -#: warehouse/templates/pages/help.html:555 +#: warehouse/templates/pages/help.html:564 msgid "Lost access to the email address associated with your account" msgstr "" -#: warehouse/templates/pages/help.html:556 +#: warehouse/templates/pages/help.html:565 msgid "" "Lost two factor authentication application, device, and recovery " "codes" msgstr "" -#: warehouse/templates/pages/help.html:559 +#: warehouse/templates/pages/help.html:568 #, python-format msgid "" "You can proceed to API Token for uploads:" msgstr "" -#: warehouse/templates/pages/help.html:573 +#: warehouse/templates/pages/help.html:582 msgid "Ensure that your API Token is valid and has not been revoked." msgstr "" -#: warehouse/templates/pages/help.html:574 +#: warehouse/templates/pages/help.html:583 msgid "" "Ensure that your API Token is properly " "formatted and does not contain any trailing characters such as " "newlines." msgstr "" -#: warehouse/templates/pages/help.html:576 +#: warehouse/templates/pages/help.html:585 msgid "" "In both cases, remember that PyPI and TestPyPI each require you to create" " an account, so your credentials may be different." msgstr "" -#: warehouse/templates/pages/help.html:580 +#: warehouse/templates/pages/help.html:589 #, python-format msgid "" "Transport Layer Security, or TLS, is part of how we make sure connections" @@ -4304,7 +4403,7 @@ msgid "" "target=\"_blank\" rel=\"noopener\">Learn why on the PSF blog." msgstr "" -#: warehouse/templates/pages/help.html:587 +#: warehouse/templates/pages/help.html:596 #, python-format msgid "" "If you are having trouble with %(command)s and get a " @@ -4313,7 +4412,7 @@ msgid "" "information:" msgstr "" -#: warehouse/templates/pages/help.html:589 +#: warehouse/templates/pages/help.html:598 msgid "" "If you see an error like There was a problem confirming the ssl " "certificate or tlsv1 alert protocol version or " @@ -4321,7 +4420,7 @@ msgid "" "PyPI with a newer TLS support library." msgstr "" -#: warehouse/templates/pages/help.html:590 +#: warehouse/templates/pages/help.html:599 msgid "" "The specific steps you need to take will depend on your operating system " "version, where your installation of Python originated (python.org, your " @@ -4329,7 +4428,7 @@ msgid "" " Python, setuptools, and pip." msgstr "" -#: warehouse/templates/pages/help.html:592 +#: warehouse/templates/pages/help.html:601 #, python-format msgid "" "For help, go to %(command)s." msgstr "" -#: warehouse/templates/pages/help.html:603 +#: warehouse/templates/pages/help.html:612 #, python-format msgid "" "We take , so we can try to fix the problem, for you and others." msgstr "" -#: warehouse/templates/pages/help.html:616 +#: warehouse/templates/pages/help.html:625 #, python-format msgid "" "In a previous version of PyPI, it used to be possible for maintainers to " @@ -4368,7 +4467,7 @@ msgid "" "rel=\"noopener\">use twine to upload your project to PyPI." msgstr "" -#: warehouse/templates/pages/help.html:625 +#: warehouse/templates/pages/help.html:634 msgid "" "Spammers return to PyPI with some regularity hoping to place their Search" " Engine Optimized phishing, scam, and click-farming content on the site. " @@ -4377,7 +4476,7 @@ msgid "" "prime target." msgstr "" -#: warehouse/templates/pages/help.html:627 +#: warehouse/templates/pages/help.html:636 #, python-format msgid "" "When the PyPI administrators are overwhelmed by spam or " @@ -4388,29 +4487,29 @@ msgid "" "have updated it with reasoning for the intervention." msgstr "" -#: warehouse/templates/pages/help.html:636 +#: warehouse/templates/pages/help.html:645 msgid "PyPI will return these errors for one of these reasons:" msgstr "" -#: warehouse/templates/pages/help.html:638 +#: warehouse/templates/pages/help.html:647 msgid "Filename has been used and file exists" msgstr "" -#: warehouse/templates/pages/help.html:639 +#: warehouse/templates/pages/help.html:648 msgid "Filename has been used but file no longer exists" msgstr "" -#: warehouse/templates/pages/help.html:640 +#: warehouse/templates/pages/help.html:649 msgid "A file with the exact same content exists" msgstr "" -#: warehouse/templates/pages/help.html:642 +#: warehouse/templates/pages/help.html:651 msgid "" "PyPI does not allow for a filename to be reused, even once a project has " "been deleted and recreated." msgstr "" -#: warehouse/templates/pages/help.html:644 +#: warehouse/templates/pages/help.html:653 #, python-format msgid "" "To avoid this situation, pypi.org." msgstr "" -#: warehouse/templates/pages/help.html:651 +#: warehouse/templates/pages/help.html:660 #, python-format msgid "" "If you would like to request a new trove classifier file a pull request " @@ -4428,7 +4527,7 @@ msgid "" " to include a brief justification of why it is important." msgstr "" -#: warehouse/templates/pages/help.html:659 +#: warehouse/templates/pages/help.html:668 #, python-format msgid "" "If you're experiencing an issue with PyPI itself, we welcome " @@ -4439,14 +4538,14 @@ msgid "" " first check that a similar issue does not already exist." msgstr "" -#: warehouse/templates/pages/help.html:666 +#: warehouse/templates/pages/help.html:675 msgid "" "If you are having an issue is with a specific package installed from " "PyPI, you should reach out to the maintainers of that project directly " "instead." msgstr "" -#: warehouse/templates/pages/help.html:675 +#: warehouse/templates/pages/help.html:684 #, python-format msgid "" "PyPI is powered by the Warehouse project; ." msgstr "" -#: warehouse/templates/pages/help.html:702 +#: warehouse/templates/pages/help.html:711 msgid "" "As of April 16, 2018, PyPI.org is at \"production\" status, meaning that " "it has moved out of beta and replaced the old site (pypi.python.org). It " "is now robust, tested, and ready for expected browser and API traffic." msgstr "" -#: warehouse/templates/pages/help.html:704 +#: warehouse/templates/pages/help.html:713 #, python-format msgid "" "PyPI is heavily cached and distributed via private index." msgstr "" -#: warehouse/templates/pages/help.html:718 +#: warehouse/templates/pages/help.html:727 #, python-format msgid "" "We have a huge amount of work to do to continue to maintain and improve " @@ -4519,22 +4618,22 @@ msgid "" "target=\"_blank\" rel=\"noopener\">the Warehouse project)." msgstr "" -#: warehouse/templates/pages/help.html:723 +#: warehouse/templates/pages/help.html:732 msgid "Financial:" msgstr "" -#: warehouse/templates/pages/help.html:723 +#: warehouse/templates/pages/help.html:732 #, python-format msgid "" "We would deeply appreciate your donations to fund " "development and maintenance." msgstr "" -#: warehouse/templates/pages/help.html:724 +#: warehouse/templates/pages/help.html:733 msgid "Development:" msgstr "" -#: warehouse/templates/pages/help.html:724 +#: warehouse/templates/pages/help.html:733 msgid "" "Warehouse is open source, and we would love to see some new faces working" " on the project. You do not need to be an experienced " @@ -4542,7 +4641,7 @@ msgid "" " you make your first open source pull request!" msgstr "" -#: warehouse/templates/pages/help.html:726 +#: warehouse/templates/pages/help.html:735 #, python-format msgid "" "If you have skills in Python, ElasticSearch, HTML, SCSS, JavaScript, or " @@ -4556,7 +4655,7 @@ msgid "" "here." msgstr "" -#: warehouse/templates/pages/help.html:734 +#: warehouse/templates/pages/help.html:743 #, python-format msgid "" "Issues are grouped into PyPA Dev message group." msgstr "" -#: warehouse/templates/pages/help.html:752 +#: warehouse/templates/pages/help.html:761 #, python-format msgid "" "Changes to PyPI are generally announced on both the +
      +
    • The {{ project }} release {{ release }} released on {{ release_date }} has been un-yanked.
    • +
    • + Un-yanked by: {{ submitter }} with a role: {{ submitter_role }}. +
    • +
    +

    + +

    + If you have questions or concerns, you can email admin@pypi.org to communicate with the PyPI administrators. +

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

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

    +{% endblock %} diff --git a/warehouse/templates/email/unyanked-project-release/body.txt b/warehouse/templates/email/unyanked-project-release/body.txt new file mode 100644 index 000000000000..46fd7e335e37 --- /dev/null +++ b/warehouse/templates/email/unyanked-project-release/body.txt @@ -0,0 +1,27 @@ +{# + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +-#} + +{% extends "email/_base/body.txt" %} + +{% block content %} + The {{ project }} release {{ release }} released on {{ release_date }} has been un-yanked. + + Un-yanked by: {{ submitter }} with a role: {{ submitter_role }}. + + If you have questions or concerns, you can email admin@pypi.org to communicate with the PyPI administrators. +{% endblock %} + +{% block reason %} + You are receiving this because you are {{ recipient_role_descr }} of this project. +{% endblock %} diff --git a/warehouse/templates/email/unyanked-project-release/subject.txt b/warehouse/templates/email/unyanked-project-release/subject.txt new file mode 100644 index 000000000000..2ccfd8366627 --- /dev/null +++ b/warehouse/templates/email/unyanked-project-release/subject.txt @@ -0,0 +1,17 @@ +{# + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +-#} + +{% extends "email/_base/subject.txt" %} + +{% block subject %}{% trans project=project, release=release %}The {{ project }} release {{ release }} has been un-yanked.{% endtrans %}{% endblock %} diff --git a/warehouse/templates/email/yanked-project-release/body.html b/warehouse/templates/email/yanked-project-release/body.html new file mode 100644 index 000000000000..a87cc72fd902 --- /dev/null +++ b/warehouse/templates/email/yanked-project-release/body.html @@ -0,0 +1,34 @@ +{# + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +-#} +{% extends "email/_base/body.html" %} + +{% block content %} +

    +

      +
    • The {{ project }} release {{ release }} released on {{ release_date }} has been un-yanked.
    • +
    • Un-yanked by: {{ submitter }} with a role: {{ submitter_role }}. +
    • +
    +

    + +

    + If you have questions or concerns, you can email admin@pypi.org to communicate with the PyPI administrators. +

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

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

    +{% endblock %} diff --git a/warehouse/templates/email/yanked-project-release/body.txt b/warehouse/templates/email/yanked-project-release/body.txt new file mode 100644 index 000000000000..46fd7e335e37 --- /dev/null +++ b/warehouse/templates/email/yanked-project-release/body.txt @@ -0,0 +1,27 @@ +{# + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +-#} + +{% extends "email/_base/body.txt" %} + +{% block content %} + The {{ project }} release {{ release }} released on {{ release_date }} has been un-yanked. + + Un-yanked by: {{ submitter }} with a role: {{ submitter_role }}. + + If you have questions or concerns, you can email admin@pypi.org to communicate with the PyPI administrators. +{% endblock %} + +{% block reason %} + You are receiving this because you are {{ recipient_role_descr }} of this project. +{% endblock %} diff --git a/warehouse/templates/email/yanked-project-release/subject.txt b/warehouse/templates/email/yanked-project-release/subject.txt new file mode 100644 index 000000000000..d653bcf79cbb --- /dev/null +++ b/warehouse/templates/email/yanked-project-release/subject.txt @@ -0,0 +1,17 @@ +{# + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. +-#} + +{% extends "email/_base/subject.txt" %} + +{% block subject %}{% trans project=project, release=release %}The {{ project }} release {{ release }} has been yanked.{% endtrans %}{% endblock %} diff --git a/warehouse/templates/legacy/api/simple/detail.html b/warehouse/templates/legacy/api/simple/detail.html index 4f3d888f88f8..394fd9560021 100644 --- a/warehouse/templates/legacy/api/simple/detail.html +++ b/warehouse/templates/legacy/api/simple/detail.html @@ -19,7 +19,7 @@

    Links for {{ project.name }}

    {% for file in files -%} - {{ file.filename }}
    + {{ file.filename }}
    {% endfor -%} diff --git a/warehouse/templates/manage/manage_base.html b/warehouse/templates/manage/manage_base.html index fd90caab6c02..ed19e6e3dfbf 100644 --- a/warehouse/templates/manage/manage_base.html +++ b/warehouse/templates/manage/manage_base.html @@ -86,7 +86,8 @@ method="POST", warning=True, custom_warning_text="", - confirm_string_in_title="True") + confirm_string_in_title="True", + modifier="--danger") %}