From 9e70f47a58c0bfb895324e34e48e36c6e7c3f0bf Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Fri, 15 Dec 2017 11:53:43 -0600 Subject: [PATCH 01/26] Add 'manage' permission --- tests/unit/packaging/test_models.py | 4 ++-- warehouse/packaging/models.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py index 7529f31a6658..6b1a89a98905 100644 --- a/tests/unit/packaging/test_models.py +++ b/tests/unit/packaging/test_models.py @@ -95,8 +95,8 @@ def test_acl(self, db_session): assert project.__acl__() == [ (Allow, "group:admins", "admin"), - (Allow, owner1.user.id, ["upload"]), - (Allow, owner2.user.id, ["upload"]), + (Allow, owner1.user.id, ["manage", "upload"]), + (Allow, owner2.user.id, ["manage", "upload"]), (Allow, maintainer1.user.id, ["upload"]), (Allow, maintainer2.user.id, ["upload"]), ] diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py index e8c240552b93..0d1decfa9687 100644 --- a/warehouse/packaging/models.py +++ b/warehouse/packaging/models.py @@ -147,8 +147,10 @@ def __acl__(self): for role in sorted( query.all(), key=lambda x: ["Owner", "Maintainer"].index(x.role_name)): - acls.append((Allow, role.user.id, ["upload"])) - + if role.role_name == "Owner": + acls.append((Allow, role.user.id, ["manage", "upload"])) + else: + acls.append((Allow, role.user.id, ["upload"])) return acls @property From 54b8f32c5aa55fbd42c614b632fb04d4f7b08f10 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Fri, 15 Dec 2017 13:22:26 -0600 Subject: [PATCH 02/26] Always store principals as strings Before, these were always UUID objects, but since #1329 this is stored as a string for session-based authentication only. To keep everything consistent, always use strings over UUID objects. --- tests/unit/accounts/test_auth_policy.py | 5 +++-- tests/unit/packaging/test_models.py | 8 ++++---- warehouse/accounts/auth_policy.py | 2 +- warehouse/packaging/models.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/unit/accounts/test_auth_policy.py b/tests/unit/accounts/test_auth_policy.py index ff01d6d55d3f..f116ecfb5968 100644 --- a/tests/unit/accounts/test_auth_policy.py +++ b/tests/unit/accounts/test_auth_policy.py @@ -11,6 +11,7 @@ # limitations under the License. import pretend +import uuid from pyramid import authentication from pyramid.interfaces import IAuthenticationPolicy @@ -74,7 +75,7 @@ def test_unauthenticated_userid_with_userid(self, monkeypatch): add_vary_cb = pretend.call_recorder(lambda *v: vary_cb) monkeypatch.setattr(auth_policy, "add_vary_callback", add_vary_cb) - userid = pretend.stub() + userid = uuid.uuid4() service = pretend.stub( find_userid=pretend.call_recorder(lambda username: userid), ) @@ -83,7 +84,7 @@ def test_unauthenticated_userid_with_userid(self, monkeypatch): add_response_callback=pretend.call_recorder(lambda cb: None), ) - assert policy.unauthenticated_userid(request) is userid + assert policy.unauthenticated_userid(request) == str(userid) assert extract_http_basic_credentials.calls == [pretend.call(request)] assert request.find_service.calls == [ pretend.call(IUserService, context=None), diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py index 6b1a89a98905..ffc80f5d219f 100644 --- a/tests/unit/packaging/test_models.py +++ b/tests/unit/packaging/test_models.py @@ -95,10 +95,10 @@ def test_acl(self, db_session): assert project.__acl__() == [ (Allow, "group:admins", "admin"), - (Allow, owner1.user.id, ["manage", "upload"]), - (Allow, owner2.user.id, ["manage", "upload"]), - (Allow, maintainer1.user.id, ["upload"]), - (Allow, maintainer2.user.id, ["upload"]), + (Allow, str(owner1.user.id), ["manage", "upload"]), + (Allow, str(owner2.user.id), ["manage", "upload"]), + (Allow, str(maintainer1.user.id), ["upload"]), + (Allow, str(maintainer2.user.id), ["upload"]), ] diff --git a/warehouse/accounts/auth_policy.py b/warehouse/accounts/auth_policy.py index cd2db2defb97..2b416ac55f08 100644 --- a/warehouse/accounts/auth_policy.py +++ b/warehouse/accounts/auth_policy.py @@ -34,7 +34,7 @@ def unauthenticated_userid(self, request): # want to locate the userid from the IUserService. if username is not None: login_service = request.find_service(IUserService, context=None) - return login_service.find_userid(username) + return str(login_service.find_userid(username)) class SessionAuthenticationPolicy(_SessionAuthenticationPolicy): diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py index 0d1decfa9687..182fbd62a65e 100644 --- a/warehouse/packaging/models.py +++ b/warehouse/packaging/models.py @@ -148,9 +148,9 @@ def __acl__(self): query.all(), key=lambda x: ["Owner", "Maintainer"].index(x.role_name)): if role.role_name == "Owner": - acls.append((Allow, role.user.id, ["manage", "upload"])) + acls.append((Allow, str(role.user.id), ["manage", "upload"])) else: - acls.append((Allow, role.user.id, ["upload"])) + acls.append((Allow, str(role.user.id), ["upload"])) return acls @property From c623660cfafc3bb9040be5d823086f1a3a56b7ff Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Dec 2017 14:04:36 -0500 Subject: [PATCH 03/26] Split profile and project management This gives the logged in user a place to manage their profile, and a place to manage their projects. Mostly stubbed out for now. --- tests/unit/manage/test_views.py | 42 +++++++++++++++++ tests/unit/test_config.py | 1 + tests/unit/test_routes.py | 17 +++++++ warehouse/config.py | 3 ++ warehouse/manage/__init__.py | 2 + warehouse/manage/views.py | 45 ++++++++++++++++++ warehouse/routes.py | 11 +++++ .../includes/current-user-indicator.html | 6 ++- warehouse/templates/manage/profile.html | 43 +++++++++++++++++ warehouse/templates/manage/project.html | 25 ++++++++++ .../manage/project_settings_base.html | 35 ++++++++++++++ warehouse/templates/manage/projects.html | 46 +++++++++++++++++++ warehouse/views.py | 5 +- 13 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 tests/unit/manage/test_views.py create mode 100644 warehouse/manage/__init__.py create mode 100644 warehouse/manage/views.py create mode 100644 warehouse/templates/manage/profile.html create mode 100644 warehouse/templates/manage/project.html create mode 100644 warehouse/templates/manage/project_settings_base.html create mode 100644 warehouse/templates/manage/projects.html diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py new file mode 100644 index 000000000000..572c159de35a --- /dev/null +++ b/tests/unit/manage/test_views.py @@ -0,0 +1,42 @@ +# 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. + +import pretend + +from warehouse.manage import views + + +class TestManageProfile: + + def test_manage_profile(self): + request = pretend.stub() + + assert views.manage_profile(request) == {} + + +class TestManageProjects: + + def test_manage_projects(self): + request = pretend.stub() + + assert views.manage_projects(request) == {} + + +class TestManageProjectSettings: + + def test_manage_project_settings(self): + request = pretend.stub() + project = pretend.stub() + + assert views.manage_project_settings(project, request) == { + "project": project, + } diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index bf20885f8066..f918e5f1352d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -336,6 +336,7 @@ def __init__(self): pretend.call(".cache.http"), pretend.call(".cache.origin"), pretend.call(".accounts"), + pretend.call(".manage"), pretend.call(".packaging"), pretend.call(".redirects"), pretend.call(".routes"), diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index a6b5c107e46f..e17162d5389f 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -132,6 +132,23 @@ def add_policy(name, filename): traverse="/{username}", domain=warehouse, ), + pretend.call( + "manage.profile", + "/manage/profile/", + domain=warehouse + ), + pretend.call( + "manage.projects", + "/manage/projects/", + domain=warehouse + ), + pretend.call( + "manage.project.settings", + "/project/{name}/settings/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{name}", + domain=warehouse, + ), pretend.call( "packaging.project", "/project/{name}/", diff --git a/warehouse/config.py b/warehouse/config.py index 882c8825efe1..dbd8cb604ae1 100644 --- a/warehouse/config.py +++ b/warehouse/config.py @@ -362,6 +362,9 @@ def configure(settings=None): # Register our authentication support. config.include(".accounts") + # Register logged-in views + config.include(".manage") + # Allow the packaging app to register any services it has. config.include(".packaging") diff --git a/warehouse/manage/__init__.py b/warehouse/manage/__init__.py new file mode 100644 index 000000000000..16392bcb45ef --- /dev/null +++ b/warehouse/manage/__init__.py @@ -0,0 +1,2 @@ +def includeme(config): + pass diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py new file mode 100644 index 000000000000..44720aa7716b --- /dev/null +++ b/warehouse/manage/views.py @@ -0,0 +1,45 @@ +# 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. + +from pyramid.security import Authenticated +from pyramid.view import view_config + + +@view_config( + route_name="manage.profile", + renderer="manage/profile.html", + uses_session=True, + effective_principals=Authenticated, +) +def manage_profile(request): + return {} + + +@view_config( + route_name="manage.projects", + renderer="manage/projects.html", + uses_session=True, + effective_principals=Authenticated, +) +def manage_projects(request): + return {} + + +@view_config( + route_name="manage.project.settings", + renderer="manage/project.html", + uses_session=True, + permission="manage", + effective_principals=Authenticated, +) +def manage_project_settings(project, request): + return {"project": project} diff --git a/warehouse/routes.py b/warehouse/routes.py index e9a956bf158e..2ad2c682d403 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -105,6 +105,17 @@ def includeme(config): domain=warehouse, ) + # Management (views for logged-in users) + config.add_route("manage.profile", "/manage/profile/", domain=warehouse) + config.add_route("manage.projects", "/manage/projects/", domain=warehouse) + config.add_route( + "manage.project.settings", + "/project/{name}/settings/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{name}", + domain=warehouse, + ) + # Packaging config.add_route( "packaging.project", diff --git a/warehouse/templates/includes/current-user-indicator.html b/warehouse/templates/includes/current-user-indicator.html index 952a366498e1..5aa63b61f8c5 100644 --- a/warehouse/templates/includes/current-user-indicator.html +++ b/warehouse/templates/includes/current-user-indicator.html @@ -29,10 +29,14 @@ Admin {% endif %} - + Your Projects + + + Your Profile + Get Help diff --git a/warehouse/templates/manage/profile.html b/warehouse/templates/manage/profile.html new file mode 100644 index 000000000000..29474c44e8bb --- /dev/null +++ b/warehouse/templates/manage/profile.html @@ -0,0 +1,43 @@ +{# + # 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 "base.html" %} + +{% set user = request.user %} +{% set title = "Manage Your Profile" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+

