Skip to content

Commit 871556a

Browse files
divbzerosterbo
authored andcommitted
Request organization account (pypi#11184)
* admin-new-organization-requested email template * new-orgnization-requested email template * Test admin-new-organization-requested email * Test new-organization-requested email * Translate *-organization-requested consistently * `make translations` for *-organization-requested * Remove translations from admin-* emails On second thought, we probably don't want localization for admin emails. * Initial cut at db model architecture * Ran code thru make reformat and lint (pypa#11070) * Added tests for new db models (pypa#11070) * Numerous tweaks to db models * Require value for is_active and ran reformat. * Regenerate migrations for Organization models docker-compose run web python -m warehouse db revision --autogenerate --message "Create Organization models" make reformat Forced to regenerate migrations due to recent database changes (pypi#11157). * Numerous tweaks to alembic scripts * Initial implementation of organization service. * Implementing organization events functionality. * Added tests for organization services. * Added OrganizationFactory class for model. * Blank /manage/organizations/ page * Create organization form on /manage/organizations/ - Organization account name - Organization name - Organization URL - Organization description - Organization type * Register DatabaseOrganizationService Found the droids that we're looking for. * POST /manage/organizations/ database updates * Add .get_admins method to user service * POST /manage/organizations/ email notifications * Blank /admin/organizations/approve/ page This is a placeholder so we can reference `admin.organization.approve` as a route in the admin-new-organization-requested email. * Test GET /manage/organizations/ - `ManageOrganizationsViews.default_response` - `ManageOrganizationsViews.manage_organizations()` * Translations for /manage/organizations/ make translations * Fixed OrganizationType enum. * Test POST /manage/organizations/ - `ManageOrganizationsViews.create_organization()` * Test CreateOrganizationForm * Placeholder to test /admin/organizations/approve/ Provides code coverage for the blank page added in 2c70616. * Test `DatabaseUserService.get_admins()` * NFC: Add comments about intentionally blank page * Record events for POST /manage/organizations/ Co-Authored-By: sterbo <matt.sterba@gmail.com> * Test record events for POST /manage/organizations/ * Functional test for POST /manage/organizations/ * Comment out `OrganizationFactory` for future use Co-Authored-By: sterbo <matt.sterba@gmail.com> * NFC: Fix camel case for class names * Converted OrganizationRoleType to SQLAlchemy Enum * Add disable-organizations global admin flag * Add `AdminFlagValue.DISABLE_ORGANIZATIONS` * Modified org name catalog to store normalized name * {OrganizationEvents => Organization.Events} - Remove `OrganizationEvents` class - Add `HasEvents` mixing to `Organization` - Update references {OrganizationEvents => Organization.Events} - Update database migration {organization_id => source_id} * Store id instead of username in new events `Organization.Event` with tag: - organization:create - organization:catalog_entry:add - organization:organization_role:invite - organization:organization_role:accepted `User.Event` with tag: - account:organization_role:accepted * Display CreateOrganizationForm errors If there is a validation error, return the existing invalid form instead of a new blank form so user can actually see that validation error. * Tweak naming in events data {*_id => *_user_id} - {created_by_id => created_by_user_id} - {submitted_by_id => submitted_by_user_id} Discussed with @ewdurbin. Using `*_user_id` seems more clear. Co-authored-by: sterbo <matt.sterba@gmail.com>
1 parent 488b145 commit 871556a

39 files changed

+2419
-14
lines changed

tests/common/db/organizations.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import datetime
14+
15+
import factory
16+
import faker
17+
18+
from warehouse.organizations.models import (
19+
Organization,
20+
OrganizationInvitation,
21+
OrganizationNameCatalog,
22+
OrganizationProject,
23+
OrganizationRole,
24+
)
25+
26+
from .accounts import UserFactory
27+
from .base import WarehouseFactory
28+
from .packaging import ProjectFactory
29+
30+
fake = faker.Faker()
31+
32+
33+
class OrganizationFactory(WarehouseFactory):
34+
class Meta:
35+
model = Organization
36+
37+
id = factory.Faker("uuid4", cast_to=None)
38+
name = factory.Faker("word")
39+
normalized_name = factory.Faker("word")
40+
display_name = factory.Faker("word")
41+
orgtype = "Community"
42+
link_url = factory.Faker("uri")
43+
description = factory.Faker("sentence")
44+
is_active = True
45+
is_approved = False
46+
created = factory.Faker(
47+
"date_time_between_dates",
48+
datetime_start=datetime.datetime(2020, 1, 1),
49+
datetime_end=datetime.datetime(2022, 1, 1),
50+
)
51+
date_approved = factory.Faker(
52+
"date_time_between_dates", datetime_start=datetime.datetime(2020, 1, 1)
53+
)
54+
55+
56+
class OrganizationEventFactory(WarehouseFactory):
57+
class Meta:
58+
model = Organization.Event
59+
60+
source = factory.SubFactory(OrganizationFactory)
61+
62+
63+
class OrganizationNameCatalogFactory(WarehouseFactory):
64+
class Meta:
65+
model = OrganizationNameCatalog
66+
67+
name = factory.Faker("orgname")
68+
organization_id = factory.Faker("uuid4", cast_to=None)
69+
70+
71+
class OrganizationRoleFactory(WarehouseFactory):
72+
class Meta:
73+
model = OrganizationRole
74+
75+
role_name = "Owner"
76+
user = factory.SubFactory(UserFactory)
77+
organization = factory.SubFactory(OrganizationFactory)
78+
79+
80+
class OrganizationInvitationFactory(WarehouseFactory):
81+
class Meta:
82+
model = OrganizationInvitation
83+
84+
invite_status = "pending"
85+
token = "test_token"
86+
user = factory.SubFactory(UserFactory)
87+
organization = factory.SubFactory(OrganizationFactory)
88+
89+
90+
class OrganizationProjectFactory(WarehouseFactory):
91+
class Meta:
92+
model = OrganizationProject
93+
94+
id = factory.Faker("uuid4", cast_to=None)
95+
is_active = True
96+
organization = factory.SubFactory(OrganizationFactory)
97+
project = factory.SubFactory(ProjectFactory)

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from warehouse.email.interfaces import IEmailSender
4646
from warehouse.macaroons import services as macaroon_services
4747
from warehouse.metrics import IMetricsService
48+
from warehouse.organizations import services as organization_services
4849

4950
from .common.db import Session
5051

@@ -285,6 +286,13 @@ def macaroon_service(db_session):
285286
return macaroon_services.DatabaseMacaroonService(db_session)
286287

287288

289+
@pytest.fixture
290+
def organization_service(db_session, remote_addr):
291+
return organization_services.DatabaseOrganizationService(
292+
db_session, remote_addr=remote_addr
293+
)
294+
295+
288296
@pytest.fixture
289297
def token_service(app_config):
290298
return account_services.TokenService(secret="secret", salt="salt", max_age=21600)

tests/functional/manage/test_views.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,91 @@
1515
from webob.multidict import MultiDict
1616

1717
from warehouse.accounts.interfaces import IPasswordBreachedService, IUserService
18+
from warehouse.admin.flags import AdminFlagValue
1819
from warehouse.manage import views
20+
from warehouse.organizations.interfaces import IOrganizationService
21+
from warehouse.organizations.models import OrganizationType
1922

2023
from ...common.db.accounts import EmailFactory, UserFactory
2124

2225

2326
class TestManageAccount:
2427
def test_save_account(self, pyramid_services, user_service, db_request):
2528
breach_service = pretend.stub()
29+
organization_service = pretend.stub()
2630
pyramid_services.register_service(user_service, IUserService, None)
2731
pyramid_services.register_service(
2832
breach_service, IPasswordBreachedService, None
2933
)
34+
pyramid_services.register_service(
35+
organization_service, IOrganizationService, None
36+
)
3037
user = UserFactory.create(name="old name")
3138
EmailFactory.create(primary=True, verified=True, public=True, user=user)
3239
db_request.user = user
3340
db_request.method = "POST"
3441
db_request.path = "/manage/accounts/"
3542
db_request.POST = MultiDict({"name": "new name", "public_email": ""})
36-
views.ManageAccountViews(db_request).save_account()
3743

44+
views.ManageAccountViews(db_request).save_account()
3845
user = user_service.get_user(user.id)
46+
3947
assert user.name == "new name"
4048
assert user.public_email is None
49+
50+
51+
class TestManageOrganizations:
52+
def test_create_organization(
53+
self,
54+
pyramid_services,
55+
user_service,
56+
organization_service,
57+
db_request,
58+
monkeypatch,
59+
):
60+
pyramid_services.register_service(user_service, IUserService, None)
61+
pyramid_services.register_service(
62+
organization_service, IOrganizationService, None
63+
)
64+
user = UserFactory.create(name="old name")
65+
EmailFactory.create(primary=True, verified=True, public=True, user=user)
66+
db_request.user = user
67+
db_request.method = "POST"
68+
db_request.path = "/manage/organizations/"
69+
db_request.POST = MultiDict(
70+
{
71+
"name": "psf",
72+
"display_name": "Python Software Foundation",
73+
"orgtype": "Community",
74+
"link_url": "https://www.python.org/psf/",
75+
"description": (
76+
"To promote, protect, and advance the Python programming "
77+
"language, and to support and facilitate the growth of a "
78+
"diverse and international community of Python programmers"
79+
),
80+
}
81+
)
82+
monkeypatch.setattr(
83+
db_request,
84+
"flags",
85+
pretend.stub(enabled=pretend.call_recorder(lambda *a: False)),
86+
)
87+
send_email = pretend.call_recorder(lambda *a, **kw: None)
88+
monkeypatch.setattr(
89+
views, "send_admin_new_organization_requested_email", send_email
90+
)
91+
monkeypatch.setattr(views, "send_new_organization_requested_email", send_email)
92+
93+
views.ManageOrganizationsViews(db_request).create_organization()
94+
organization = organization_service.get_organization_by_name(
95+
db_request.POST["name"]
96+
)
97+
98+
assert db_request.flags.enabled.calls == [
99+
pretend.call(AdminFlagValue.DISABLE_ORGANIZATIONS),
100+
]
101+
assert organization.name == db_request.POST["name"]
102+
assert organization.display_name == db_request.POST["display_name"]
103+
assert organization.orgtype == OrganizationType[db_request.POST["orgtype"]]
104+
assert organization.link_url == db_request.POST["link_url"]
105+
assert organization.description == db_request.POST["description"]

tests/unit/accounts/test_services.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,14 @@ def test_get_user_by_email_failure(self, user_service):
408408

409409
assert found_user is None
410410

411+
def test_get_admins(self, user_service):
412+
admin = UserFactory.create(is_superuser=True)
413+
user = UserFactory.create(is_superuser=False)
414+
admins = user_service.get_admins()
415+
416+
assert admin in admins
417+
assert user not in admins
418+
411419
def test_disable_password(self, user_service):
412420
user = UserFactory.create()
413421

tests/unit/admin/test_routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ def test_includeme():
2626

2727
assert config.add_route.calls == [
2828
pretend.call("admin.dashboard", "/admin/", domain=warehouse),
29+
pretend.call(
30+
"admin.organization.approve",
31+
"/admin/organizations/approve/",
32+
domain=warehouse,
33+
),
2934
pretend.call("admin.user.list", "/admin/users/", domain=warehouse),
3035
pretend.call("admin.user.detail", "/admin/users/{user_id}/", domain=warehouse),
3136
pretend.call(
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
import pretend
14+
15+
from warehouse.admin.views import organizations as views
16+
17+
18+
class TestOrganizations:
19+
def test_approve(self):
20+
assert views.approve(pretend.stub()) == {}

0 commit comments

Comments
 (0)