Skip to content

Commit f020942

Browse files
authored
org admin work (#17944)
* don't require typed confirm for approve/decline its already a two-step process * allow admins to rename organizations
1 parent 4db8db3 commit f020942

File tree

14 files changed

+292
-120
lines changed

14 files changed

+292
-120
lines changed

tests/unit/admin/test_routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ def test_includeme():
3434
"/admin/organizations/{organization_id}/",
3535
domain=warehouse,
3636
),
37+
pretend.call(
38+
"admin.organization.rename",
39+
"/admin/organizations/{organization_id}/rename/",
40+
domain=warehouse,
41+
),
3742
pretend.call(
3843
"admin.organization_application.list",
3944
"/admin/organization_applications/",

tests/unit/admin/views/test_organizations.py

Lines changed: 68 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,73 @@ def test_detail_not_found(self):
328328
views.organization_detail(request)
329329

330330

331+
class TestOrganizationActions:
332+
@pytest.mark.usefixtures("_enable_organizations")
333+
def test_rename_not_found(self, db_request):
334+
admin = UserFactory.create()
335+
336+
db_request.matchdict = {
337+
"organization_id": "deadbeef-dead-beef-dead-beefdeadbeef"
338+
}
339+
db_request.params = {
340+
"new_organization_name": "widget",
341+
}
342+
db_request.user = admin
343+
db_request.route_path = pretend.call_recorder(_organization_application_routes)
344+
345+
with pytest.raises(HTTPNotFound):
346+
views.organization_rename(db_request)
347+
348+
@pytest.mark.usefixtures("_enable_organizations")
349+
def test_rename(self, db_request):
350+
admin = UserFactory.create()
351+
organization = OrganizationFactory.create(name="example")
352+
353+
db_request.matchdict = {"organization_id": organization.id}
354+
db_request.params = {
355+
"new_organization_name": "widget",
356+
}
357+
db_request.user = admin
358+
db_request.route_path = pretend.call_recorder(_organization_application_routes)
359+
db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None)
360+
361+
result = views.organization_rename(db_request)
362+
363+
assert db_request.session.flash.calls == [
364+
pretend.call(
365+
'"example" organization renamed "widget"',
366+
queue="success",
367+
),
368+
]
369+
assert result.status_code == 303
370+
assert result.location == f"/admin/organizations/{organization.id}/"
371+
372+
@pytest.mark.usefixtures("_enable_organizations")
373+
def test_rename_fails_on_conflict(self, db_request):
374+
admin = UserFactory.create()
375+
organization = OrganizationFactory.create(name="widget")
376+
organization = OrganizationFactory.create(name="example")
377+
378+
db_request.matchdict = {"organization_id": organization.id}
379+
db_request.params = {
380+
"new_organization_name": "widget",
381+
}
382+
db_request.user = admin
383+
db_request.route_path = pretend.call_recorder(_organization_application_routes)
384+
db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None)
385+
386+
result = views.organization_rename(db_request)
387+
388+
assert db_request.session.flash.calls == [
389+
pretend.call(
390+
'Organization name "widget" has been used',
391+
queue="error",
392+
),
393+
]
394+
assert result.status_code == 303
395+
assert result.location == f"/admin/organizations/{organization.id}/"
396+
397+
331398
class TestOrganizationApplicationList:
332399
@pytest.mark.usefixtures("_enable_organizations")
333400
def test_no_query(self, db_request):
@@ -688,7 +755,7 @@ def _organization_application_routes(
688755
raise ValueError("No dummy route found")
689756

690757

691-
class TestActions:
758+
class TestOrganizationApplicationActions:
692759
@pytest.mark.usefixtures("_enable_organizations")
693760
def test_approve(self, db_request):
694761
admin = UserFactory.create()
@@ -784,48 +851,6 @@ def _approve(*a, **kw):
784851
assert result.status_code == 303
785852
assert result.location == "/admin/"
786853

787-
@pytest.mark.usefixtures("_enable_organizations")
788-
def test_approve_wrong_confirmation_input(self, db_request):
789-
admin = UserFactory.create()
790-
user = UserFactory.create()
791-
organization_application = OrganizationApplicationFactory.create(
792-
name="example", submitted_by=user
793-
)
794-
organization = OrganizationFactory.create(name="example")
795-
796-
organization_service = pretend.stub(
797-
get_organization_application=lambda *a, **kw: organization_application,
798-
approve_organization_application=pretend.call_recorder(
799-
lambda *a, **kw: organization
800-
),
801-
)
802-
803-
db_request.matchdict = {
804-
"organization_application_id": organization_application.id
805-
}
806-
db_request.params = {"organization_name": "incorrect", "message": "Welcome!"}
807-
db_request.user = admin
808-
db_request.route_path = pretend.call_recorder(_organization_application_routes)
809-
db_request.find_service = pretend.call_recorder(
810-
lambda iface, context: organization_service
811-
)
812-
db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None)
813-
814-
result = views.organization_application_approve(db_request)
815-
816-
assert organization_service.approve_organization_application.calls == []
817-
assert db_request.session.flash.calls == [
818-
pretend.call(
819-
"Wrong confirmation input",
820-
queue="error",
821-
),
822-
]
823-
assert result.status_code == 303
824-
assert (
825-
result.location
826-
== f"/admin/organization_applications/{organization_application.id}/"
827-
)
828-
829854
@pytest.mark.usefixtures("_enable_organizations")
830855
def test_approve_not_found(self):
831856
organization_service = pretend.stub(
@@ -1142,43 +1167,6 @@ def test_decline_turbo_mode(self, db_request):
11421167
== f"/admin/organization_applications/{organization_application.id}/"
11431168
)
11441169