{{ title }}

+
+
+ {% set alt = "Avatar for {} from gravatar.com".format(user.name|default(user.username, true)) %} + {{ alt }} + Change this image +

{{ user.name|default(user.username, true) }}

+
+ {% if user.name %} +

  {{ user.username }}

+ {% endif %} + {% if user.date_joined %} +

  Joined on {{ user.date_joined|format_date() }}

+ {% endif %} +
+
+
+
+
+{% endblock %} diff --git a/warehouse/templates/manage/project.html b/warehouse/templates/manage/project.html new file mode 100644 index 000000000000..334f0c4a8bb1 --- /dev/null +++ b/warehouse/templates/manage/project.html @@ -0,0 +1,25 @@ +{# + # 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 "project_settings_base.html" %} + +{% block title %}{{ project.name }}{% endblock %} + +{% block main %} +

Options

+

{{ project.name }}

+

+ Last released on {{ project.releases[0].created|format_date() }} +

+

{{ project.releases[0].summary }}

+{% endblock %} diff --git a/warehouse/templates/manage/project_settings_base.html b/warehouse/templates/manage/project_settings_base.html new file mode 100644 index 000000000000..1a6d7742f2ac --- /dev/null +++ b/warehouse/templates/manage/project_settings_base.html @@ -0,0 +1,35 @@ +{# + # 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 "base.html" %} + +{% block title %}{{ project.name }}{% endblock %} + +{% block content %} +
+
+

{{ project.name }}

+
+ +
+
+ {% block main %}{% endblock %} +
+
+
+ +{% endblock %} diff --git a/warehouse/templates/manage/projects.html b/warehouse/templates/manage/projects.html new file mode 100644 index 000000000000..fe95a96536fa --- /dev/null +++ b/warehouse/templates/manage/projects.html @@ -0,0 +1,46 @@ +{# + # 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 "base.html" %} + +{% set title = "Manage Your Projects" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+

{{ title }}

+
+ {% if request.user.projects %} + {% for project in request.user.projects %} + {% set release = project.releases[0] %} +
+

{{ project.name }}

+

+ Last released on {{ release.created|format_date() }} +

+

{{ release.summary }}

+
+ {% endfor %} + {% else %} +
+

You have not uploaded any projects to PyPI, yet. To learn how to get started, visit the Python Packaging User Guide

+
+ {% endif %} +
+
+
+ + +{% endblock %} diff --git a/warehouse/views.py b/warehouse/views.py index 841da776a901..c157f502b587 100644 --- a/warehouse/views.py +++ b/warehouse/views.py @@ -16,10 +16,12 @@ HTTPException, HTTPSeeOther, HTTPMovedPermanently, HTTPNotFound, HTTPBadRequest, exception_response, ) +from pyramid.exceptions import PredicateMismatch from pyramid.renderers import render_to_response from pyramid.response import Response from pyramid.view import ( - notfound_view_config, forbidden_view_config, view_config, + notfound_view_config, forbidden_view_config, exception_view_config, + view_config, ) from elasticsearch_dsl import Q from sqlalchemy import func @@ -107,6 +109,7 @@ def httpexception_view(exc, request): @forbidden_view_config() +@exception_view_config(PredicateMismatch) def forbidden(exc, request, redirect_to="accounts.login"): # If the forbidden error is because the user isn't logged in, then we'll # redirect them to the log in page. From f8d179eb109adf110cb14592317114402b586492 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Dec 2017 14:05:35 -0500 Subject: [PATCH 04/26] Put the gravatar link on the 'Manage Profile' page This no longer needs to be a client-side include because we can just edit it via profile management when the user is logged in. --- tests/unit/accounts/test_views.py | 9 --------- tests/unit/test_routes.py | 7 ------- warehouse/accounts/views.py | 9 --------- warehouse/routes.py | 7 ------- .../accounts/csi/edit_gravatar.csi.html | 17 ----------------- warehouse/templates/accounts/profile.html | 2 -- 6 files changed, 51 deletions(-) delete mode 100644 warehouse/templates/accounts/csi/edit_gravatar.csi.html diff --git a/tests/unit/accounts/test_views.py b/tests/unit/accounts/test_views.py index 98d37a8bfc41..9bbacc6cf6f2 100644 --- a/tests/unit/accounts/test_views.py +++ b/tests/unit/accounts/test_views.py @@ -559,15 +559,6 @@ def test_reset_password(self, db_request, user_service, token_service): ] -class TestClientSideIncludes: - - def test_edit_gravatar_csi_returns_user(self, db_request): - user = UserFactory.create() - assert views.edit_gravatar_csi(user, db_request) == { - "user": user, - } - - class TestProfileCallout: def test_profile_callout_returns_user(self): diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index e17162d5389f..2c1041d4637f 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -125,13 +125,6 @@ def add_policy(name, filename): "/account/reset-password/", domain=warehouse, ), - pretend.call( - "accounts.edit_gravatar", - "/user/{username}/edit_gravatar/", - factory="warehouse.accounts.models:UserFactory", - traverse="/{username}", - domain=warehouse, - ), pretend.call( "manage.profile", "/manage/profile/", diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py index 0b413a987b3a..62db5d1ad2c8 100644 --- a/warehouse/accounts/views.py +++ b/warehouse/accounts/views.py @@ -323,15 +323,6 @@ def reset_password(request, _form_class=ResetPasswordForm): return {"form": form} -@view_config( - route_name="accounts.edit_gravatar", - renderer="accounts/csi/edit_gravatar.csi.html", - uses_session=True, -) -def edit_gravatar_csi(user, request): - return {"user": user} - - def _login_user(request, userid): # We have a session factory associated with this request, so in order # to protect against session fixation attacks we're going to make sure diff --git a/warehouse/routes.py b/warehouse/routes.py index 2ad2c682d403..f882ca2f9a78 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -97,13 +97,6 @@ def includeme(config): "/account/reset-password/", domain=warehouse, ) - config.add_route( - "accounts.edit_gravatar", - "/user/{username}/edit_gravatar/", - factory="warehouse.accounts.models:UserFactory", - traverse="/{username}", - domain=warehouse, - ) # Management (views for logged-in users) config.add_route("manage.profile", "/manage/profile/", domain=warehouse) diff --git a/warehouse/templates/accounts/csi/edit_gravatar.csi.html b/warehouse/templates/accounts/csi/edit_gravatar.csi.html deleted file mode 100644 index 60a02b01bf0a..000000000000 --- a/warehouse/templates/accounts/csi/edit_gravatar.csi.html +++ /dev/null @@ -1,17 +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 own_profile = request.user and request.user.username == user.username %} -{% if own_profile %} -Change this image -{% endif %} diff --git a/warehouse/templates/accounts/profile.html b/warehouse/templates/accounts/profile.html index 91a3f10bf869..1663d5264694 100644 --- a/warehouse/templates/accounts/profile.html +++ b/warehouse/templates/accounts/profile.html @@ -22,8 +22,6 @@
{% set alt = "Avatar for {} from gravatar.com".format(user.name|default(user.username, true)) %} {{ alt }} - {% csi request.route_path('accounts.edit_gravatar', username=user.username) %} - {% endcsi %}

{{ user.name|default(user.username, true) }}

{% if user.name %} From b16bdc3efc63d5f39bcd6c26e5205fd5e06351d7 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Dec 2017 14:07:29 -0500 Subject: [PATCH 05/26] Role management Adding and deleting roles --- tests/unit/manage/__init__.py | 11 + tests/unit/manage/test_forms.py | 71 +++++ tests/unit/manage/test_views.py | 251 ++++++++++++++++++ tests/unit/test_routes.py | 14 + warehouse/manage/__init__.py | 13 + warehouse/manage/forms.py | 46 ++++ warehouse/manage/views.py | 97 +++++++ warehouse/routes.py | 14 + .../manage/project_settings_base.html | 3 + warehouse/templates/manage/roles.html | 78 ++++++ 10 files changed, 598 insertions(+) create mode 100644 tests/unit/manage/__init__.py create mode 100644 tests/unit/manage/test_forms.py create mode 100644 warehouse/manage/forms.py create mode 100644 warehouse/templates/manage/roles.html diff --git a/tests/unit/manage/__init__.py b/tests/unit/manage/__init__.py new file mode 100644 index 000000000000..164f68b09175 --- /dev/null +++ b/tests/unit/manage/__init__.py @@ -0,0 +1,11 @@ +# 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. diff --git a/tests/unit/manage/test_forms.py b/tests/unit/manage/test_forms.py new file mode 100644 index 000000000000..8b637f658f9c --- /dev/null +++ b/tests/unit/manage/test_forms.py @@ -0,0 +1,71 @@ +# 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. + +import pretend +import pytest +import wtforms + +from webob.multidict import MultiDict + +from warehouse.manage import forms + + +class TestCreateRoleForm: + + def test_creation(self): + user_service = pretend.stub() + form = forms.CreateRoleForm(user_service=user_service) + + assert form.user_service is user_service + + def test_validate_username_with_no_user(self): + user_service = pretend.stub( + find_userid=pretend.call_recorder(lambda userid: None), + ) + form = forms.CreateRoleForm(user_service=user_service) + field = pretend.stub(data="my_username") + + with pytest.raises(wtforms.validators.ValidationError): + form.validate_username(field) + + assert user_service.find_userid.calls == [pretend.call("my_username")] + + def test_validate_username_with_user(self): + user_service = pretend.stub( + find_userid=pretend.call_recorder(lambda userid: 1), + ) + form = forms.CreateRoleForm(user_service=user_service) + field = pretend.stub(data="my_username") + + form.validate_username(field) + + assert user_service.find_userid.calls == [pretend.call("my_username")] + + @pytest.mark.parametrize(("value", "expected"), [ + ("", "Must select a role"), + ("invalid", "Not a valid choice"), + (None, "Not a valid choice"), + ]) + def test_validate_role_name_fails(self, value, expected): + user_service = pretend.stub( + find_userid=pretend.call_recorder(lambda userid: 1), + ) + form = forms.CreateRoleForm( + MultiDict({ + 'role_name': value, + 'username': 'valid_username', + }), + user_service=user_service, + ) + + assert not form.validate() + assert form.role_name.errors == [expected] diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index 572c159de35a..ca530b5a2214 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -10,9 +10,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import uuid + import pretend +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 Role + +from ...common.db.packaging import ProjectFactory, RoleFactory, UserFactory class TestManageProfile: @@ -40,3 +49,245 @@ def test_manage_project_settings(self): assert views.manage_project_settings(project, request) == { "project": project, } + + +class TestManageProjectRoles: + + def test_get_manage_project_roles(self, db_request): + user_service = pretend.stub() + db_request.find_service = pretend.call_recorder( + lambda iface, context: user_service + ) + form_obj = pretend.stub() + form_class = pretend.call_recorder(lambda d, user_service: form_obj) + + project = ProjectFactory.create(name="foobar") + user = UserFactory.create() + role = RoleFactory.create(user=user, project=project) + + result = views.manage_project_roles( + project, db_request, _form_class=form_class + ) + + assert db_request.find_service.calls == [ + pretend.call(IUserService, context=None), + ] + assert form_class.calls == [ + pretend.call(db_request.POST, user_service=user_service), + ] + assert result == { + "project": project, + "roles": [role], + "form": form_obj, + } + + def test_post_new_role_validation_fails(self, db_request): + project = ProjectFactory.create(name="foobar") + user = UserFactory.create(username="testuser") + role = RoleFactory.create(user=user, project=project) + + user_service = pretend.stub() + db_request.find_service = pretend.call_recorder( + lambda iface, context: user_service + ) + db_request.method = "POST" + form_obj = pretend.stub(validate=pretend.call_recorder(lambda: False)) + form_class = pretend.call_recorder(lambda d, user_service: form_obj) + + result = views.manage_project_roles( + project, db_request, _form_class=form_class + ) + + assert db_request.find_service.calls == [ + pretend.call(IUserService, context=None), + ] + assert form_class.calls == [ + pretend.call(db_request.POST, user_service=user_service), + ] + assert form_obj.validate.calls == [pretend.call()] + assert result == { + "project": project, + "roles": [role], + "form": form_obj, + } + + def test_post_new_role(self, db_request): + project = ProjectFactory.create(name="foobar") + user = UserFactory.create(username="testuser") + + user_service = pretend.stub( + find_userid=lambda username: user.id, + get_user=lambda userid: user, + ) + db_request.find_service = pretend.call_recorder( + lambda iface, context: user_service + ) + db_request.method = "POST" + db_request.POST = pretend.stub() + form_obj = pretend.stub( + validate=pretend.call_recorder(lambda: True), + username=pretend.stub(data=user.username), + role_name=pretend.stub(data="Owner"), + ) + form_class = pretend.call_recorder(lambda *a, **kw: form_obj) + db_request.session = pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ) + + result = views.manage_project_roles( + project, db_request, _form_class=form_class + ) + + assert db_request.find_service.calls == [ + pretend.call(IUserService, context=None), + ] + assert form_obj.validate.calls == [pretend.call()] + assert form_class.calls == [ + pretend.call(db_request.POST, user_service=user_service), + pretend.call(user_service=user_service), + ] + assert db_request.session.flash.calls == [ + pretend.call("Added collaborator 'testuser'", queue="success"), + ] + + # Only one role is created + role = db_request.db.query(Role).one() + + assert result == { + "project": project, + "roles": [role], + "form": form_obj, + } + + def test_post_duplicate_role(self, db_request): + project = ProjectFactory.create(name="foobar") + user = UserFactory.create(username="testuser") + role = RoleFactory.create( + user=user, project=project, role_name="Owner" + ) + + user_service = pretend.stub( + find_userid=lambda username: user.id, + get_user=lambda userid: user, + ) + db_request.find_service = pretend.call_recorder( + lambda iface, context: user_service + ) + db_request.method = "POST" + db_request.POST = pretend.stub() + form_obj = pretend.stub( + validate=pretend.call_recorder(lambda: True), + username=pretend.stub(data=user.username), + role_name=pretend.stub(data=role.role_name), + ) + form_class = pretend.call_recorder(lambda *a, **kw: form_obj) + db_request.session = pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ) + + result = views.manage_project_roles( + project, db_request, _form_class=form_class + ) + + assert db_request.find_service.calls == [ + pretend.call(IUserService, context=None), + ] + assert form_obj.validate.calls == [pretend.call()] + assert form_class.calls == [ + pretend.call(db_request.POST, user_service=user_service), + pretend.call(user_service=user_service), + ] + assert db_request.session.flash.calls == [ + pretend.call( + "User 'testuser' already has Owner role for project", + queue="error", + ), + ] + + # No additional roles are created + assert role == db_request.db.query(Role).one() + + assert result == { + "project": project, + "roles": [role], + "form": form_obj, + } + + +class TestDeleteProjectRoles: + + def test_delete_role(self, db_request): + project = ProjectFactory.create(name="foobar") + user = UserFactory.create(username="testuser") + role = RoleFactory.create( + user=user, project=project, role_name="Owner" + ) + + db_request.method = "POST" + db_request.user = pretend.stub() + db_request.POST = MultiDict({"role_id": role.id}) + db_request.session = pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ) + db_request.route_path = pretend.call_recorder( + lambda *a, **kw: "/the-redirect" + ) + + result = views.delete_project_role(project, db_request) + + assert db_request.route_path.calls == [ + pretend.call('manage.project.roles', name=project.name), + ] + assert db_request.db.query(Role).all() == [] + assert db_request.session.flash.calls == [ + pretend.call("Successfully removed role", queue="success"), + ] + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + def test_delete_missing_role(self, db_request): + project = ProjectFactory.create(name="foobar") + missing_role_id = str(uuid.uuid4()) + + db_request.method = "POST" + db_request.user = pretend.stub() + db_request.POST = MultiDict({"role_id": missing_role_id}) + db_request.session = pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ) + db_request.route_path = pretend.call_recorder( + lambda *a, **kw: "/the-redirect" + ) + + result = views.delete_project_role(project, db_request) + + assert db_request.session.flash.calls == [ + pretend.call("Could not find role", queue="error"), + ] + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" + + def test_delete_own_owner_role(self, db_request): + project = ProjectFactory.create(name="foobar") + user = UserFactory.create(username="testuser") + role = RoleFactory.create( + user=user, project=project, role_name="Owner" + ) + + db_request.method = "POST" + db_request.user = user + db_request.POST = MultiDict({"role_id": role.id}) + db_request.session = pretend.stub( + flash=pretend.call_recorder(lambda *a, **kw: None), + ) + db_request.route_path = pretend.call_recorder( + lambda *a, **kw: "/the-redirect" + ) + + result = views.delete_project_role(project, db_request) + + assert db_request.session.flash.calls == [ + pretend.call("Cannot remove yourself as Owner", queue="error"), + ] + assert isinstance(result, HTTPSeeOther) + assert result.headers["Location"] == "/the-redirect" diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index 2c1041d4637f..f41551929b77 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -142,6 +142,20 @@ def add_policy(name, filename): traverse="/{name}", domain=warehouse, ), + pretend.call( + "manage.project.roles", + "/project/{name}/collaboration/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{name}", + domain=warehouse, + ), + pretend.call( + "manage.project.delete_role", + "/project/{name}/collaboration/delete/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{name}", + domain=warehouse, + ), pretend.call( "packaging.project", "/project/{name}/", diff --git a/warehouse/manage/__init__.py b/warehouse/manage/__init__.py index 16392bcb45ef..9bc84df13ac6 100644 --- a/warehouse/manage/__init__.py +++ b/warehouse/manage/__init__.py @@ -1,2 +1,15 @@ +# 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. + + def includeme(config): pass diff --git a/warehouse/manage/forms.py b/warehouse/manage/forms.py new file mode 100644 index 000000000000..cd952142659c --- /dev/null +++ b/warehouse/manage/forms.py @@ -0,0 +1,46 @@ +# 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. + +import wtforms + +from warehouse import forms + + +class CreateRoleForm(forms.Form): + username = wtforms.StringField( + validators=[ + wtforms.validators.DataRequired(message="Must specify a username"), + ] + ) + + role_name = wtforms.SelectField( + 'Select a role', + choices=[ + ('Owner', 'Owner'), + ('Maintainer', 'Maintainer'), + ], + validators=[ + wtforms.validators.DataRequired(message="Must select a role"), + ] + ) + + def validate_username(self, field): + userid = self.user_service.find_userid(field.data) + + if userid is None: + raise wtforms.validators.ValidationError( + "No user found with that username. Please try again." + ) + + def __init__(self, *args, user_service, **kwargs): + super().__init__(*args, **kwargs) + self.user_service = user_service diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py index 44720aa7716b..498ec201d21a 100644 --- a/warehouse/manage/views.py +++ b/warehouse/manage/views.py @@ -10,8 +10,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pyramid.httpexceptions import HTTPSeeOther from pyramid.security import Authenticated from pyramid.view import view_config +from sqlalchemy.orm.exc import NoResultFound + +from warehouse.accounts.interfaces import IUserService +from warehouse.accounts.models import User +from warehouse.manage.forms import CreateRoleForm +from warehouse.packaging.models import Role @view_config( @@ -43,3 +50,93 @@ def manage_projects(request): ) def manage_project_settings(project, request): return {"project": project} + + +@view_config( + route_name="manage.project.roles", + renderer="manage/roles.html", + uses_session=True, + require_methods=False, + permission="manage", +) +def manage_project_roles(project, request, _form_class=CreateRoleForm): + user_service = request.find_service(IUserService, context=None) + form = _form_class(request.POST, user_service=user_service) + + if request.method == "POST" and form.validate(): + username = form.username.data + role_name = form.role_name.data + userid = user_service.find_userid(username) + user = user_service.get_user(userid) + + if (request.db.query( + request.db.query(Role).filter( + Role.user == user, + Role.project == project, + Role.role_name == role_name, + ) + .exists()).scalar()): + request.session.flash( + f"User '{username}' already has {role_name} role for project", + queue="error" + ) + else: + request.db.add( + Role(user=user, project=project, role_name=form.role_name.data) + ) + request.session.flash( + f"Added collaborator '{form.username.data}'", + queue="success" + ) + form = _form_class(user_service=user_service) + + roles = ( + request.db.query(Role) + .join(User) + .filter(Role.project == project) + .all() + ) + + return { + "project": project, + "roles": roles, + "form": form, + } + + +@view_config( + route_name="manage.project.delete_role", + uses_session=True, + require_methods=["POST"], + permission="manage", +) +def delete_project_role(project, request): + try: + role = ( + request.db.query(Role) + .filter( + Role.id == request.POST.get('role_id'), + Role.project == project, + ) + .one() + ) + except NoResultFound: + request.session.flash("Could not find role", queue="error") + return HTTPSeeOther( + request.route_path('manage.project.roles', name=project.name) + ) + + if role.role_name == "Owner" and role.user == request.user: + request.session.flash( + "Cannot remove yourself as Owner", queue="error" + ) + return HTTPSeeOther( + request.route_path('manage.project.roles', name=project.name) + ) + + request.db.delete(role) + request.session.flash("Successfully removed role", queue="success") + + return HTTPSeeOther( + request.route_path('manage.project.roles', name=project.name) + ) diff --git a/warehouse/routes.py b/warehouse/routes.py index f882ca2f9a78..aba78d26fe3a 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -108,6 +108,20 @@ def includeme(config): traverse="/{name}", domain=warehouse, ) + config.add_route( + "manage.project.roles", + "/project/{name}/collaboration/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{name}", + domain=warehouse, + ) + config.add_route( + "manage.project.delete_role", + "/project/{name}/collaboration/delete/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{name}", + domain=warehouse, + ) # Packaging config.add_route( diff --git a/warehouse/templates/manage/project_settings_base.html b/warehouse/templates/manage/project_settings_base.html index 1a6d7742f2ac..e7e5a97ed1df 100644 --- a/warehouse/templates/manage/project_settings_base.html +++ b/warehouse/templates/manage/project_settings_base.html @@ -24,6 +24,9 @@

{{ project.name }}

  • Options
  • +
  • + Collaborators +
  • diff --git a/warehouse/templates/manage/roles.html b/warehouse/templates/manage/roles.html new file mode 100644 index 000000000000..893550ddb5ec --- /dev/null +++ b/warehouse/templates/manage/roles.html @@ -0,0 +1,78 @@ +{# + # 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 "project_settings_base.html" %} + +{% block title %}{{ project.name }}{% endblock %} + +{% block main %} +

    Collaborators

    +

    + There are two possible roles for collaborators: +

    +
    Owner
    +
    Owns a package, may add other collaborators for that package, and upload releases for a package.
    +
    Maintainer
    +
    May upload releases for a package.
    +
    +

    +

    Collaborators for {{ project.name }}

    + +

    Add a new collaborator

    +

    + Add a new collaborator by entering their exact username. +

    +
    + + {{ form.username }} + {{ form.role_name }} + +
    + {% if form.errors %} +
      + {% for field, errors in form.errors|dictsort if errors %} + {% for error in errors %} +
    • {{ error }}
    • + {% endfor %} + {% endfor %} +
    + {% endif %} +{% endblock %} From b7af24b660825332b249ceaf3246cef50235ac82 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Wed, 20 Dec 2017 13:58:03 -0500 Subject: [PATCH 06/26] Some really rudimentary styling, please revert --- .../static/sass/blocks/_project-settings.scss | 29 +++++++++++++++++++ warehouse/static/sass/warehouse.scss | 1 + 2 files changed, 30 insertions(+) create mode 100644 warehouse/static/sass/blocks/_project-settings.scss diff --git a/warehouse/static/sass/blocks/_project-settings.scss b/warehouse/static/sass/blocks/_project-settings.scss new file mode 100644 index 000000000000..05a10c316c1b --- /dev/null +++ b/warehouse/static/sass/blocks/_project-settings.scss @@ -0,0 +1,29 @@ +/*! + * 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. + */ + +ul.collaborator-list { + list-style: none; + margin: 0; + + li { + img { + margin-right: 10px; + } + + button { + position: relative; + float: right; + } + } +} diff --git a/warehouse/static/sass/warehouse.scss b/warehouse/static/sass/warehouse.scss index 1b4ef769bb27..bf15293353a9 100644 --- a/warehouse/static/sass/warehouse.scss +++ b/warehouse/static/sass/warehouse.scss @@ -93,6 +93,7 @@ @import "blocks/package-list"; @import "blocks/package-snippet"; @import "blocks/project-description"; +@import "blocks/project-settings"; @import "blocks/release"; @import "blocks/release-timeline"; @import "blocks/search-form"; From 8c0a45382a0eb6ce6dbe23743dba99cf5f3788f7 Mon Sep 17 00:00:00 2001 From: Nicole Harris Date: Mon, 8 Jan 2018 18:09:31 +0000 Subject: [PATCH 07/26] Update logged in information architecture, begin styling --- warehouse/static/sass/base/_forms.scss | 2 + warehouse/static/sass/base/_tables.scss | 56 ------- warehouse/static/sass/blocks/_button.scss | 15 ++ .../static/sass/blocks/_downloads-table.scss | 81 ---------- .../sass/blocks/_package-description.scss | 3 +- .../static/sass/blocks/_project-settings.scss | 29 ---- warehouse/static/sass/blocks/_table.scss | 143 ++++++++++++++++++ .../static/sass/blocks/_vertical-tabs.scss | 30 ++++ .../sass/layout-helpers/_split-layout.scss | 4 + .../static/sass/tools/_design-utilities.scss | 1 - warehouse/static/sass/warehouse.scss | 4 +- warehouse/templates/manage/manage_base.html | 41 +++++ warehouse/templates/manage/profile.html | 39 ++--- warehouse/templates/manage/project.html | 109 +++++++++++-- .../manage/project_settings_base.html | 38 ----- warehouse/templates/manage/projects.html | 49 +++--- warehouse/templates/manage/roles.html | 132 +++++++++------- warehouse/templates/packaging/detail.html | 13 +- 18 files changed, 468 insertions(+), 321 deletions(-) delete mode 100644 warehouse/static/sass/base/_tables.scss delete mode 100644 warehouse/static/sass/blocks/_downloads-table.scss delete mode 100644 warehouse/static/sass/blocks/_project-settings.scss create mode 100644 warehouse/static/sass/blocks/_table.scss create mode 100644 warehouse/templates/manage/manage_base.html delete mode 100644 warehouse/templates/manage/project_settings_base.html diff --git a/warehouse/static/sass/base/_forms.scss b/warehouse/static/sass/base/_forms.scss index 238e54f4490b..ad00a5286691 100644 --- a/warehouse/static/sass/base/_forms.scss +++ b/warehouse/static/sass/base/_forms.scss @@ -25,6 +25,8 @@ select { border: 1px solid $border-color; color: $text-color; vertical-align: middle; + min-width: 250px; + max-width: 100%; } #{$all-text-inputs-focus}, diff --git a/warehouse/static/sass/base/_tables.scss b/warehouse/static/sass/base/_tables.scss deleted file mode 100644 index 81739d43e5ba..000000000000 --- a/warehouse/static/sass/base/_tables.scss +++ /dev/null @@ -1,56 +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. - */ - -// TABLE - -table { - @include card; - @include border-width(1px null); - border-collapse: collapse; - border-spacing: 0; - text-align: left; - margin: 25px 0 0 0; - font-size: $small-font-size; - width: 100%; - - tr { - border-bottom: 1px solid $border-color; - } - - th, - td { - margin: 0; - border: 0; - padding: 10px 7px; - border-right: 1px solid $border-color; - } - - thead tr { - background-color: $white; - - th { - vertical-align: bottom; - font-weight: $bold-font-weight; - } - } - - tbody { - tr { - background-color: darken($background-color, 0.75); - } - tr:nth-child(even) { - background-color: darken($background-color, 2); - } - } -} diff --git a/warehouse/static/sass/blocks/_button.scss b/warehouse/static/sass/blocks/_button.scss index 549069069940..d21b8dcb56e0 100644 --- a/warehouse/static/sass/blocks/_button.scss +++ b/warehouse/static/sass/blocks/_button.scss @@ -104,6 +104,21 @@ } } + &--danger { + border-color: $danger-color; + background-color: $danger-color; + color: $white; + + &:focus, + &:hover, + &:active { + border-color: darken($danger-color, 10); + background-color: darken($danger-color, 9); + text-decoration-color: transparentize($white, 0.8); + color: $white; + } + } + &[disabled], &--disabled { cursor: not-allowed; diff --git a/warehouse/static/sass/blocks/_downloads-table.scss b/warehouse/static/sass/blocks/_downloads-table.scss deleted file mode 100644 index d5d1a416dc48..000000000000 --- a/warehouse/static/sass/blocks/_downloads-table.scss +++ /dev/null @@ -1,81 +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. - */ - -.downloads-table { - box-sizing: border-box; - table-layout: fixed; - word-wrap: break-word; - margin-top: ($spacing-unit / 2); - margin-bottom: 10px; - - &__filename { - width: 65%; - } - - &__version { - width: 9%; - } - - &__type, - &__upload-date { - width: 13%; - } - - &__sha256-link { - margin-left: 5px; - } - - @media only screen and (max-width: $large-desktop){ - // Hide "file type" column - th:nth-child(3), - td:nth-child(3) { - display: none; - } - } - - @media only screen and (max-width: $desktop){ - // Hide "version" column - th:nth-child(5), - td:nth-child(5) { - display: none; - } - } - - @media only screen and (max-width: $tablet){ - // Hide "upload date" column - th:nth-child(4), - td:nth-child(4) { - display: none; - } - - // Hide "upload date" column - th:nth-child(2), - td:nth-child(2) { - width: 20%; - } - - th:first, - td:first { - width: 80%; - } - } - - @media only screen and (max-width: $mobile){ - // Hide "upload date" column - th:nth-child(2), - td:nth-child(2) { - display: none; - } - } -} diff --git a/warehouse/static/sass/blocks/_package-description.scss b/warehouse/static/sass/blocks/_package-description.scss index 98baa417ec11..cc0c4855d414 100644 --- a/warehouse/static/sass/blocks/_package-description.scss +++ b/warehouse/static/sass/blocks/_package-description.scss @@ -21,5 +21,6 @@ .package-description { font-size: 1.1rem; font-style: italic; - margin: 5px 0; + margin: 0; + padding: 0; } diff --git a/warehouse/static/sass/blocks/_project-settings.scss b/warehouse/static/sass/blocks/_project-settings.scss deleted file mode 100644 index 05a10c316c1b..000000000000 --- a/warehouse/static/sass/blocks/_project-settings.scss +++ /dev/null @@ -1,29 +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. - */ - -ul.collaborator-list { - list-style: none; - margin: 0; - - li { - img { - margin-right: 10px; - } - - button { - position: relative; - float: right; - } - } -} diff --git a/warehouse/static/sass/blocks/_table.scss b/warehouse/static/sass/blocks/_table.scss new file mode 100644 index 000000000000..7be5b5d4d1a8 --- /dev/null +++ b/warehouse/static/sass/blocks/_table.scss @@ -0,0 +1,143 @@ +/*! + * 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. + */ + +// TABLE + +.table { + @include card; + @include border-width(1px null); + border-collapse: collapse; + border-spacing: 0; + text-align: left; + margin: 25px 0 0 0; + font-size: $small-font-size; + width: 100%; + + tr { + border-bottom: 1px solid $border-color; + } + + th, + td { + margin: 0; + border: 0; + padding: 10px 7px; + border-right: 1px solid $border-color; + } + + thead tr { + background-color: $white; + + th { + vertical-align: bottom; + font-weight: $bold-font-weight; + } + } + + tbody { + tr { + background-color: darken($background-color, 0.75); + } + tr:nth-child(even) { + background-color: darken($background-color, 2); + } + } + + &--light { + border: 0; + font-size: $base-font-size; + box-shadow: none; + + tbody tr, + tbody tr:nth-child(2n), + th, + td { + border: 0; + background-color: transparent; + } + + th, + td { + border-bottom: 1px solid $base-grey; + } + } + + &--downloads { + box-sizing: border-box; + table-layout: fixed; + word-wrap: break-word; + margin-top: ($spacing-unit / 2); + margin-bottom: 10px; + + &__filename { + width: 65%; + } + + &__version { + width: 9%; + } + + &__type, + &__upload-date { + width: 13%; + } + + &__sha256-link { + margin-left: 5px; + } + + @media only screen and (max-width: $large-desktop){ + // Hide "file type" column + th:nth-child(3), + td:nth-child(3) { + display: none; + } + } + + @media only screen and (max-width: $desktop){ + // Hide "version" column + th:nth-child(5), + td:nth-child(5) { + display: none; + } + } + + @media only screen and (max-width: $tablet){ + // Hide "upload date" column + th:nth-child(4), + td:nth-child(4) { + display: none; + } + + // Hide "upload date" column + th:nth-child(2), + td:nth-child(2) { + width: 20%; + } + + th:first, + td:first { + width: 80%; + } + } + + @media only screen and (max-width: $mobile){ + // Hide "upload date" column + th:nth-child(2), + td:nth-child(2) { + display: none; + } + } + } +} diff --git a/warehouse/static/sass/blocks/_vertical-tabs.scss b/warehouse/static/sass/blocks/_vertical-tabs.scss index b4e638e0b9ef..c578a109677b 100644 --- a/warehouse/static/sass/blocks/_vertical-tabs.scss +++ b/warehouse/static/sass/blocks/_vertical-tabs.scss @@ -84,11 +84,37 @@ &--with-icon { i { + width: 20px; + text-align: center; margin-right: 5px; } } } + &__sub-menu { + display: none; + list-style-type: none; + margin: 5px 0 5px 25px; + padding: 0; + } + + &__sub-tab { + display: block; + padding: $spacing-unit / 4; + cursor: pointer; + + &:hover { + text-decoration: none; + color: darken($brand-color, 10) + } + + &--is-active, + &--is-active:hover { + background: mix($border-color, #fff, 50); + color: $text-color; + } + } + &__panel { @include span-columns(9); @@ -105,3 +131,7 @@ } } } + +.vertical-tabs__tab--is-active + .vertical-tabs__sub-menu { + display: block; +} diff --git a/warehouse/static/sass/layout-helpers/_split-layout.scss b/warehouse/static/sass/layout-helpers/_split-layout.scss index 0de9d5a25612..d711f700b6e4 100644 --- a/warehouse/static/sass/layout-helpers/_split-layout.scss +++ b/warehouse/static/sass/layout-helpers/_split-layout.scss @@ -31,6 +31,10 @@ text-align: right; } + &--middle { + align-items: center; + } + &--table { @include split-table-layout; } diff --git a/warehouse/static/sass/tools/_design-utilities.scss b/warehouse/static/sass/tools/_design-utilities.scss index 377a83a677c4..b9eccc40075e 100644 --- a/warehouse/static/sass/tools/_design-utilities.scss +++ b/warehouse/static/sass/tools/_design-utilities.scss @@ -22,7 +22,6 @@ &:focus { background-color: $highlight-color; color: darken($highlight-color, 50); - outline: 2px solid $highlight-color; cursor: pointer; } } diff --git a/warehouse/static/sass/warehouse.scss b/warehouse/static/sass/warehouse.scss index bf15293353a9..e58b4c60e272 100644 --- a/warehouse/static/sass/warehouse.scss +++ b/warehouse/static/sass/warehouse.scss @@ -49,7 +49,6 @@ @import "base/typography"; @import "base/images-figures"; @import "base/lists"; -@import "base/tables"; @import "base/forms"; // LAYOUT HELPERS LAYER: reusable layout helpers @@ -73,7 +72,6 @@ @import "blocks/common-question"; @import "blocks/dark-overlay"; @import "blocks/download-graph"; -@import "blocks/downloads-table"; @import "blocks/dropdown"; @import "blocks/filter-badge"; @import "blocks/filter-panel"; @@ -93,7 +91,6 @@ @import "blocks/package-list"; @import "blocks/package-snippet"; @import "blocks/project-description"; -@import "blocks/project-settings"; @import "blocks/release"; @import "blocks/release-timeline"; @import "blocks/search-form"; @@ -102,6 +99,7 @@ @import "blocks/sponsors"; @import "blocks/status-badge"; @import "blocks/statistics-bar"; +@import "blocks/table"; @import "blocks/tooltip"; @import "blocks/vertical-tabs"; @import "blocks/viewport-section"; diff --git a/warehouse/templates/manage/manage_base.html b/warehouse/templates/manage/manage_base.html new file mode 100644 index 000000000000..1a5ed21e27f3 --- /dev/null +++ b/warehouse/templates/manage/manage_base.html @@ -0,0 +1,41 @@ +{# + # 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 "base.html" %} + +{% block content %} +
    +
    +
    + +
    +
    + {% block main %}{% endblock %} +
    +
    +
    +
    +
    +{% endblock %} diff --git a/warehouse/templates/manage/profile.html b/warehouse/templates/manage/profile.html index 29474c44e8bb..3d3c7077f729 100644 --- a/warehouse/templates/manage/profile.html +++ b/warehouse/templates/manage/profile.html @@ -11,33 +11,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -#} -{% extends "base.html" %} +{% extends "manage_base.html" %} {% set user = request.user %} {% set title = "Manage Your Profile" %} - +{% block profile_active %}vertical-tabs__tab--is-active{% endblock %} {% block title %}{{ title }}{% endblock %} -{% block content %} -
    -
    -

    {{ title }}

    -
    -
    - {% set alt = "Avatar for {} from gravatar.com".format(user.name|default(user.username, true)) %} - {{ alt }} - Change this image -

    {{ user.name|default(user.username, true) }}

    -
    - {% if user.name %} -

      {{ user.username }}

    - {% endif %} - {% if user.date_joined %} -

      Joined on {{ user.date_joined|format_date() }}

    - {% endif %} -
    -
    +{% block main %} +

    {{ title }}

    +
    + {% set alt = "Avatar for {} from gravatar.com".format(user.name|default(user.username, true)) %} + {{ alt }} + Change this image +

    {{ user.name|default(user.username, true) }}

    +
    + {% if user.name %} +

      {{ user.username }}

    + {% endif %} + {% if user.date_joined %} +

      Joined on {{ user.date_joined|format_date() }}

    + {% endif %}
    -
    -
    {% endblock %} diff --git a/warehouse/templates/manage/project.html b/warehouse/templates/manage/project.html index 334f0c4a8bb1..0afeaefe1bfb 100644 --- a/warehouse/templates/manage/project.html +++ b/warehouse/templates/manage/project.html @@ -11,15 +11,106 @@ # See the License for the specific language governing permissions and # limitations under the License. -#} -{% extends "project_settings_base.html" %} +{% extends "base.html" %} -{% block title %}{{ project.name }}{% endblock %} +{% set user = request.user %} +{% set projects = user.projects %} +{% set current_project = project %} +{% set active_page = active_page|default('releases') %} -{% block main %} -

    Options

    -

    {{ project.name }}

    -

    - Last released on {{ project.releases[0].created|format_date() }} -

    -

    {{ project.releases[0].summary }}

    +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    +

    {{ project.name }}

    + +
    + {% block main %} +

    Releases ({{ project.releases|length }})

    + {% if project.releases %} + + + + + + + + + {% for release in project.releases %} + + + + + + + {% endfor %} + +
    VersionRelease DateSummary
    {{ release.version }} + {% if release.summary %} + {{ release.summary }} + {% else %} + — + {% endif %} + + +
    + + {% else %} + No releases yet! + {% endif %} + {% endblock %} +
    +
    +
    +
    +
    {% endblock %} diff --git a/warehouse/templates/manage/project_settings_base.html b/warehouse/templates/manage/project_settings_base.html deleted file mode 100644 index e7e5a97ed1df..000000000000 --- a/warehouse/templates/manage/project_settings_base.html +++ /dev/null @@ -1,38 +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. --#} -{% extends "base.html" %} - -{% block title %}{{ project.name }}{% endblock %} - -{% block content %} -
    -
    -

    {{ project.name }}

    -
    - -
    -
    - {% block main %}{% endblock %} -
    -
    -
    - -{% endblock %} diff --git a/warehouse/templates/manage/projects.html b/warehouse/templates/manage/projects.html index fe95a96536fa..90b878704540 100644 --- a/warehouse/templates/manage/projects.html +++ b/warehouse/templates/manage/projects.html @@ -11,36 +11,33 @@ # See the License for the specific language governing permissions and # limitations under the License. -#} -{% extends "base.html" %} - -{% set title = "Manage Your Projects" %} +{% extends "manage_base.html" %} +{% block projects_active %}vertical-tabs__tab--is-active{% endblock %} {% block title %}{{ title }}{% endblock %} -{% block content %} -
    -
    -

    {{ title }}

    -
    - {% if request.user.projects %} - {% for project in request.user.projects %} - {% set release = project.releases[0] %} -
    -

    {{ project.name }}

    -

    - Last released on {{ release.created|format_date() }} -

    -

    {{ release.summary }}

    +{% block main %} +

    Your Projects

    +
    + {% if request.user.projects %} + {% for project in request.user.projects %} + {% set release = project.releases[0] %} +
    +

    {{ project.name }}

    +

    + Last released on {{ release.created|format_date() }} +

    +

    {{ release.summary }}

    + - {% endfor %} - {% else %} -
    -

    You have not uploaded any projects to PyPI, yet. To learn how to get started, visit the Python Packaging User Guide

    -
    - {% endif %}
    + {% endfor %} + {% else %} +
    +

    You have not uploaded any projects to PyPI, yet. To learn how to get started, visit the Python Packaging User Guide

    +
    + {% endif %}
    -
    - - {% endblock %} diff --git a/warehouse/templates/manage/roles.html b/warehouse/templates/manage/roles.html index 893550ddb5ec..bf29bfc6bf7f 100644 --- a/warehouse/templates/manage/roles.html +++ b/warehouse/templates/manage/roles.html @@ -11,68 +11,98 @@ # See the License for the specific language governing permissions and # limitations under the License. -#} -{% extends "project_settings_base.html" %} +{% extends "project.html" %} {% block title %}{{ project.name }}{% endblock %} +{% set active_page = 'collaborators' %} {% block main %}

    Collaborators

    -

    - There are two possible roles for collaborators: +

    Use this page to control which PyPI users can help you to manage '{{ project.name }}'.

    +
    +

    There are two possible roles for collaborators:

    Owner
    Owns a package, may add other collaborators for that package, and upload releases for a package.
    Maintainer
    May upload releases for a package.
    -

    -

    Collaborators for {{ project.name }}

    -
    +
    + + + + + + + + {% for role in roles|sort(attribute="user.username") %} + + + + + + {% endfor %} + - - + + + + + + {% if form.errors %} + + + + {% endif %} - - {% endfor %} - -

    Add a new collaborator

    -

    - Add a new collaborator by entering their exact username. -

    - - - {{ form.username }} - {{ form.role_name }} - - - {% if form.errors %} -
      - {% for field, errors in form.errors|dictsort if errors %} - {% for error in errors %} -
    • {{ error }}
    • - {% endfor %} - {% endfor %} -
    - {% endif %} + +
    UserRoleAction
    + + {{ role.user.username }} + {% if role.user.name %} + - {{ role.user.name }} + {% endif %} + + + {% if role.user == request.user %} + {{ role.role_name }} + {% else %} + + + + {% endif %} + +
    + + + +
    +
    + {{ form.username(placeholder="user's exact username") }} + {{ form.role_name }}
    +
      + {% for field, errors in form.errors|dictsort if errors %} + {% for error in errors %} +
    • {{ error }}
    • + {% endfor %} + {% endfor %} +
    +
    +
    + {% endblock %} diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index 57aba1e3fc5a..d21355d930ba 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -98,10 +98,17 @@

    -{% if release.summary %} +{% if release.summary %}
    -

    {{ release.summary }}

    +
    + {% if release.summary %} +

    {{ release.summary }}

    + {% endif %} + + Edit Project + +
    {% endif %} @@ -258,7 +265,7 @@

    Release History

    Download Files

    Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

    - +
    - + {% if form.errors %} @@ -104,5 +104,4 @@

    Collaborators

    From 66ed62f60395974f6dfe6d3f648505d2e00b2118 Mon Sep 17 00:00:00 2001 From: Nicole Harris Date: Mon, 8 Jan 2018 18:18:05 +0000 Subject: [PATCH 08/26] Make collaborator form more simple --- warehouse/templates/manage/roles.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/warehouse/templates/manage/roles.html b/warehouse/templates/manage/roles.html index bf29bfc6bf7f..ec9ff1fd5371 100644 --- a/warehouse/templates/manage/roles.html +++ b/warehouse/templates/manage/roles.html @@ -85,7 +85,7 @@

    Collaborators

    {{ form.username(placeholder="user's exact username") }}
    {{ form.role_name }}

    - {% endblock %} From a118bd8f3726dc5b68207d24e81c683fe5b66996 Mon Sep 17 00:00:00 2001 From: Nicole Harris Date: Mon, 8 Jan 2018 18:36:17 +0000 Subject: [PATCH 09/26] Allow stacking flash messages --- warehouse/static/sass/blocks/_notification-bar.scss | 8 -------- 1 file changed, 8 deletions(-) diff --git a/warehouse/static/sass/blocks/_notification-bar.scss b/warehouse/static/sass/blocks/_notification-bar.scss index ee23f08e5262..c70cedbc9bc4 100644 --- a/warehouse/static/sass/blocks/_notification-bar.scss +++ b/warehouse/static/sass/blocks/_notification-bar.scss @@ -34,7 +34,6 @@ .notification-bar { border-bottom: 2px solid $white; - border-top: 2px solid $white; text-align: center; background-color: darken($brand-color, 10); color: $white; @@ -63,10 +62,3 @@ background-color: $success-color; } } - -// Remove top border when notification-bars are stacked on top of each other, -// or when notification bar is under a sticky element -.notification-bar + .notification-bar, -body.with-sticky > .notification-bar { - border-top: 0; -} From 16d6d90e9ba11e8aa8d7dac27d67aa596cce0f8f Mon Sep 17 00:00:00 2001 From: Nicole Harris Date: Tue, 9 Jan 2018 07:28:16 +0000 Subject: [PATCH 10/26] Reuse dropdown SCSS --- warehouse/static/sass/blocks/_dropdown.scss | 59 +++++++++++++------ .../includes/current-user-indicator.html | 2 +- warehouse/templates/manage/project.html | 6 +- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/warehouse/static/sass/blocks/_dropdown.scss b/warehouse/static/sass/blocks/_dropdown.scss index 9749896cb19b..f1a9945525cc 100644 --- a/warehouse/static/sass/blocks/_dropdown.scss +++ b/warehouse/static/sass/blocks/_dropdown.scss @@ -28,14 +28,6 @@ position: relative; display: inline-block; - // Remove form styling - form, - button { - border: 0; - background-color: transparent; - padding: 0; - } - &__trigger { cursor: pointer; } @@ -49,11 +41,10 @@ &__content { position: absolute; right: 0; - border: 1px solid $header-border-color; + margin-bottom: -4px; box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.05); z-index: index($z-index-scale, "dropdown"); - border: 1px solid rgba(255, 255, 255, 0.25); - border-top: 1px solid $header-border-color; + border: 1px solid $border-color; display: none; } @@ -68,11 +59,11 @@ &__link, button.dropdown__link { display: block; - padding: 15px 15px 15px 45px; - border-bottom: 1px solid $header-border-color; - background-color: $header-background-color; + padding: 15px 15px 15px 15px; + border-bottom: 1px solid $border-color; + background-color: $white; min-width: 175px; - color: $white; + color: $text-color; cursor: pointer; text-align: left; position: relative; @@ -82,8 +73,8 @@ } &:hover { - background-color: darken($header-background-color, 1.5); - color: $white; + background-color: mix($border-color, #fff, 50);; + color: $text-color; text-decoration: underline; } @@ -94,4 +85,38 @@ top: 20px; } } + + &--on-menu { + // Remove form styling + form, + button { + border: 0; + background-color: transparent; + padding: 0; + } + + .dropdown__content { + border-color: $header-border-color; + margin-bottom: 0; + } + + .dropdown__link, + button.dropdown__link { + border-bottom-color: $header-border-color; + background-color: $header-background-color; + color: $white; + + &:hover { + background-color: darken($header-background-color, 1.5); + color: $white; + } + } + } + + &--with-icons { + .dropdown__link, + button.dropdown__link { + padding: 15px 15px 15px 45px; + } + } } diff --git a/warehouse/templates/includes/current-user-indicator.html b/warehouse/templates/includes/current-user-indicator.html index 5aa63b61f8c5..eef041980498 100644 --- a/warehouse/templates/includes/current-user-indicator.html +++ b/warehouse/templates/includes/current-user-indicator.html @@ -14,7 +14,7 @@ {% if request.user %}
    -
    - - {% block main %}

    Releases ({{ project.releases|length }})

    {% if project.releases %} @@ -91,137 +95,135 @@

    Releases ({{ project.releases|length }})

    - {% for release in project.releases %} - - {{ release.version }} - - - {% if release.summary %} - {{ release.summary }} - {% else %} - — - {% endif %} + {% for release in project.releases %} + + + {# TODO: https://github.com/pypa/warehouse/issues/2807 {{ release.version }} #} + {{ release.version }} - - - - -
    - - - {% for release in project.releases %} - {% endblock %} diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index 87885ac202c9..aabb4fb9f2fd 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -98,16 +98,16 @@

    -{% if release.summary %} +{% if release.summary %}{# TODO: https://github.com/pypa/warehouse/issues/2810 - add "or logged in user can edit package" #}
    {% if release.summary %}

    {{ release.summary }}

    {% endif %} - - Edit Project - + Edit Project + {# TODO: https://github.com/pypa/warehouse/issues/2810 (if logged in user can edit package) + #}
    From 9a1f4fe00094f5583444694dc1c6145155547dd2 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 18 Jan 2018 12:07:32 -0600 Subject: [PATCH 24/26] Properly comment out Edit Project link --- warehouse/templates/packaging/detail.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warehouse/templates/packaging/detail.html b/warehouse/templates/packaging/detail.html index aabb4fb9f2fd..42e28a80343b 100644 --- a/warehouse/templates/packaging/detail.html +++ b/warehouse/templates/packaging/detail.html @@ -105,9 +105,9 @@

    {% if release.summary %}

    {{ release.summary }}

    {% endif %} + {# TODO: https://github.com/pypa/warehouse/issues/2810 (if logged in user can edit package) Edit Project - {# TODO: https://github.com/pypa/warehouse/issues/2810 (if logged in user can edit package) - #} + #} From 60d1144fe1c500e3db4a7a26697a30dda7346a0c Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 18 Jan 2018 12:10:53 -0600 Subject: [PATCH 25/26] Change 'Preview' to 'View' This will always link to a project/release that is live, so it's never really a "Preview" per se. Also, this allows us to actually have a "Preview" some day when we allow for staged releases. --- warehouse/templates/manage/project.html | 4 ++-- warehouse/templates/manage/projects.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/warehouse/templates/manage/project.html b/warehouse/templates/manage/project.html index 5eb3a36a1f38..9ffb6290cc50 100644 --- a/warehouse/templates/manage/project.html +++ b/warehouse/templates/manage/project.html @@ -59,7 +59,7 @@

    {{ project.name }}

    From 9f394913665d0edf6297f7ced35b1015782fb75d Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Thu, 18 Jan 2018 12:55:54 -0600 Subject: [PATCH 26/26] Fix more linting errors --- warehouse/templates/manage/project.html | 88 ++++++++++++------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/warehouse/templates/manage/project.html b/warehouse/templates/manage/project.html index 9ffb6290cc50..d1b5ae5e4111 100644 --- a/warehouse/templates/manage/project.html +++ b/warehouse/templates/manage/project.html @@ -95,54 +95,54 @@

    Releases ({{ project.releases|length }})

    - {% for release in project.releases %} - - - {# TODO: https://github.com/pypa/warehouse/issues/2807 {{ release.version }} #} - {{ release.version }} + {% for release in project.releases %} + + + {# TODO: https://github.com/pypa/warehouse/issues/2807 {{ release.version }} #} + {{ release.version }} + + + + {% if release.summary %} + {{ release.summary }} + {% else %} + — + {% endif %} - - - {% if release.summary %} - {{ release.summary }} - {% else %} - — - {% endif %} - - -