diff --git a/Gulpfile.babel.js b/Gulpfile.babel.js index 6ae8f835c57b..bab18b3d3f7a 100644 --- a/Gulpfile.babel.js +++ b/Gulpfile.babel.js @@ -163,6 +163,7 @@ gulp.task("dist:admin:css", () => { "warehouse/admin/static/css/ionicons.min.css", "warehouse/admin/static/css/AdminLTE.min.css", "warehouse/admin/static/css/skins/skin-purple.min.css", + "warehouse/admin/static/css/admin.css", ]; return gulp.src(files) .pipe(gulpConcat("all.css")) diff --git a/tests/common/db/organizations.py b/tests/common/db/organizations.py index 0c5bda236cc3..09ce232fd8cc 100644 --- a/tests/common/db/organizations.py +++ b/tests/common/db/organizations.py @@ -41,15 +41,13 @@ class Meta: link_url = factory.Faker("uri") description = factory.Faker("sentence") is_active = True - is_approved = False + is_approved = None created = factory.Faker( "date_time_between_dates", datetime_start=datetime.datetime(2020, 1, 1), datetime_end=datetime.datetime(2022, 1, 1), ) - date_approved = factory.Faker( - "date_time_between_dates", datetime_start=datetime.datetime(2020, 1, 1) - ) + date_approved = None class OrganizationEventFactory(WarehouseFactory): diff --git a/tests/unit/admin/views/test_organizations.py b/tests/unit/admin/views/test_organizations.py index a2bd921d8c6c..10f7790ba602 100644 --- a/tests/unit/admin/views/test_organizations.py +++ b/tests/unit/admin/views/test_organizations.py @@ -59,11 +59,9 @@ def test_basic_query(self, enable_organizations, db_request): db_request.GET["q"] = organizations[0].name result = views.organization_list(db_request) - assert result == { - "organizations": [organizations[0]], - "query": organizations[0].name, - "terms": [organizations[0].name], - } + assert organizations[0] in result["organizations"] + assert result["query"] == organizations[0].name + assert result["terms"] == [organizations[0].name] def test_name_query(self, enable_organizations, db_request): organizations = sorted( @@ -73,11 +71,9 @@ def test_name_query(self, enable_organizations, db_request): db_request.GET["q"] = f"name:{organizations[0].name}" result = views.organization_list(db_request) - assert result == { - "organizations": [organizations[0]], - "query": f"name:{organizations[0].name}", - "terms": [f"name:{organizations[0].name}"], - } + assert organizations[0] in result["organizations"] + assert result["query"] == f"name:{organizations[0].name}" + assert result["terms"] == [f"name:{organizations[0].name}"] def test_organization_query(self, enable_organizations, db_request): organizations = sorted( @@ -87,11 +83,9 @@ def test_organization_query(self, enable_organizations, db_request): db_request.GET["q"] = f"organization:{organizations[0].display_name}" result = views.organization_list(db_request) - assert result == { - "organizations": [organizations[0]], - "query": f"organization:{organizations[0].display_name}", - "terms": [f"organization:{organizations[0].display_name}"], - } + assert organizations[0] in result["organizations"] + assert result["query"] == f"organization:{organizations[0].display_name}" + assert result["terms"] == [f"organization:{organizations[0].display_name}"] def test_url_query(self, enable_organizations, db_request): organizations = sorted( @@ -101,11 +95,9 @@ def test_url_query(self, enable_organizations, db_request): db_request.GET["q"] = f"url:{organizations[0].link_url}" result = views.organization_list(db_request) - assert result == { - "organizations": [organizations[0]], - "query": f"url:{organizations[0].link_url}", - "terms": [f"url:{organizations[0].link_url}"], - } + assert organizations[0] in result["organizations"] + assert result["query"] == f"url:{organizations[0].link_url}" + assert result["terms"] == [f"url:{organizations[0].link_url}"] def test_description_query(self, enable_organizations, db_request): organizations = sorted( @@ -115,11 +107,9 @@ def test_description_query(self, enable_organizations, db_request): db_request.GET["q"] = f"description:'{organizations[0].description}'" result = views.organization_list(db_request) - assert result == { - "organizations": [organizations[0]], - "query": f"description:'{organizations[0].description}'", - "terms": [f"description:{organizations[0].description}"], - } + assert organizations[0] in result["organizations"] + assert result["query"] == f"description:'{organizations[0].description}'" + assert result["terms"] == [f"description:{organizations[0].description}"] def test_is_approved_query(self, enable_organizations, db_request): organizations = sorted( diff --git a/tests/unit/manage/test_views.py b/tests/unit/manage/test_views.py index c38eecc86eae..fb914b47990e 100644 --- a/tests/unit/manage/test_views.py +++ b/tests/unit/manage/test_views.py @@ -58,6 +58,7 @@ from warehouse.utils.project import remove_documentation from ...common.db.accounts import EmailFactory +from ...common.db.organizations import OrganizationFactory from ...common.db.packaging import ( FileFactory, JournalEntryFactory, @@ -2313,11 +2314,26 @@ def test_default_response(self, monkeypatch): ) monkeypatch.setattr(views, "CreateOrganizationForm", create_organization_cls) + organization = pretend.stub(name=pretend.stub()) + + user_organizations = pretend.call_recorder( + lambda *a, **kw: { + "organizations_managed": [], + "organizations_owned": [organization], + "organizations_billing": [], + } + ) + monkeypatch.setattr(views, "user_organizations", user_organizations) + + organization_service = pretend.stub( + get_organizations_by_user=lambda *a, **kw: [organization] + ) + user_service = pretend.stub() request = pretend.stub( user=pretend.stub(id=pretend.stub(), username=pretend.stub()), find_service=lambda interface, **kw: { - IOrganizationService: pretend.stub(), - IUserService: pretend.stub(), + IOrganizationService: organization_service, + IUserService: user_service, }[interface], ) @@ -2325,6 +2341,10 @@ def test_default_response(self, monkeypatch): assert view.default_response == { "create_organization_form": create_organization_obj, + "organizations": [organization], + "organizations_managed": [], + "organizations_owned": [organization.name], + "organizations_billing": [], } def test_manage_organizations(self, monkeypatch): @@ -2593,6 +2613,27 @@ def test_create_organizations_disable_organizations(self, monkeypatch): ] +class TestManageOrganizationRoles: + def test_get_manage_organization_roles(self, db_request): + organization = OrganizationFactory.create(name="foobar") + request = pretend.stub( + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: False)), + ) + + result = views.manage_organization_roles(organization, request) + + assert result == {"organization": organization} + + def test_get_manage_organization_roles_disable_organizations(self, db_request): + organization = OrganizationFactory.create(name="foobar") + request = pretend.stub( + flags=pretend.stub(enabled=pretend.call_recorder(lambda *a: True)), + ) + + with pytest.raises(HTTPNotFound): + views.manage_organization_roles(organization, request) + + class TestManageProjects: def test_manage_projects(self, db_request): older_release = ReleaseFactory(created=datetime.datetime(2015, 1, 1)) diff --git a/tests/unit/organizations/test_models.py b/tests/unit/organizations/test_models.py new file mode 100644 index 000000000000..bafc61deab06 --- /dev/null +++ b/tests/unit/organizations/test_models.py @@ -0,0 +1,33 @@ +# 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 pytest + +from warehouse.organizations.models import OrganizationFactory + +from ...common.db.organizations import OrganizationFactory as DBOrganizationFactory + + +class TestOrganizationFactory: + @pytest.mark.parametrize(("name", "normalized"), [("foo", "foo"), ("Bar", "bar")]) + def test_traversal_finds(self, db_request, name, normalized): + organization = DBOrganizationFactory.create(name=name) + root = OrganizationFactory(db_request) + + assert root[normalized] == organization + + def test_traversal_cant_find(self, db_request): + organization = DBOrganizationFactory.create() + root = OrganizationFactory(db_request) + + with pytest.raises(KeyError): + root[organization.name + "invalid"] diff --git a/tests/unit/organizations/test_services.py b/tests/unit/organizations/test_services.py index 571469911ee1..02c44a5dbbd8 100644 --- a/tests/unit/organizations/test_services.py +++ b/tests/unit/organizations/test_services.py @@ -64,6 +64,54 @@ def test_find_organizationid(self, organization_service): def test_find_organizationid_nonexistent_org(self, organization_service): assert organization_service.find_organizationid("a_spoon_in_the_matrix") is None + def test_get_organizations(self, organization_service): + organization = OrganizationFactory.create(name="org") + another_organization = OrganizationFactory.create(name="another_org") + orgs = organization_service.get_organizations() + + assert organization in orgs + assert another_organization in orgs + + def test_get_organizations_needing_approval(self, organization_service): + i_need_it = OrganizationFactory.create() + assert i_need_it.is_approved is None + + i_has_it = OrganizationFactory.create() + organization_service.approve_organization(i_has_it.id) + assert i_has_it.is_approved is True + + orgs_needing_approval = ( + organization_service.get_organizations_needing_approval() + ) + + assert i_need_it in orgs_needing_approval + assert i_has_it not in orgs_needing_approval + + def test_get_organizations_by_user(self, organization_service, user_service): + user_organization = OrganizationFactory.create() + user = UserFactory.create() + organization_service.add_organization_role( + OrganizationRoleType.Owner.value, user.id, user_organization.id + ) + + another_user_organization = OrganizationFactory.create() + another_user = UserFactory.create() + organization_service.add_organization_role( + OrganizationRoleType.Owner.value, + another_user.id, + another_user_organization.id, + ) + + user_orgs = organization_service.get_organizations_by_user(user.id) + another_user_orgs = organization_service.get_organizations_by_user( + another_user.id + ) + + assert user_organization in user_orgs + assert user_organization not in another_user_orgs + assert another_user_organization in another_user_orgs + assert another_user_organization not in user_orgs + def test_add_organization(self, organization_service): organization = OrganizationFactory.create() new_org = organization_service.add_organization( diff --git a/tests/unit/test_routes.py b/tests/unit/test_routes.py index dcccf822e5d8..1baadf16bfb5 100644 --- a/tests/unit/test_routes.py +++ b/tests/unit/test_routes.py @@ -240,6 +240,13 @@ def add_policy(name, filename): pretend.call( "manage.organizations", "/manage/organizations/", domain=warehouse ), + pretend.call( + "manage.organization.roles", + "/manage/organization/{organization_name}/people/", + factory="warehouse.organizations.models:OrganizationFactory", + traverse="/{organization_name}", + domain=warehouse, + ), pretend.call("manage.projects", "/manage/projects/", domain=warehouse), pretend.call( "manage.project.settings", diff --git a/warehouse/admin/static/css/admin.css b/warehouse/admin/static/css/admin.css new file mode 100644 index 000000000000..38cec5d3ee10 --- /dev/null +++ b/warehouse/admin/static/css/admin.css @@ -0,0 +1,22 @@ +/*! + * 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. + */ + +@charset "utf-8"; + +/* Admin styles for the Warehouse project (PyPI) */ + +.table td, +.table th { + max-width: 30em; +} diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot index 697009e306b4..9714a7459a80 100644 --- a/warehouse/locale/messages.pot +++ b/warehouse/locale/messages.pot @@ -116,7 +116,7 @@ msgstr "" msgid "Successful WebAuthn assertion" msgstr "" -#: warehouse/accounts/views.py:441 warehouse/manage/views.py:817 +#: warehouse/accounts/views.py:441 warehouse/manage/views.py:822 msgid "Recovery code accepted. The supplied code cannot be used again." msgstr "" @@ -261,51 +261,51 @@ msgid "" "description with 400 characters or less." msgstr "" -#: warehouse/manage/views.py:248 +#: warehouse/manage/views.py:253 msgid "Email ${email_address} added - check your email for a verification link" msgstr "" -#: warehouse/manage/views.py:765 +#: warehouse/manage/views.py:770 msgid "Recovery codes already generated" msgstr "" -#: warehouse/manage/views.py:766 +#: warehouse/manage/views.py:771 msgid "Generating new recovery codes will invalidate your existing codes." msgstr "" -#: warehouse/manage/views.py:1299 +#: warehouse/manage/views.py:1389 msgid "" "There have been too many attempted OpenID Connect registrations. Try " "again later." msgstr "" -#: warehouse/manage/views.py:1980 +#: warehouse/manage/views.py:2070 msgid "User '${username}' already has ${role_name} role for project" msgstr "" -#: warehouse/manage/views.py:1991 +#: warehouse/manage/views.py:2081 msgid "" "User '${username}' does not have a verified primary email address and " "cannot be added as a ${role_name} for project" msgstr "" -#: warehouse/manage/views.py:2004 +#: warehouse/manage/views.py:2094 msgid "User '${username}' already has an active invite. Please try again later." msgstr "" -#: warehouse/manage/views.py:2062 +#: warehouse/manage/views.py:2152 msgid "Invitation sent to '${username}'" msgstr "" -#: warehouse/manage/views.py:2109 +#: warehouse/manage/views.py:2199 msgid "Could not find role invitation." msgstr "" -#: warehouse/manage/views.py:2120 +#: warehouse/manage/views.py:2210 msgid "Invitation already expired." msgstr "" -#: warehouse/manage/views.py:2144 +#: warehouse/manage/views.py:2234 msgid "Invitation revoked from '${username}'." msgstr "" @@ -623,8 +623,8 @@ msgstr "" #: warehouse/templates/includes/session-notifications.html:20 #: warehouse/templates/manage/account.html:716 #: warehouse/templates/manage/documentation.html:27 -#: warehouse/templates/manage/manage_base.html:278 -#: warehouse/templates/manage/manage_base.html:330 +#: warehouse/templates/manage/manage_base.html:294 +#: warehouse/templates/manage/manage_base.html:346 #: warehouse/templates/manage/release.html:163 #: warehouse/templates/manage/settings.html:119 msgid "Warning" @@ -906,7 +906,7 @@ msgstr "" #: warehouse/templates/accounts/login.html:69 #: warehouse/templates/accounts/register.html:110 #: warehouse/templates/accounts/reset-password.html:38 -#: warehouse/templates/manage/manage_base.html:339 +#: warehouse/templates/manage/manage_base.html:355 #: warehouse/templates/re-auth.html:49 msgid "Password" msgstr "" @@ -932,11 +932,11 @@ msgstr "" #: warehouse/templates/manage/account/recovery_codes-burn.html:70 #: warehouse/templates/manage/account/totp-provision.html:69 #: warehouse/templates/manage/account/webauthn-provision.html:44 -#: warehouse/templates/manage/organizations.html:34 -#: warehouse/templates/manage/organizations.html:57 -#: warehouse/templates/manage/organizations.html:79 -#: warehouse/templates/manage/organizations.html:97 -#: warehouse/templates/manage/organizations.html:116 +#: warehouse/templates/manage/organizations.html:89 +#: warehouse/templates/manage/organizations.html:112 +#: warehouse/templates/manage/organizations.html:134 +#: warehouse/templates/manage/organizations.html:152 +#: warehouse/templates/manage/organizations.html:171 #: warehouse/templates/manage/publishing.html:85 #: warehouse/templates/manage/publishing.html:97 #: warehouse/templates/manage/publishing.html:109 @@ -954,7 +954,7 @@ msgid "Your password" msgstr "" #: warehouse/templates/accounts/login.html:92 -#: warehouse/templates/manage/manage_base.html:342 +#: warehouse/templates/manage/manage_base.html:358 #: warehouse/templates/re-auth.html:72 msgid "Show password" msgstr "" @@ -1088,7 +1088,7 @@ msgstr "" #: warehouse/templates/accounts/profile.html:73 #: warehouse/templates/manage/projects.html:34 -#: warehouse/templates/manage/projects.html:99 +#: warehouse/templates/manage/projects.html:98 #, python-format msgid "Last released %(release_date)s" msgstr "" @@ -1398,9 +1398,8 @@ msgid "" msgstr "" #: warehouse/templates/email/new-organization-requested/body.html:19 -msgid "" -"You will receive another email when the PyPI organization has been " -"approved." +#: warehouse/templates/manage/organizations.html:30 +msgid "You will receive an email when the organization has been approved" msgstr "" #: warehouse/templates/email/oidc-provider-added/body.html:19 @@ -1691,7 +1690,7 @@ msgstr "" #: warehouse/templates/includes/current-user-indicator.html:37 #: warehouse/templates/manage/manage_base.html:204 -#: warehouse/templates/manage/manage_base.html:224 +#: warehouse/templates/manage/manage_base.html:232 #: warehouse/templates/manage/projects.html:18 msgid "Your projects" msgstr "" @@ -1699,8 +1698,8 @@ msgstr "" #: warehouse/templates/includes/current-user-indicator.html:43 #: warehouse/templates/manage/account.html:17 #: warehouse/templates/manage/account/two-factor.html:17 -#: warehouse/templates/manage/manage_base.html:210 -#: warehouse/templates/manage/manage_base.html:230 +#: warehouse/templates/manage/manage_base.html:218 +#: warehouse/templates/manage/manage_base.html:246 msgid "Account settings" msgstr "" @@ -1730,8 +1729,8 @@ msgstr "" #: warehouse/templates/manage/account.html:199 #: warehouse/templates/manage/account.html:201 #: warehouse/templates/manage/account.html:211 -#: warehouse/templates/manage/manage_base.html:266 -#: warehouse/templates/manage/manage_base.html:268 +#: warehouse/templates/manage/manage_base.html:282 +#: warehouse/templates/manage/manage_base.html:284 #: warehouse/templates/manage/release.html:118 #: warehouse/templates/manage/releases.html:172 #: warehouse/templates/manage/roles.html:38 @@ -1874,6 +1873,16 @@ msgstr "" msgid "Email" msgstr "" +#: warehouse/templates/includes/manage/manage-organization-menu.html:14 +#, python-format +msgid "Navigation for managing %(organization)s" +msgstr "" + +#: warehouse/templates/includes/manage/manage-organization-menu.html:19 +#: warehouse/templates/manage/organization/roles.html:35 +msgid "People" +msgstr "" + #: warehouse/templates/includes/manage/manage-project-menu.html:14 #, python-format msgid "Navigation for managing %(project)s" @@ -2764,8 +2773,12 @@ msgid "from %(ip_address)s" msgstr "" #: warehouse/templates/manage/manage_base.html:16 -#: warehouse/templates/manage/projects.html:143 -#: warehouse/templates/manage/projects.html:148 +#: warehouse/templates/manage/organizations.html:49 +#: warehouse/templates/manage/organizations.html:54 +#: warehouse/templates/manage/organizations.html:59 +#: warehouse/templates/manage/organizations.html:64 +#: warehouse/templates/manage/projects.html:142 +#: warehouse/templates/manage/projects.html:147 #: warehouse/templates/manage/releases.html:88 msgid "Manage" msgstr "" @@ -2944,34 +2957,40 @@ msgid "Your account" msgstr "" #: warehouse/templates/manage/manage_base.html:199 -#: warehouse/templates/manage/manage_base.html:219 +#: warehouse/templates/manage/manage_base.html:227 msgid "Account navigation" msgstr "" -#: warehouse/templates/manage/manage_base.html:279 -#: warehouse/templates/manage/manage_base.html:331 +#: warehouse/templates/manage/manage_base.html:211 +#: warehouse/templates/manage/manage_base.html:239 +#: warehouse/templates/manage/organizations.html:18 +msgid "Your organizations" +msgstr "" + +#: warehouse/templates/manage/manage_base.html:295 +#: warehouse/templates/manage/manage_base.html:347 msgid "This action cannot be undone!" msgstr "" -#: warehouse/templates/manage/manage_base.html:287 +#: warehouse/templates/manage/manage_base.html:303 msgid "Confirm your username to continue." msgstr "" -#: warehouse/templates/manage/manage_base.html:289 +#: warehouse/templates/manage/manage_base.html:305 #, python-format msgid "Confirm the %(item)s to continue." msgstr "" -#: warehouse/templates/manage/manage_base.html:297 -#: warehouse/templates/manage/manage_base.html:349 +#: warehouse/templates/manage/manage_base.html:313 +#: warehouse/templates/manage/manage_base.html:365 msgid "Cancel" msgstr "" -#: warehouse/templates/manage/manage_base.html:320 +#: warehouse/templates/manage/manage_base.html:336 msgid "close" msgstr "" -#: warehouse/templates/manage/manage_base.html:336 +#: warehouse/templates/manage/manage_base.html:352 msgid "Enter your password to continue." msgstr "" @@ -2990,67 +3009,117 @@ msgstr "" msgid "Back to projects" msgstr "" -#: warehouse/templates/manage/organizations.html:18 -msgid "Your organizations" +#: warehouse/templates/manage/organization/manage_organization_base.html:32 +#: warehouse/templates/manage/organizations.html:21 +#, python-format +msgid "Your organizations (%(organization_count)s)" msgstr "" -#: warehouse/templates/manage/organizations.html:25 -msgid "Create new organization" +#: warehouse/templates/manage/organizations.html:30 +msgid "Request Submitted" +msgstr "" + +#: warehouse/templates/manage/organizations.html:32 +#: warehouse/templates/manage/organizations.html:53 +msgid "This organization is not active" msgstr "" #: warehouse/templates/manage/organizations.html:32 +msgid "Inactive" +msgstr "" + +#: warehouse/templates/manage/organizations.html:34 +msgid "Manager" +msgstr "" + +#: warehouse/templates/manage/organizations.html:36 +#: warehouse/templates/manage/publishing.html:83 +#: warehouse/templates/manage/roles.html:43 +#: warehouse/templates/manage/roles.html:77 +#: warehouse/templates/manage/roles.html:88 +msgid "Owner" +msgstr "" + +#: warehouse/templates/manage/organizations.html:38 +msgid "Billing Manager" +msgstr "" + +#: warehouse/templates/manage/organizations.html:42 +#: warehouse/templates/manage/projects.html:41 +#: warehouse/templates/manage/projects.html:105 +#, python-format +msgid "Created %(creation_date)s" +msgstr "" + +#: warehouse/templates/manage/organizations.html:48 +msgid "You are not a manager or an owner of this organization" +msgstr "" + +#: warehouse/templates/manage/organizations.html:63 +msgid "Manage this organization" +msgstr "" + +#: warehouse/templates/manage/organizations.html:72 +msgid "You have not joined any organizations on PyPI, yet." +msgstr "" + +#: warehouse/templates/manage/organizations.html:81 +msgid "Create new organization" +msgstr "" + +#: warehouse/templates/manage/organizations.html:87 msgid "Organization account name" msgstr "" -#: warehouse/templates/manage/organizations.html:37 +#: warehouse/templates/manage/organizations.html:92 msgid "Select an organization account name" msgstr "" -#: warehouse/templates/manage/organizations.html:48 +#: warehouse/templates/manage/organizations.html:103 msgid "This account name will be used in URLs on PyPI." msgstr "" -#: warehouse/templates/manage/organizations.html:49 -#: warehouse/templates/manage/organizations.html:71 -#: warehouse/templates/manage/organizations.html:89 +#: warehouse/templates/manage/organizations.html:104 +#: warehouse/templates/manage/organizations.html:126 +#: warehouse/templates/manage/organizations.html:144 msgid "For example" msgstr "" -#: warehouse/templates/manage/organizations.html:55 +#: warehouse/templates/manage/organizations.html:110 msgid "Organization name" msgstr "" -#: warehouse/templates/manage/organizations.html:60 +#: warehouse/templates/manage/organizations.html:115 msgid "Name of your business, product, or project" msgstr "" -#: warehouse/templates/manage/organizations.html:77 +#: warehouse/templates/manage/organizations.html:132 msgid "️Organization URL" msgstr "" -#: warehouse/templates/manage/organizations.html:83 +#: warehouse/templates/manage/organizations.html:138 msgid "URL for your business, product, or project" msgstr "" -#: warehouse/templates/manage/organizations.html:95 +#: warehouse/templates/manage/organizations.html:150 msgid "Organization description" msgstr "" -#: warehouse/templates/manage/organizations.html:100 +#: warehouse/templates/manage/organizations.html:155 msgid "Description of your business, product, or project" msgstr "" -#: warehouse/templates/manage/organizations.html:114 +#: warehouse/templates/manage/organizations.html:169 msgid "️Organization type" msgstr "" -#: warehouse/templates/manage/organizations.html:126 +#: warehouse/templates/manage/organizations.html:181 msgid "" "Companies can create organization accounts as a paid service while " "community projects are granted complimentary access." msgstr "" -#: warehouse/templates/manage/organizations.html:132 +#: warehouse/templates/manage/organizations.html:187 msgid "Create" msgstr "" @@ -3059,69 +3128,63 @@ msgstr "" msgid "Pending invitations (%(project_count)s)" msgstr "" -#: warehouse/templates/manage/projects.html:41 -#: warehouse/templates/manage/projects.html:106 -#, python-format -msgid "Created %(creation_date)s" +#: warehouse/templates/manage/projects.html:71 +#: warehouse/templates/manage/projects.html:134 +msgid "This project requires 2FA to be enabled to manage" msgstr "" #: warehouse/templates/manage/projects.html:72 #: warehouse/templates/manage/projects.html:135 -msgid "This project requires 2FA to be enabled to manage" -msgstr "" - -#: warehouse/templates/manage/projects.html:73 -#: warehouse/templates/manage/projects.html:136 msgid "2FA Required" msgstr "" -#: warehouse/templates/manage/projects.html:79 -#: warehouse/templates/manage/projects.html:89 +#: warehouse/templates/manage/projects.html:78 +#: warehouse/templates/manage/projects.html:88 msgid "This is a critical project for the Python ecosystem" msgstr "" -#: warehouse/templates/manage/projects.html:80 -#: warehouse/templates/manage/projects.html:90 +#: warehouse/templates/manage/projects.html:79 +#: warehouse/templates/manage/projects.html:89 msgid "Critical Project" msgstr "" -#: warehouse/templates/manage/projects.html:82 -#: warehouse/templates/manage/projects.html:123 +#: warehouse/templates/manage/projects.html:81 +#: warehouse/templates/manage/projects.html:122 msgid "PyPI requires 2FA to be enabled to manage this project" msgstr "" -#: warehouse/templates/manage/projects.html:83 -#: warehouse/templates/manage/projects.html:124 +#: warehouse/templates/manage/projects.html:82 +#: warehouse/templates/manage/projects.html:123 msgid "2FA Mandated" msgstr "" -#: warehouse/templates/manage/projects.html:94 +#: warehouse/templates/manage/projects.html:93 msgid "Sole owner" msgstr "" -#: warehouse/templates/manage/projects.html:142 +#: warehouse/templates/manage/projects.html:141 msgid "Manage this project" msgstr "" -#: warehouse/templates/manage/projects.html:147 +#: warehouse/templates/manage/projects.html:146 msgid "You are not an owner of this project" msgstr "" -#: warehouse/templates/manage/projects.html:155 +#: warehouse/templates/manage/projects.html:154 msgid "View this project's public page" msgstr "" -#: warehouse/templates/manage/projects.html:156 -#: warehouse/templates/manage/projects.html:159 +#: warehouse/templates/manage/projects.html:155 +#: warehouse/templates/manage/projects.html:158 #: warehouse/templates/manage/releases.html:94 msgid "View" msgstr "" -#: warehouse/templates/manage/projects.html:158 +#: warehouse/templates/manage/projects.html:157 msgid "This project has no releases" msgstr "" -#: warehouse/templates/manage/projects.html:168 +#: warehouse/templates/manage/projects.html:166 #, python-format msgid "" "You have not uploaded any projects to PyPI, yet. To learn how to get " @@ -3159,13 +3222,6 @@ msgid "" "href=\"%(href)s\">here." msgstr "" -#: warehouse/templates/manage/publishing.html:83 -#: warehouse/templates/manage/roles.html:43 -#: warehouse/templates/manage/roles.html:77 -#: warehouse/templates/manage/roles.html:88 -msgid "Owner" -msgstr "" - #: warehouse/templates/manage/publishing.html:88 msgid "owner" msgstr "" @@ -3553,16 +3609,20 @@ msgstr "" msgid "Manage '%(project_name)s' collaborators" msgstr "" +#: warehouse/templates/manage/organization/roles.html:22 #: warehouse/templates/manage/roles.html:22 msgid "2FA enabled" msgstr "" +#: warehouse/templates/manage/organization/roles.html:23 +#: warehouse/templates/manage/organization/roles.html:28 #: warehouse/templates/manage/roles.html:23 #: warehouse/templates/manage/roles.html:28 #: warehouse/templates/manage/roles.html:53 msgid "2FA" msgstr "" +#: warehouse/templates/manage/organization/roles.html:27 #: warehouse/templates/manage/roles.html:27 msgid "2FA disabled" msgstr "" @@ -3571,7 +3631,7 @@ msgstr "" #, python-format msgid "" "Use this page to control which PyPI users can help you to manage " -"%(project_name)s" +"%(project_name)s." msgstr "" #: warehouse/templates/manage/roles.html:39 @@ -4114,6 +4174,27 @@ msgid "" " not work with PyPI." msgstr "" +#: warehouse/templates/manage/organization/manage_organization_base.html:21 +#, python-format +msgid "Manage '%(organization_name)s'" +msgstr "" + +#: warehouse/templates/manage/organization/manage_organization_base.html:41 +msgid "Back to organizations" +msgstr "" + +#: warehouse/templates/manage/organization/roles.html:18 +#, python-format +msgid "Manage people in '%(organization_name)s'" +msgstr "" + +#: warehouse/templates/manage/organization/roles.html:36 +#, python-format +msgid "" +"Use this page to control which PyPI users can help you to manage " +"%(organization_name)s." +msgstr "" + #: warehouse/templates/packaging/detail.html:106 msgid "view hashes" msgstr "" diff --git a/warehouse/manage/views.py b/warehouse/manage/views.py index df878e7ae075..3c487360a130 100644 --- a/warehouse/manage/views.py +++ b/warehouse/manage/views.py @@ -87,6 +87,11 @@ from warehouse.oidc.interfaces import TooManyOIDCRegistrations from warehouse.oidc.models import GitHubProvider, OIDCProvider from warehouse.organizations.interfaces import IOrganizationService +from warehouse.organizations.models import ( + Organization, + OrganizationRole, + OrganizationRoleType, +) from warehouse.packaging.models import ( File, JournalEntry, @@ -972,6 +977,57 @@ def delete_macaroon(self): return HTTPSeeOther(redirect_to) +def user_organizations(request): + """Return all the organizations for which the user has a privileged role.""" + organizations_managed = ( + request.db.query(Organization.id) + .join(OrganizationRole.organization) + .filter( + OrganizationRole.role_name == OrganizationRoleType.Manager, + OrganizationRole.user == request.user, + ) + .subquery() + ) + organizations_owned = ( + request.db.query(Organization.id) + .join(OrganizationRole.organization) + .filter( + OrganizationRole.role_name == OrganizationRoleType.Owner, + OrganizationRole.user == request.user, + ) + .subquery() + ) + organizations_billing = ( + request.db.query(Organization.id) + .join(OrganizationRole.organization) + .filter( + OrganizationRole.role_name == OrganizationRoleType.BillingManager, + OrganizationRole.user == request.user, + ) + .subquery() + ) + return { + "organizations_owned": ( + request.db.query(Organization) + .join(organizations_owned, Organization.id == organizations_owned.c.id) + .order_by(Organization.name) + .all() + ), + "organizations_managed": ( + request.db.query(Organization) + .join(organizations_managed, Organization.id == organizations_managed.c.id) + .order_by(Organization.name) + .all() + ), + "organizations_billing": ( + request.db.query(Organization) + .join(organizations_billing, Organization.id == organizations_billing.c.id) + .order_by(Organization.name) + .all() + ), + } + + @view_defaults( route_name="manage.organizations", renderer="manage/organizations.html", @@ -991,7 +1047,24 @@ def __init__(self, request): @property def default_response(self): + all_user_organizations = user_organizations(self.request) return { + "organizations": self.organization_service.get_organizations_by_user( + self.request.user.id + ), + **all_user_organizations, + "organizations_managed": list( + organization.name + for organization in all_user_organizations["organizations_managed"] + ), + "organizations_owned": list( + organization.name + for organization in all_user_organizations["organizations_owned"] + ), + "organizations_billing": list( + organization.name + for organization in all_user_organizations["organizations_billing"] + ), "create_organization_form": CreateOrganizationForm( organization_service=self.organization_service, ), @@ -1077,6 +1150,23 @@ def create_organization(self): return self.default_response +@view_config( + route_name="manage.organization.roles", + context=Organization, + renderer="manage/organization/roles.html", + uses_session=True, + require_methods=False, + # permission="manage:organization", + has_translations=True, + require_reauth=True, +) +def manage_organization_roles(organization, request): + if request.flags.enabled(AdminFlagValue.DISABLE_ORGANIZATIONS): + raise HTTPNotFound + + return {"organization": organization} + + @view_config( route_name="manage.projects", renderer="manage/projects.html", diff --git a/warehouse/organizations/interfaces.py b/warehouse/organizations/interfaces.py index 02894dc5e01e..f54a04824749 100644 --- a/warehouse/organizations/interfaces.py +++ b/warehouse/organizations/interfaces.py @@ -32,6 +32,22 @@ def find_organizationid(name): is no organization with the given name. """ + def get_organizations(): + """ + Return a list of all organization objects, or None if there are none. + """ + + def get_organizations_needing_approval(): + """ + Return a list of all organization objects in need of approval or None + if there are currently no organization requests. + """ + + def get_organizations_by_user(user_id): + """ + Return a list of all organization objects associated with a given user id. + """ + def add_organization(name, display_name, orgtype, link_url, description): """ Accepts a organization object, and attempts to create an organization with those diff --git a/warehouse/organizations/models.py b/warehouse/organizations/models.py index 38b8bdb860f0..e8070a836168 100644 --- a/warehouse/organizations/models.py +++ b/warehouse/organizations/models.py @@ -26,8 +26,7 @@ orm, sql, ) - -# from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm.exc import NoResultFound from sqlalchemy_utils.types.url import URLType from warehouse import db @@ -110,23 +109,22 @@ class OrganizationType(enum.Enum): Company = "Company" -# TODO: For future use -# class OrganizationFactory: -# def __init__(self, request): -# self.request = request -# -# def __getitem__(self, organization): -# try: -# return ( -# self.request.db.query(Organization) -# .filter( -# Organization.normalized_name -# == func.normalize_pep426_name(organization) -# ) -# .one() -# ) -# except NoResultFound: -# raise KeyError from None +class OrganizationFactory: + def __init__(self, request): + self.request = request + + def __getitem__(self, organization): + try: + return ( + self.request.db.query(Organization) + .filter( + Organization.normalized_name + == func.normalize_pep426_name(organization) + ) + .one() + ) + except NoResultFound: + raise KeyError from None # TODO: Determine if this should also utilize SitemapMixin and TwoFactorRequireable diff --git a/warehouse/organizations/services.py b/warehouse/organizations/services.py index 0558f6aa1413..6cbafa4f6eca 100644 --- a/warehouse/organizations/services.py +++ b/warehouse/organizations/services.py @@ -64,6 +64,36 @@ def find_organizationid(self, name): return organization.id + def get_organizations(self): + """ + Return a list of all organization objects, or None if there are none. + """ + return self.db.query(Organization).order_by(Organization.name).all() + + def get_organizations_needing_approval(self): + """ + Return a list of all organization objects in need of approval or None + if there are currently no organization requests. + """ + return ( + self.db.query(Organization) + .filter(Organization.is_approved == None) # noqa + .order_by(Organization.name) + .all() + ) + + def get_organizations_by_user(self, user_id): + """ + Return a list of all organization objects associated with a given user id. + """ + return ( + self.db.query(Organization) + .join(OrganizationRole, OrganizationRole.organization_id == Organization.id) + .filter(OrganizationRole.user_id == user_id) + .order_by(Organization.name) + .all() + ) + def add_organization(self, name, display_name, orgtype, link_url, description): """ Accepts a organization object, and attempts to create an organization with those diff --git a/warehouse/routes.py b/warehouse/routes.py index c3e28a28f529..5382660949db 100644 --- a/warehouse/routes.py +++ b/warehouse/routes.py @@ -222,6 +222,13 @@ def includeme(config): ) config.add_route("manage.account.token", "/manage/account/token/", domain=warehouse) config.add_route("manage.organizations", "/manage/organizations/", domain=warehouse) + config.add_route( + "manage.organization.roles", + "/manage/organization/{organization_name}/people/", + factory="warehouse.organizations.models:OrganizationFactory", + traverse="/{organization_name}", + domain=warehouse, + ) config.add_route("manage.projects", "/manage/projects/", domain=warehouse) config.add_route( "manage.project.settings", diff --git a/warehouse/static/images/users.png b/warehouse/static/images/users.png new file mode 100644 index 000000000000..a25395b01b60 Binary files /dev/null and b/warehouse/static/images/users.png differ diff --git a/warehouse/static/images/users.svg b/warehouse/static/images/users.svg new file mode 100644 index 000000000000..b591f93b5633 --- /dev/null +++ b/warehouse/static/images/users.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/warehouse/static/sass/blocks/_organization-snippet.scss b/warehouse/static/sass/blocks/_organization-snippet.scss new file mode 100644 index 000000000000..129a81defec4 --- /dev/null +++ b/warehouse/static/sass/blocks/_organization-snippet.scss @@ -0,0 +1,33 @@ +/*! + * 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 "snippet"; + +/* + A card that contains information about a organization. Often found in organization lists. + The card can be an "a" or "div" element, but if it contains a link, choosing + a top-level "a" element is recommended for accessibility reasons. + + +

+ // Package title + // Version // Optional! +

+

// Description

+
+*/ + +.organization-snippet { + @include snippet(url("../images/users.png"), url("../images/users.svg")); +} diff --git a/warehouse/static/sass/blocks/_package-snippet.scss b/warehouse/static/sass/blocks/_package-snippet.scss index db24f51789d6..472945c11e67 100644 --- a/warehouse/static/sass/blocks/_package-snippet.scss +++ b/warehouse/static/sass/blocks/_package-snippet.scss @@ -12,6 +12,8 @@ * limitations under the License. */ +@import "snippet"; + /* A card that contains information about a package. Often found in package lists. The card can be an "a" or "div" element, but if it contains a link, choosing @@ -27,92 +29,5 @@ */ .package-snippet { - @include card; - direction: ltr; - text-align: left; - display: block; - padding: ($spacing-unit / 2) 20px ($spacing-unit / 2) 75px; - margin: 0 0 20px; - - @media only screen and (max-width: $tablet) { - padding: $spacing-unit / 2; - } - - @media only screen and (min-width: $tablet + 1px) { - &, - &:hover { - // Use png fallback - background: $white url("../images/white-cube.png") no-repeat 0 50%; - // Or svg if the browser supports it - background-image: url("../images/white-cube.svg"), linear-gradient(transparent, transparent); - background-position: 20px; - } - } - - &__title { - @include h3; - padding-bottom: 0; - display: block; - - &--page-title, - &--page-title:first-child { - @include h1-title; - padding-top: 1px; - padding-bottom: 5px; - } - } - - &__meta { - padding: 1px 0 2px; - } - - &__version { - font-weight: $bold-font-weight; - } - - &__released { - font-weight: $base-font-weight; - float: right; - color: $text-color; - font-size: 1rem; - - @media only screen and (max-width: $tablet) { - float: none; - display: block; - } - } - - &__description { - clear: both; - color: $text-color; - } - - &__project-badge { - margin-left: 4px; - position: relative; - top: -1px; - } - - &__buttons { - flex-shrink: 0; - - .button { - display: inline-block; - float: left; - pointer-events: auto; - } - - .button--primary, - .button--danger { - margin-right: 5px; - } - } - - &--margin-bottom { - margin-bottom: 0; - - @media only screen and (max-width: $tablet) { - margin-bottom: 30px; - } - } + @include snippet(url("../images/white-cube.png"), url("../images/white-cube.svg")); } diff --git a/warehouse/static/sass/blocks/_snippet.scss b/warehouse/static/sass/blocks/_snippet.scss new file mode 100644 index 000000000000..ec884ef667c9 --- /dev/null +++ b/warehouse/static/sass/blocks/_snippet.scss @@ -0,0 +1,110 @@ +/*! + * 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. + */ + +/* + A card that contains information about a package or an organization. + - Extended by .package-snippet and often found in package lists. + - Extended by .organization-snippet and often found in package lists. +*/ + +@mixin snippet($snippet-icon-svg, $snippet-icon-png) { + @include card; + direction: ltr; + text-align: left; + display: block; + padding: ($spacing-unit / 2) 20px ($spacing-unit / 2) 75px; + margin: 0 0 20px; + + @media only screen and (max-width: $tablet) { + padding: $spacing-unit / 2; + } + + @media only screen and (min-width: $tablet + 1px) { + &, + &:hover { + // Use png fallback + background: $white $snippet-icon-png no-repeat 0 50%; + // Or svg if the browser supports it + background-image: $snippet-icon-svg, linear-gradient(transparent, transparent); + background-position: 20px; + } + } + + &__title { + @include h3; + padding-bottom: 0; + display: block; + + &--page-title, + &--page-title:first-child { + @include h1-title; + padding-top: 1px; + padding-bottom: 5px; + } + } + + &__meta { + padding: 1px 0 2px; + } + + &__version { + font-weight: $bold-font-weight; + } + + &__created { + font-weight: $base-font-weight; + float: right; + color: $text-color; + font-size: 1rem; + + @media only screen and (max-width: $tablet) { + float: none; + display: block; + } + } + + &__description { + clear: both; + color: $text-color; + } + + &__badge { + margin-left: 4px; + position: relative; + top: -1px; + } + + &__buttons { + flex-shrink: 0; + + .button { + display: inline-block; + float: left; + pointer-events: auto; + } + + .button--primary, + .button--danger { + margin-right: 5px; + } + } + + &--margin-bottom { + margin-bottom: 0; + + @media only screen and (max-width: $tablet) { + margin-bottom: 30px; + } + } +} diff --git a/warehouse/static/sass/warehouse.scss b/warehouse/static/sass/warehouse.scss index bfec85ddff5e..65153384a0b8 100644 --- a/warehouse/static/sass/warehouse.scss +++ b/warehouse/static/sass/warehouse.scss @@ -100,6 +100,7 @@ @import "blocks/modal"; @import "blocks/notification-bar"; /*rtl:begin:ignore*/ +@import "blocks/organization-snippet"; @import "blocks/package-description"; @import "blocks/package-header"; @import "blocks/package-snippet"; diff --git a/warehouse/templates/email/new-organization-requested/body.html b/warehouse/templates/email/new-organization-requested/body.html index b09819e63566..95f208c0ed28 100644 --- a/warehouse/templates/email/new-organization-requested/body.html +++ b/warehouse/templates/email/new-organization-requested/body.html @@ -16,5 +16,5 @@ {% block content %}

{% trans organization_name=organization_name %}Your request for a new PyPI organization named "{{ organization_name }}" has been submitted.{% endtrans %}

-

{% trans %}You will receive another email when the PyPI organization has been approved.{% endtrans %}

+

{% trans %}You will receive an email when the organization has been approved{% endtrans %}.

{% endblock %} diff --git a/warehouse/templates/email/new-organization-requested/body.txt b/warehouse/templates/email/new-organization-requested/body.txt index 73ac8c2e6b29..dcab1a3454d1 100644 --- a/warehouse/templates/email/new-organization-requested/body.txt +++ b/warehouse/templates/email/new-organization-requested/body.txt @@ -16,5 +16,5 @@ {% block content %} {% trans organization_name=organization_name %}Your request for a new PyPI organization named "{{ organization_name }}" has been submitted.{% endtrans %} -{% trans %}You will receive another email when the PyPI organization has been approved.{% endtrans %} +{% trans %}You will receive an email when the organization has been approved{% endtrans %}. {% endblock %} diff --git a/warehouse/templates/includes/manage/manage-organization-menu.html b/warehouse/templates/includes/manage/manage-organization-menu.html new file mode 100644 index 000000000000..5d2dc57352f1 --- /dev/null +++ b/warehouse/templates/includes/manage/manage-organization-menu.html @@ -0,0 +1,23 @@ +{# + # 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/warehouse/templates/manage/manage_base.html b/warehouse/templates/manage/manage_base.html index 404868fbe043..ec52afad62a0 100644 --- a/warehouse/templates/manage/manage_base.html +++ b/warehouse/templates/manage/manage_base.html @@ -204,6 +204,14 @@ {% trans %}Your projects{% endtrans %} + {% if not request.flags.enabled(AdminFlagValue.DISABLE_ORGANIZATIONS) %} +
  • + + + {% trans %}Your organizations{% endtrans %} + +
  • + {% endif %}
  • @@ -224,6 +232,14 @@ {% trans %}Your projects{% endtrans %}
  • + {% if not request.flags.enabled(AdminFlagValue.DISABLE_ORGANIZATIONS) %} +
  • + + + {% trans %}Your organizations{% endtrans %} + +
  • + {% endif %}
  • diff --git a/warehouse/templates/manage/organization/manage_organization_base.html b/warehouse/templates/manage/organization/manage_organization_base.html new file mode 100644 index 000000000000..c217e7df993c --- /dev/null +++ b/warehouse/templates/manage/organization/manage_organization_base.html @@ -0,0 +1,58 @@ +{# + # 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 "manage/manage_base.html" %} + +{% set user = request.user %} +{% set organizations = user.organizations %} +{% set current_organization = organization %} +{% set active_tab = active_tab|default('collaborators') %} + +{% block title %}{% trans organization_name=organization.name %}Manage '{{ organization_name }}'{% endtrans %}{% endblock %} + +{# Hide mobile search on manager pages #} +{% block mobile_search %}{% endblock %} + +{% block content %} +
    +
    +
    + +
    +
    + + + + {% trans %}Back to organizations{% endtrans %} + +
    +
    +

    {{ organization.name }}

    +
    + + {% with mode="mobile" %} + {% include "warehouse:templates/includes/manage/manage-organization-menu.html" %} + {% endwith %} + + {% block main %}{% endblock %} +
    + {% block mobile_tabs_bottom %}{% endblock %} +
    +
    +
    +{% endblock %} diff --git a/warehouse/templates/manage/organization/roles.html b/warehouse/templates/manage/organization/roles.html new file mode 100644 index 000000000000..3e50ac411496 --- /dev/null +++ b/warehouse/templates/manage/organization/roles.html @@ -0,0 +1,37 @@ +{# + # 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 "manage_organization_base.html" %} + +{% set active_tab = 'people' %} + +{% block title %}{% trans organization_name=organization.name %}Manage people in '{{ organization_name }}'{% endtrans %}{% endblock %} + +{% macro two_factor_badge(user) -%} + {% if user.has_two_factor %} + + {% trans %}2FA{% endtrans %} + + + {% else %} + + {% trans %}2FA{% endtrans %} + + + {% endif %} +{% endmacro %} + +{% block main %} +

    {% trans %}People{% endtrans %}

    +

    {% trans organization_name=organization.name %}Use this page to control which PyPI users can help you to manage {{ organization_name }}.{% endtrans %}

    +{% endblock %} diff --git a/warehouse/templates/manage/organizations.html b/warehouse/templates/manage/organizations.html index 1dacd0227bca..419e088e7362 100644 --- a/warehouse/templates/manage/organizations.html +++ b/warehouse/templates/manage/organizations.html @@ -18,12 +18,67 @@ {% block title %}{% trans %}Your organizations{% endtrans %}{% endblock %} {% block main %} -

    {{ title }}

    +

    {% trans organization_count=organizations|length %}Your organizations ({{ organization_count }}){% endtrans %}

    + +
    + {% for organization in organizations %} +
    +
    +
    +

    {{ organization.name }} + {% if organization.is_approved == None %} + {% trans %}Request Submitted{% endtrans %} + {% elif organization.is_active == False %} + {% trans %}Inactive{% endtrans %} + {% elif organization.name in organizations_managed %} + {% trans %}Manager{% endtrans %} + {% elif organization.name in organizations_owned %} + {% trans %}Owner{% endtrans %} + {% elif organization.name in organizations_billing %} + {% trans %}Billing Manager{% endtrans %} + {% endif %} +

    +

    + {% trans creation_date=humanize(organization.created) %}Created {{ creation_date }}{% endtrans %} +

    +
    +
    + {% if organization.name not in (organizations_managed + organizations_owned + organizations_billing) %} + {# Show disabled "Manage" button for non-manager and non-owner #} + + {% elif not organization.is_active %} + {# Show disabled "Manage" button for inactive organization #} + + {% elif organization.name in organizations_billing %} + {# Show "Manage" button for billing managers #} + + {% trans %}Manage{% endtrans %} + + {% else %} + {# Show "Manage" button for managers and owners #} + + {% trans %}Manage{% endtrans %} + + {% endif %} +
    +
    +
    + {% else %} +
    +

    {% trans %}You have not joined any organizations on PyPI, yet.{% endtrans %}

    +
    + {% endfor %} +
    + +
    {{ form_error_anchor(create_organization_form) }}

    {% trans %}Create new organization{% endtrans %}

    -
    {{ form_errors(create_organization_form) }} diff --git a/warehouse/templates/manage/projects.html b/warehouse/templates/manage/projects.html index 0c52f778a6e2..0e47d68e4026 100644 --- a/warehouse/templates/manage/projects.html +++ b/warehouse/templates/manage/projects.html @@ -57,7 +57,6 @@

    {% endif %}

    {% trans project_count=projects|length %}Your projects ({{ project_count }}){% endtrans %}

    - {% if projects %} {% set user_has_two_factor = request.user.has_two_factor %} {% for project in projects %} {% set release = project.releases[0] if project.releases else None %} @@ -69,29 +68,29 @@

    {{ project.name }} request.registry.settings["warehouse.two_factor_requirement.enabled"] and project.owners_require_2fa ) %} - + {% trans %}2FA Required{% endtrans %} {% elif ( request.registry.settings["warehouse.two_factor_mandate.enabled"] and project.pypi_mandates_2fa ) %} - + {% trans %}Critical Project{% endtrans %} - + {% trans %}2FA Mandated{% endtrans %} {% elif ( request.registry.settings["warehouse.two_factor_mandate.available"] and project.pypi_mandates_2fa ) %} - + {% trans %}Critical Project{% endtrans %} {% endif %} {% if project.name in projects_sole_owned %} - {% trans %}Sole owner{% endtrans %} + {% trans %}Sole owner{% endtrans %} {% endif %}

    {% if release %} @@ -162,11 +161,10 @@

    {{ project.name }}

    - {% endfor %} {% else %}

    {% trans href='https://packaging.python.org/' %}You have not uploaded any projects to PyPI, yet. To learn how to get started, visit the Python Packaging User Guide{% endtrans %}

    - {% endif %} + {% endfor %} {% endblock %} diff --git a/warehouse/templates/manage/roles.html b/warehouse/templates/manage/roles.html index 735e2d22149a..2f99f056f2cd 100644 --- a/warehouse/templates/manage/roles.html +++ b/warehouse/templates/manage/roles.html @@ -33,7 +33,7 @@ {% block main %}

    {% trans %}Collaborators{% endtrans %}

    -

    {% trans project_name=project.name %}Use this page to control which PyPI users can help you to manage {{ project_name }}{% endtrans %}

    +

    {% trans project_name=project.name %}Use this page to control which PyPI users can help you to manage {{ project_name }}.{% endtrans %}

    {% trans %}There are two possible roles for collaborators:{% endtrans %}

    diff --git a/warehouse/templates/search/results.html b/warehouse/templates/search/results.html index fb73b3f18861..390bf681605a 100644 --- a/warehouse/templates/search/results.html +++ b/warehouse/templates/search/results.html @@ -23,7 +23,7 @@

    {{ item.name }} {{ item.latest_version }} - {{ humanize(item.created) }} + {{ humanize(item.created) }}

    {{ item.summary }}