1145-
@pytest.mark.usefixtures("_enable_organizations")
1146-
def test_decline_wrong_confirmation_input(self, db_request):
1147-
admin = UserFactory.create()
1148-
user = UserFactory.create()
1149-
organization_application = OrganizationApplicationFactory.create(
1150-
name="example", submitted_by=user
1151-
)
1152-
1153-
organization_service = pretend.stub(
1154-
get_organization_application=lambda *a, **kw: organization_application,
1155-
decline_organization_application=pretend.call_recorder(
1156-
lambda *a, **kw: organization_application
1157-
),
1158-
)
1159-
1160-
db_request.matchdict = {
1161-
"organization_application_id": organization_application.id
1162-
}
1163-
db_request.params = {"organization_name": "incorrect", "message": "Welcome!"}
1164-
db_request.user = admin
1165-
db_request.route_path = pretend.call_recorder(_organization_application_routes)
1166-
db_request.find_service = pretend.call_recorder(
1167-
lambda iface, context: organization_service
1168-
)
1169-
db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None)
1170-
1171-
result = views.organization_application_decline(db_request)
1172-
1173-
assert db_request.session.flash.calls == [
1174-
pretend.call("Wrong confirmation input", queue="error"),
1175-
]
1176-
assert result.status_code == 303
1177-
assert (
1178-
result.location
1179-
== f"/admin/organization_applications/{organization_application.id}/"
1180-
)
1181-
11821170
@pytest.mark.usefixtures("_enable_organizations")
11831171
def test_decline_not_found(self):
11841172
organization_service = pretend.stub(

tests/unit/organizations/test_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def test_acl(self, db_session):
152152
(
153153
Permissions.AdminOrganizationsRead,
154154
Permissions.AdminOrganizationsWrite,
155+
Permissions.AdminOrganizationsNameWrite,
155156
),
156157
),
157158
(Allow, "group:moderators", Permissions.AdminOrganizationsRead),
@@ -369,6 +370,7 @@ def test_acl(self, db_session):
369370
(
370371
Permissions.AdminOrganizationsRead,
371372
Permissions.AdminOrganizationsWrite,
373+
Permissions.AdminOrganizationsNameWrite,
372374
),
373375
),
374376
(Allow, "group:moderators", Permissions.AdminOrganizationsRead),

tests/unit/organizations/test_services.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,51 @@ def test_rename_organization(self, organization_service, db_request):
624624
.count()
625625
)
626626

627+
def test_rename_organization_back(self, organization_service, db_request):
628+
organization = OrganizationFactory.create()
629+
original_name = organization.name
630+
631+
organization_service.rename_organization(organization.id, "some_new_name")
632+
assert organization.name == "some_new_name"
633+
634+
db_organization = organization_service.get_organization(organization.id)
635+
assert db_organization.name == "some_new_name"
636+
637+
organization_service.db.flush()
638+
assert (
639+
db_request.db.query(OrganizationNameCatalog)
640+
.filter(
641+
OrganizationNameCatalog.normalized_name == organization.normalized_name
642+
)
643+
.count()
644+
) == 1
645+
646+
organization_service.rename_organization(organization.id, original_name)
647+
assert organization.name == original_name
648+
649+
db_organization = organization_service.get_organization(organization.id)
650+
assert db_organization.name == original_name
651+
652+
organization_service.db.flush()
653+
assert (
654+
db_request.db.query(OrganizationNameCatalog)
655+
.filter(
656+
OrganizationNameCatalog.normalized_name == organization.normalized_name
657+
)
658+
.count()
659+
) == 1
660+
661+
def test_rename_fails_if_entry_exists_for_another_org(
662+
self, organization_service, db_request
663+
):
664+
conflicting_org = OrganizationFactory.create()
665+
organization = OrganizationFactory.create()
666+
667+
with pytest.raises(ValueError): # noqa: PT011
668+
organization_service.rename_organization(
669+
organization.id, conflicting_org.name
670+
)
671+
627672
def test_update_organization(self, organization_service, db_request):
628673
organization = OrganizationFactory.create()
629674

tests/unit/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ def test_root_factory_access_control_list():
572572
Permissions.AdminObservationsWrite,
573573
Permissions.AdminOrganizationsRead,
574574
Permissions.AdminOrganizationsWrite,
575+
Permissions.AdminOrganizationsNameWrite,
575576
Permissions.AdminProhibitedEmailDomainsRead,
576577
Permissions.AdminProhibitedEmailDomainsWrite,
577578
Permissions.AdminProhibitedProjectsRead,

