diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index cdb2c632de98..4cc1b4deb7a2 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -13,13 +13,14 @@ import uuid import pretend +import pytest from pyramid.httpexceptions import HTTPSeeOther from webob.multidict import MultiDict from warehouse.manage import views from warehouse.accounts.interfaces import IUserService -from warehouse.packaging.models import JournalEntry, Role +from warehouse.packaging.models import JournalEntry, Project, Role from ...common.db.packaging import ProjectFactory, RoleFactory, UserFactory @@ -50,6 +51,84 @@ def test_manage_project_settings(self): "project": project, } + def test_delete_project_no_confirm(self): + project = pretend.stub(normalized_name='foo') + request = pretend.stub( + POST={}, + session=pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ), + route_path=lambda *a, **kw: "/foo/bar/", + ) + + with pytest.raises(HTTPSeeOther) as exc: + views.delete_project(project, request) + assert exc.value.status_code == 303 + assert exc.value.headers["Location"] == "/foo/bar/" + + assert request.session.flash.calls == [ + pretend.call("Must confirm the request.", queue="error"), + ] + + def test_delete_project_wrong_confirm(self): + project = pretend.stub(normalized_name='foo') + request = pretend.stub( + POST={"confirm": "bar"}, + session=pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ), + route_path=lambda *a, **kw: "/foo/bar/", + ) + + with pytest.raises(HTTPSeeOther) as exc: + views.delete_project(project, request) + assert exc.value.status_code == 303 + assert exc.value.headers["Location"] == "/foo/bar/" + + assert request.session.flash.calls == [ + pretend.call("'bar' is not the same as 'foo'", queue="error"), + ] + + def test_delete_project(self, db_request): + project = ProjectFactory.create(name="foo") + + 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), + ) + db_request.POST["confirm"] = project.normalized_name + db_request.user = UserFactory.create() + db_request.remote_addr = "192.168.1.1" + + result = views.delete_project(project, db_request) + + assert db_request.session.flash.calls == [ + pretend.call( + "Successfully deleted the project 'foo'.", + queue="success" + ), + ] + assert db_request.route_path.calls == [ + pretend.call('manage.projects'), + ] + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + assert not (db_request.db.query(Project) + .filter(Project.name == "foo").count()) + + +class TestManageProjectReleases: + + def test_manage_project_releases(self): + request = pretend.stub() + project = pretend.stub() + + assert views.manage_project_releases(project, request) == { + "project": project, + } + class TestManageProjectRoles: @@ -76,7 +155,6 @@ def test_get_manage_project_roles(self, db_request): pretend.call(db_request.POST, user_service=user_service), ] assert result == { - "project": project, "roles_by_user": {user.username: [role]}, "form": form_obj, @@ -252,7 +330,7 @@ def test_change_role(self, db_request): assert role.role_name == new_role_name assert db_request.route_path.calls == [ - pretend.call('manage.project.roles', name=project.name), + pretend.call('manage.project.roles', project_name=project.name), ] assert db_request.session.flash.calls == [ pretend.call("Successfully changed role", queue="success"), @@ -282,7 +360,7 @@ def test_change_role_invalid_role_name(self, pyramid_request): result = views.change_project_role(project, pyramid_request) assert pyramid_request.route_path.calls == [ - pretend.call('manage.project.roles', name=project.name), + pretend.call('manage.project.roles', project_name=project.name), ] assert isinstance(result, HTTPSeeOther) assert result.headers["Location"] == "/the-redirect" @@ -317,7 +395,7 @@ def test_change_role_when_multiple(self, db_request): assert db_request.db.query(Role).all() == [maintainer_role] assert db_request.route_path.calls == [ - pretend.call('manage.project.roles', name=project.name), + pretend.call('manage.project.roles', project_name=project.name), ] assert db_request.session.flash.calls == [ pretend.call("Successfully changed role", queue="success"), @@ -441,7 +519,7 @@ def test_delete_role(self, db_request): result = views.delete_project_role(project, db_request) assert db_request.route_path.calls == [ - pretend.call('manage.project.roles', name=project.name), + pretend.call('manage.project.roles', project_name=project.name), ] assert db_request.db.query(Role).all() == [] assert db_request.session.flash.calls == [ diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index 74070747ce5b..78eadaebdff0 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -144,30 +144,44 @@ def add_policy(name, filename): ), pretend.call( "manage.project.settings", - "/manage/project/{name}/settings/", + "/manage/project/{project_name}/settings/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", + domain=warehouse, + ), + pretend.call( + "manage.project.delete_project", + "/manage/project/{project_name}/delete_project/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", + domain=warehouse, + ), + pretend.call( + "manage.project.releases", + "/manage/project/{project_name}/releases/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", domain=warehouse, ), pretend.call( "manage.project.roles", - "/manage/project/{name}/collaboration/", + "/manage/project/{project_name}/collaboration/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", domain=warehouse, ), pretend.call( "manage.project.change_role", - "/manage/project/{name}/collaboration/change/", + "/manage/project/{project_name}/collaboration/change/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", domain=warehouse, ), pretend.call( "manage.project.delete_role", - "/manage/project/{name}/collaboration/delete/", + "/manage/project/{project_name}/collaboration/delete/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", domain=warehouse, ), pretend.call( diff --git a/tests/unit/admin/test_utils.py b/tests/unit/utils/test_project.py similarity index 91% rename from tests/unit/admin/test_utils.py rename to tests/unit/utils/test_project.py index 1909ccbf72d6..dc7fffaf84d6 100644 --- a/tests/unit/admin/test_utils.py +++ b/tests/unit/utils/test_project.py @@ -14,10 +14,10 @@ from pretend import call, call_recorder, stub from pyramid.httpexceptions import HTTPSeeOther -from warehouse.admin.utils import confirm_project, remove_project from warehouse.packaging.models import ( Project, Release, Dependency, File, Role, JournalEntry ) +from warehouse.utils.project import confirm_project, remove_project from ...common.db.accounts import UserFactory from ...common.db.packaging import ( @@ -34,7 +34,7 @@ def test_confirm(): session=stub(flash=call_recorder(lambda *a, **kw: stub())), ) - confirm_project(project, request) + confirm_project(project, request, fail_route='fail_route') assert request.route_path.calls == [] assert request.session.flash.calls == [] @@ -49,11 +49,11 @@ def test_confirm_no_input(): ) with pytest.raises(HTTPSeeOther) as err: - confirm_project(project, request) + confirm_project(project, request, fail_route='fail_route') assert err.value == '/the-redirect' assert request.route_path.calls == [ - call('admin.project.detail', project_name='foobar') + call('fail_route', project_name='foobar') ] assert request.session.flash.calls == [ call('Must confirm the request.', queue='error') @@ -69,11 +69,11 @@ def test_confirm_incorrect_input(): ) with pytest.raises(HTTPSeeOther) as err: - confirm_project(project, request) + confirm_project(project, request, fail_route='fail_route') assert err.value == '/the-redirect' assert request.route_path.calls == [ - call('admin.project.detail', project_name='foobar') + call('fail_route', project_name='foobar') ] assert request.session.flash.calls == [ call("'bizbaz' is not the same as 'foobar'", queue='error') diff --git a/warehouse/admin/views/blacklist.py b/warehouse/admin/views/blacklist.py index 309c5ab7b335..00f38f8699f8 100644 --- a/warehouse/admin/views/blacklist.py +++ b/warehouse/admin/views/blacklist.py @@ -20,12 +20,12 @@ from sqlalchemy.orm.exc import NoResultFound from warehouse.accounts.models import User -from warehouse.admin.utils import remove_project from warehouse.packaging.models import ( Project, Release, File, Role, BlacklistedProject ) from warehouse.utils.http import is_safe_url from warehouse.utils.paginate import paginate_url_factory +from warehouse.utils.project import remove_project @view_config( diff --git a/warehouse/admin/views/projects.py b/warehouse/admin/views/projects.py index 74f6140ea872..642748b33d65 100644 --- a/warehouse/admin/views/projects.py +++ b/warehouse/admin/views/projects.py @@ -22,9 +22,9 @@ from sqlalchemy import or_ from warehouse.accounts.models import User -from warehouse.admin.utils import confirm_project, remove_project from warehouse.packaging.models import Project, Release, Role, JournalEntry from warehouse.utils.paginate import paginate_url_factory +from warehouse.utils.project import confirm_project, remove_project from warehouse.forklift.legacy import MAX_FILESIZE ONE_MB = 1024 * 1024 # bytes @@ -265,7 +265,7 @@ def set_upload_limit(project, request): require_methods=False, ) def delete_project(project, request): - confirm_project(project, request) + confirm_project(project, request, fail_route="admin.project.detail") remove_project(project, request) return HTTPSeeOther(request.route_path('admin.project.list')) diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py index da721a3e748f..edb948ddb9a7 100644 --- a/warehouse/manage/views.py +++ b/warehouse/manage/views.py @@ -22,6 +22,7 @@ from warehouse.accounts.models import User from warehouse.manage.forms import CreateRoleForm, ChangeRoleForm from warehouse.packaging.models import JournalEntry, Role +from warehouse.utils.project import confirm_project, remove_project @view_config( @@ -46,7 +47,7 @@ def manage_projects(request): @view_config( route_name="manage.project.settings", - renderer="manage/project.html", + renderer="manage/settings.html", uses_session=True, permission="manage", effective_principals=Authenticated, @@ -55,6 +56,30 @@ def manage_project_settings(project, request): return {"project": project} +@view_config( + route_name="manage.project.delete_project", + uses_session=True, + require_methods=["POST"], + permission="manage", +) +def delete_project(project, request): + confirm_project(project, request, fail_route="manage.project.settings") + remove_project(project, request) + + return HTTPSeeOther(request.route_path('manage.projects')) + + +@view_config( + route_name="manage.project.releases", + renderer="manage/releases.html", + uses_session=True, + permission="manage", + effective_principals=Authenticated, +) +def manage_project_releases(project, request): + return {"project": project} + + @view_config( route_name="manage.project.roles", renderer="manage/roles.html", @@ -208,7 +233,7 @@ def change_project_role(project, request, _form_class=ChangeRoleForm): request.session.flash("Could not find role", queue="error") return HTTPSeeOther( - request.route_path('manage.project.roles', name=project.name) + request.route_path('manage.project.roles', project_name=project.name) ) @@ -253,5 +278,5 @@ def delete_project_role(project, request): request.session.flash("Successfully removed role", queue="success") return HTTPSeeOther( - request.route_path('manage.project.roles', name=project.name) + request.route_path('manage.project.roles', project_name=project.name) ) diff --git a/warehouse/routes.py b/warehouse/routes.py index 05beb2f14d28..c6891e57f81f 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -110,30 +110,44 @@ def includeme(config): config.add_route("manage.projects", "/manage/projects/", domain=warehouse) config.add_route( "manage.project.settings", - "/manage/project/{name}/settings/", + "/manage/project/{project_name}/settings/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", + domain=warehouse, + ) + config.add_route( + "manage.project.delete_project", + "/manage/project/{project_name}/delete_project/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", + domain=warehouse, + ) + config.add_route( + "manage.project.releases", + "/manage/project/{project_name}/releases/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", domain=warehouse, ) config.add_route( "manage.project.roles", - "/manage/project/{name}/collaboration/", + "/manage/project/{project_name}/collaboration/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", domain=warehouse, ) config.add_route( "manage.project.change_role", - "/manage/project/{name}/collaboration/change/", + "/manage/project/{project_name}/collaboration/change/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", domain=warehouse, ) config.add_route( "manage.project.delete_role", - "/manage/project/{name}/collaboration/delete/", + "/manage/project/{project_name}/collaboration/delete/", factory="warehouse.packaging.models:ProjectFactory", - traverse="/{name}", + traverse="/{project_name}", domain=warehouse, ) diff --git a/warehouse/static/sass/blocks/_modal.scss b/warehouse/static/sass/blocks/_modal.scss index e58bc3a3318f..546cda8790de 100644 --- a/warehouse/static/sass/blocks/_modal.scss +++ b/warehouse/static/sass/blocks/_modal.scss @@ -33,7 +33,6 @@ */ - .modal { @include overlay; opacity: 0; @@ -41,6 +40,7 @@ display: flex; align-items: center; justify-content: center; + flex-grow: 1; &:target { opacity: 1; @@ -56,12 +56,19 @@ overflow: auto; background: $white; position: relative; + margin: auto; } &__body { padding: $spacing-unit; } + @media screen and (max-width: $tablet) { + &__body { + padding: $spacing-unit / 2; + } + } + &__title { font-size: 1.5rem; } @@ -99,6 +106,7 @@ input { width: 100%; + min-width: 100%; margin: 5px 0 20px 0; } diff --git a/warehouse/static/sass/blocks/_package-snippet.scss b/warehouse/static/sass/blocks/_package-snippet.scss index 3a8fe1fdddd8..fba23c04f768 100644 --- a/warehouse/static/sass/blocks/_package-snippet.scss +++ b/warehouse/static/sass/blocks/_package-snippet.scss @@ -64,15 +64,23 @@ } &__buttons { - width: 165px; + width: 140px; .button { display: inline-block; float: left; } - .button--highlight { + .button--primary { margin-right: 5px; } } + + &--margin-bottom { + margin-bottom: 0; + + @media only screen and (max-width: $tablet) { + margin-bottom: 30px; + } + } } diff --git a/warehouse/static/sass/blocks/_project-description.scss b/warehouse/static/sass/blocks/_project-description.scss index 2f710717399d..9aadde35b2b0 100644 --- a/warehouse/static/sass/blocks/_project-description.scss +++ b/warehouse/static/sass/blocks/_project-description.scss @@ -25,13 +25,6 @@ margin-bottom: $spacing-unit; line-height: 1.5; - // Remove top margin on first heading, even if it nested inside a div - > :first-child, - > div:first-child > :first-child { - display: inline-block; - margin-top: 0 !important; - } - h1, h2, h3, @@ -89,6 +82,10 @@ font-style: italic; padding-left: ($spacing-unit / 2); color: lighten($text-color, 20); + + @media only screen and (max-width: $tablet) { + margin-left: 0; + } } dl { diff --git a/warehouse/static/sass/blocks/_tabs.scss b/warehouse/static/sass/blocks/_tabs.scss deleted file mode 100644 index ce76d52baf6d..000000000000 --- a/warehouse/static/sass/blocks/_tabs.scss +++ /dev/null @@ -1,70 +0,0 @@ -/*! - * 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. - */ - -/* - Set of links styled as tabs. - - -*/ - -$tabs-color: darken($base-grey, 10); - -.tabs { - border-bottom: 2px solid $tabs-color; - @include clearfix; - margin-top: $spacing-unit; - position: relative; - - &__tab { - display: inline-block; - float: left; - line-height: 41px; - padding: 0 $spacing-unit / 2; - font-size: 17px; - font-weight: 600; - border-radius: 5px 5px 0 0; - border: 2px solid $tabs-color; - margin-right: 5px; - position: relative; - top: 2px; - background-color: #fafafa; - - &:focus, - &:hover, - &:active { - border-color: $brand-color; - border-bottom-color: $tabs-color; - text-decoration: none; - outline: none; - } - - &--is-active { - border-bottom-color: $white; - color: lighten($text-color, 10); - background-color: $white; - - &:focus, - &:hover, - &:active { - border-color: $tabs-color; - text-decoration: none; - color: lighten($text-color, 10); - border-bottom-color: $white; - } - } - } -} diff --git a/warehouse/static/sass/blocks/_vertical-tabs.scss b/warehouse/static/sass/blocks/_vertical-tabs.scss index 13bbe5e447c1..fda4d381b39f 100644 --- a/warehouse/static/sass/blocks/_vertical-tabs.scss +++ b/warehouse/static/sass/blocks/_vertical-tabs.scss @@ -39,10 +39,14 @@ overflow: hidden; margin: $spacing-unit 0; + @media only screen and (max-width: $tablet) { + margin: 0; + } + &__tabs { @include span-columns(3); - @media only screen and (max-width: $tablet){ + @media only screen and (max-width: $tablet) { display: none; } } @@ -59,8 +63,7 @@ &--mobile { display: none; - - @media only screen and (max-width: $tablet){ + @media only screen and (max-width: $tablet) { display: block; border-top: 1px solid $border-color; @@ -70,6 +73,12 @@ } } + &--no-top-border { + @media only screen and (max-width: $tablet) { + border-top: 0; + } + } + &--is-active, &--is-active:hover { background: $brand-color; @@ -94,7 +103,7 @@ &__panel { @include span-columns(9); - @media only screen and (max-width: $tablet){ + @media only screen and (max-width: $tablet) { width: 100%; } } @@ -102,8 +111,8 @@ &__content { padding-left: $spacing-unit; - @media only screen and (max-width: $tablet){ - padding: $spacing-unit ($spacing-unit / 2); + @media only screen and (max-width: $tablet) { + padding: 25px 10px 10px 10px; } } } diff --git a/warehouse/static/sass/layout-helpers/_containers.scss b/warehouse/static/sass/layout-helpers/_containers.scss index eab33705b0da..b93b7178ca2f 100644 --- a/warehouse/static/sass/layout-helpers/_containers.scss +++ b/warehouse/static/sass/layout-helpers/_containers.scss @@ -21,6 +21,18 @@ @include site-container; } +/* + Container for vertical tabs +*/ + +.tabs-container { + @include site-container; + + @media only screen and (max-width: $tablet){ + padding: 5px; + } +} + /* Narrow container for text only pages:
diff --git a/warehouse/static/sass/tools/_typography.scss b/warehouse/static/sass/tools/_typography.scss index ef825b75cf9b..e6e5d72bebf0 100644 --- a/warehouse/static/sass/tools/_typography.scss +++ b/warehouse/static/sass/tools/_typography.scss @@ -31,7 +31,7 @@ // h1 for all other pages @mixin h1-title { - font-size: 1.9rem; + font-size: 1.5rem; font-weight: $bold-font-weight; } diff --git a/warehouse/static/sass/warehouse.scss b/warehouse/static/sass/warehouse.scss index 6b7b4845b3fb..f5b5493c934f 100644 --- a/warehouse/static/sass/warehouse.scss +++ b/warehouse/static/sass/warehouse.scss @@ -102,7 +102,6 @@ @import "blocks/status-badge"; @import "blocks/statistics-bar"; @import "blocks/table"; -@import "blocks/tabs"; @import "blocks/tooltip"; @import "blocks/vertical-tabs"; @import "blocks/viewport-section"; @@ -143,7 +142,6 @@ // Adjust size of h1s on standalone pages .page-title { - @include h2; @include h1-title; } diff --git a/warehouse/templates/includes/edit-project-button.html b/warehouse/templates/includes/edit-project-button.html index 636612ed91d9..5ea96acada81 100644 --- a/warehouse/templates/includes/edit-project-button.html +++ b/warehouse/templates/includes/edit-project-button.html @@ -13,5 +13,5 @@ -#} {% if request.user %} - Edit Project + Edit Project {% endif %} diff --git a/warehouse/templates/manage/manage_base.html b/warehouse/templates/manage/manage_base.html index 38c2e5f6dd3d..9995ec8f5297 100644 --- a/warehouse/templates/manage/manage_base.html +++ b/warehouse/templates/manage/manage_base.html @@ -16,28 +16,38 @@ {% block title %}Manage{% endblock %} {% block content %} -{{ release.summary }}
+ {% endif %} +{{ release.summary }}
- {% endif %} -Version | -Release Date | -Summary | -- - - {% for release in project.releases %} - |
---|---|---|---|
- {# TODO: https://github.com/pypa/warehouse/issues/2807 {{ release.version }} #} - {{ release.version }} - | -- | - {% if release.summary %} - {{ release.summary }} - {% else %} - — - {% endif %} - | -- | - -
TODO: Some help text here
-Warning: This action cannot be undone!
-Confirm the project name and enter your password to continue.
- -Warning: This action cannot be undone!
-Enter your password to continue.
- -Version | +Release Date | +Summary | ++ + + {% for release in project.releases %} + |
---|---|---|---|
+ {# TODO: https://github.com/pypa/warehouse/issues/2807 {{ release.version }} #} + {{ release.version }} + | ++ | + {% if release.summary %} + {{ release.summary }} + {% else %} + — + {% endif %} + | ++ | + +
TODO: Some help text here
+Warning: This action cannot be undone!
+Enter your password to continue.
+ +Use this page to control which PyPI users can help you to manage '{{ project.name }}'.
@@ -28,7 +29,6 @@User | @@ -60,7 +60,7 @@
---|