warehouse/admin/routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def includeme(config):
2929
"/admin/organizations/{organization_id}/",
3030
domain=warehouse,
3131
)
32+
config.add_route(
33+
"admin.organization.rename",
34+
"/admin/organizations/{organization_id}/rename/",
35+
domain=warehouse,
36+
)
3237

3338
config.add_route(
3439
"admin.organization_application.list",

warehouse/admin/templates/admin/organization_applications/detail.html

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,7 @@ <h4 class="modal-title" id="approveModalLabel">
149149
<div class="modal-body">
150150
<p>
151151
This will approve <a href="{{ request.route_url('admin.user.detail', username=user.username) }}" title="{{ user.username }}">{{ user.username }}</a>'s request for a new organization named <strong>{{ organization_application.name }}</strong>
152-
<button type="button" class="copy-text" data-copy-text="{{ organization_application.name }}">
153-
<i class="fa fa-copy" aria-hidden="true"></i>
154-
</button>.
155152
</p>
156-
<div class="form-group">
157-
<label for="approveModalOrganizationName">
158-
Organization Name
159-
</label>
160-
<input type="text" id="approveModalOrganizationName" class="form-control" aria-describedby="approveModalOrganizationNameHelpBlock" name="organization_name" placeholder="{{ organization_application.name }}" required>
161-
<span id="approveModalOrganizationNameHelpBlock">
162-
Type the organization name "{{ organization_application.name }}" to confirm you are <strong>approving</strong> the request.
163-
</span>
164-
</div>
165153
<div class="form-group">
166154
<label for="approveModalMessage">
167155
Message
@@ -280,19 +268,7 @@ <h4 class="modal-title" id="declineModalLabel">
280268
<div class="modal-body">
281269
<p>
282270
This will decline <a href="{{ request.route_url('admin.user.detail', username=user.username) }}" title="{{ user.username }}">{{ user.username }}</a>'s request for a new organization named <strong>{{ organization_application.name }}</strong>.
283-
<button type="button" class="copy-text" data-copy-text="{{ organization_application.name }}">
284-
<i class="fa fa-copy" aria-hidden="true"></i>
285-
</button>.
286271
</p>
287-
<div class="form-group">
288-
<label for="declineModalOrganizationName">
289-
Organization Name
290-
</label>
291-
<input type="text" id="declineModalOrganizationName" class="form-control" aria-describedby="declineModalOrganizationNameHelpBlock" name="organization_name" placeholder="{{ organization_application.name }}" required>
292-
<span id="declineModalOrganizationNameHelpBlock">
293-
Type the organization name "{{ organization_application.name }}" to confirm you are <strong>declining</strong> the request.
294-
</span>
295-
</div>
296272
<div class="form-group">
297273
<label for="declineModalMessage">
298274
Message

warehouse/admin/templates/admin/organizations/detail.html

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,55 @@ <h3 class="widget-user-username text-center">{{ organization.display_name }}</h3
4141
<h2 class="card-title">Actions</h2>
4242
</div>
4343
<div class="card-body">
44+
45+
<button
46+
type="button"
47+
class="btn btn-danger btn-block"
48+
data-toggle="modal"
49+
data-target="#renameModal" {{ 'disabled' if not request.has_permission(Permissions.AdminOrganizationsWrite) }}
50+
title="Rename"
51+
>
52+
<i class="icon fa fa-rotate"></i> Rename
53+
</button>
54+
55+
<div class="modal fade" id="renameModal" tabindex="-1" role="dialog">
56+
<form method="POST" action="{{ request.route_path('admin.organization.rename', organization_id=organization.id) }}">
57+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
58+
<div class="modal-dialog" role="document">
59+
<div class="modal-content">
60+
<div class="modal-header">
61+
<h4 class="modal-title" id="renameModalLabel">
62+
Rename {{ organization.name }}?
63+
</h4>
64+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
65+
<span aria-hidden="true">&times;</span>
66+
</button>
67+
</div>
68+
<div class="modal-body">
69+
<p>
70+
This will rename <strong>{{ organization.name }}</strong>
71+
</p>
72+
<div class="form-group">
73+
<label for="renameModalOrganizationName">
74+
New Organization Name
75+
</label>
76+
<input type="text" id="renameModalOrganizationName" class="form-control" aria-describedby="renameModalOrganizationNameHelpBlock" name="new_organization_name" placeholder="{{ organization.name }}" required>
77+
<span id="renameModalOrganizationNameHelpBlock">
78+
Type the NEW organization name for "{{ organization.name }}" to confirm you are <strong>renaming</strong> the organization.
79+
</span>
80+
</div>
81+
</div>
82+
<div class="modal-footer justify-content-between">
83+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
84+
<button type="submit" class="btn btn-danger">
85+
<i class="icon fa fa-check"></i> Rename organization
86+
</button>
87+
</div>
88+
</div>
89+
</div>
90+
</form>
91+
</div>
92+
4493
</div>
4594
</div>
4695
</div>

0 commit comments

Comments
 (0)