Skip to content

Commit 187b0df

Browse files
authored
Org admin enhancements (#17701)
* surface role and organization invitations throughout user/org/project admin * admin: default organization application view to submitted only * admin: organization applications, use datatables for pagination * orgs admin: surface all user email addresses and confliting applications
1 parent 8a27ad4 commit 187b0df

File tree

11 files changed

+221
-251
lines changed

11 files changed

+221
-251
lines changed

tests/unit/admin/views/test_organizations.py

Lines changed: 62 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -419,46 +419,21 @@ class TestOrganizationApplicationList:
419419
def test_no_query(self, db_request):
420420
organization_applications = sorted(
421421
OrganizationApplicationFactory.create_batch(30),
422-
key=lambda o: o.normalized_name,
423-
)
424-
result = views.organization_applications_list(db_request)
425-
426-
assert result == {
427-
"organization_applications": organization_applications[:25],
428-
"query": "",
429-
"terms": [],
430-
}
431-
432-
@pytest.mark.usefixtures("_enable_organizations")
433-
def test_with_page(self, db_request):
434-
organization_applications = sorted(
435-
OrganizationApplicationFactory.create_batch(30),
436-
key=lambda o: o.normalized_name,
422+
key=lambda o: o.submitted,
437423
)
438-
db_request.GET["page"] = "2"
439424
result = views.organization_applications_list(db_request)
440425

441426
assert result == {
442-
"organization_applications": organization_applications[25:],
427+
"organization_applications": organization_applications,
443428
"query": "",
444429
"terms": [],
445430
}
446431

447-
@pytest.mark.usefixtures("_enable_organizations")
448-
def test_with_invalid_page(self):
449-
request = pretend.stub(
450-
flags=pretend.stub(enabled=lambda *a: False),
451-
params={"page": "not an integer"},
452-
)
453-
454-
with pytest.raises(HTTPBadRequest):
455-
views.organization_applications_list(request)
456-
457432
@pytest.mark.usefixtures("_enable_organizations")
458433
def test_basic_query(self, db_request):
459434
organization_applications = sorted(
460435
OrganizationApplicationFactory.create_batch(5),
461-
key=lambda o: o.normalized_name,
436+
key=lambda o: o.submitted,
462437
)
463438
db_request.GET["q"] = organization_applications[0].name
464439
result = views.organization_applications_list(db_request)
@@ -471,7 +446,7 @@ def test_basic_query(self, db_request):
471446
def test_name_query(self, db_request):
472447
organization_applications = sorted(
473448
OrganizationApplicationFactory.create_batch(5),
474-
key=lambda o: o.normalized_name,
449+
key=lambda o: o.submitted,
475450
)
476451
db_request.GET["q"] = f"name:{organization_applications[0].name}"
477452
result = views.organization_applications_list(db_request)
@@ -484,7 +459,7 @@ def test_name_query(self, db_request):
484459
def test_organization_application_query(self, db_request):
485460
organization_applications = sorted(
486461
OrganizationApplicationFactory.create_batch(5),
487-
key=lambda o: o.normalized_name,
462+
key=lambda o: o.submitted,
488463
)
489464
db_request.GET["q"] = (
490465
f"organization:{organization_applications[0].display_name}"
@@ -504,7 +479,7 @@ def test_organization_application_query(self, db_request):
504479
def test_url_query(self, db_request):
505480
organization_applications = sorted(
506481
OrganizationApplicationFactory.create_batch(5),
507-
key=lambda o: o.normalized_name,
482+
key=lambda o: o.submitted,
508483
)
509484
db_request.GET["q"] = f"url:{organization_applications[0].link_url}"
510485
result = views.organization_applications_list(db_request)
@@ -517,7 +492,7 @@ def test_url_query(self, db_request):
517492
def test_description_query(self, db_request):
518493
organization_applications = sorted(
519494
OrganizationApplicationFactory.create_batch(5),
520-
key=lambda o: o.normalized_name,
495+
key=lambda o: o.submitted,
521496
)
522497
db_request.GET["q"] = (
523498
f"description:'{organization_applications[0].description}'"
@@ -537,7 +512,7 @@ def test_description_query(self, db_request):
537512
def test_is_approved_query(self, db_request):
538513
organization_applications = sorted(
539514
OrganizationApplicationFactory.create_batch(5),
540-
key=lambda o: o.normalized_name,
515+
key=lambda o: o.submitted,
541516
)
542517
organization_applications[0].is_approved = True
543518
organization_applications[1].is_approved = True
@@ -557,7 +532,7 @@ def test_is_approved_query(self, db_request):
557532
def test_is_declined_query(self, db_request):
558533
organization_applications = sorted(
559534
OrganizationApplicationFactory.create_batch(5),
560-
key=lambda o: o.normalized_name,
535+
key=lambda o: o.submitted,
561536
)
562537
organization_applications[0].is_approved = True
563538
organization_applications[1].is_approved = True
@@ -577,7 +552,7 @@ def test_is_declined_query(self, db_request):
577552
def test_is_submitted_query(self, db_request):
578553
organization_applications = sorted(
579554
OrganizationApplicationFactory.create_batch(5),
580-
key=lambda o: o.normalized_name,
555+
key=lambda o: o.submitted,
581556
)
582557
organization_applications[0].is_approved = True
583558
organization_applications[1].is_approved = True
@@ -638,163 +613,84 @@ def test_invalid_type_query(self, db_request):
638613
def test_is_invalid_query(self, db_request):
639614
organization_applications = sorted(
640615
OrganizationApplicationFactory.create_batch(5),
641-
key=lambda o: o.normalized_name,
616+
key=lambda o: o.submitted,
642617
)
643618
db_request.GET["q"] = "is:not-actually-a-valid-query"
644619
result = views.organization_applications_list(db_request)
645620

646621
assert result == {
647-
"organization_applications": organization_applications[:25],
622+
"organization_applications": organization_applications,
648623
"query": "is:not-actually-a-valid-query",
649624
"terms": ["is:not-actually-a-valid-query"],
650625
}
651626

652627

653628
class TestOrganizationApplicationDetail:
654629
@pytest.mark.usefixtures("_enable_organizations")
655-
def test_detail(self):
656-
admin = pretend.stub(
657-
id="admin-id",
658-
username="admin",
659-
name="Admin",
660-
public_email="[email protected]",
630+
def test_detail(self, db_request):
631+
organization_application = OrganizationApplicationFactory.create()
632+
db_request.matchdict["organization_application_id"] = (
633+
organization_application.id
661634
)
662-
user = pretend.stub(
663-
id="user-id",
664-
username="example",
665-
name="Example",
666-
public_email="[email protected]",
667-
)
668-
user_service = pretend.stub(
669-
get_user=lambda userid, **kw: {admin.id: admin, user.id: user}[userid],
670-
)
671-
organization_application = pretend.stub(
672-
id=pretend.stub(),
673-
name="example",
674-
display_name="Example",
675-
orgtype=pretend.stub(name="Company"),
676-
link_url="https://www.example.com/",
677-
description=(
678-
"This company is for use in illustrative examples in documents "
679-
"You may use this company in literature without prior "
680-
"coordination or asking for permission."
681-
),
682-
is_approved=None,
683-
submitted_by_id=user.id,
684-
submitted_by=user,
685-
)
686-
organization_service = pretend.stub(
687-
get_organization_application=lambda *a, **kw: organization_application,
688-
)
689-
request = pretend.stub(
690-
flags=pretend.stub(enabled=lambda *a: False),
691-
find_service=lambda iface, **kw: {
692-
IUserService: user_service,
693-
IOrganizationService: organization_service,
694-
}[iface],
695-
matchdict={"organization_application_id": pretend.stub()},
696-
)
697-
698-
assert views.organization_application_detail(request) == {
699-
"user": user,
635+
assert views.organization_application_detail(db_request) == {
636+
"user": organization_application.submitted_by,
637+
"conflicting_applications": [],
700638
"organization_application": organization_application,
701639
}
702640

703641
@pytest.mark.usefixtures("_enable_organizations")
704-
def test_detail_is_approved_true(self):
705-
admin = pretend.stub(
706-
id="admin-id",
707-
username="admin",
708-
name="Admin",
709-
public_email="[email protected]",
710-
)
711-
user = pretend.stub(
712-
id="user-id",
713-
username="example",
714-
name="Example",
715-
public_email="[email protected]",
642+
def test_detail_is_approved_true(self, db_request):
643+
organization_application = OrganizationApplicationFactory.create(
644+
is_approved=True
716645
)
717-
user_service = pretend.stub(
718-
get_user=lambda userid, **kw: {admin.id: admin, user.id: user}[userid],
646+
db_request.matchdict["organization_application_id"] = (
647+
organization_application.id
719648
)
720-
organization_application = pretend.stub(
721-
id=pretend.stub(),
722-
name="example",
723-
display_name="Example",
724-
orgtype=pretend.stub(name="Company"),
725-
link_url="https://www.example.com/",
726-
description=(
727-
"This company is for use in illustrative examples in documents "
728-
"You may use this company in literature without prior "
729-
"coordination or asking for permission."
730-
),
731-
is_approved=True,
732-
submitted_by_id=user.id,
733-
submitted_by=user,
734-
)
735-
organization_service = pretend.stub(
736-
get_organization_application=lambda *a, **kw: organization_application,
737-
)
738-
request = pretend.stub(
739-
flags=pretend.stub(enabled=lambda *a: False),
740-
find_service=lambda iface, **kw: {
741-
IUserService: user_service,
742-
IOrganizationService: organization_service,
743-
}[iface],
744-
matchdict={"organization_application_id": pretend.stub()},
745-
)
746-
747-
assert views.organization_application_detail(request) == {
748-
"user": user,
649+
assert views.organization_application_detail(db_request) == {
650+
"user": organization_application.submitted_by,
651+
"conflicting_applications": [],
749652
"organization_application": organization_application,
750653
}
751654

752655
@pytest.mark.usefixtures("_enable_organizations")
753-
def test_detail_is_approved_false(self):
754-
admin = pretend.stub(
755-
id="admin-id",
756-
username="admin",
757-
name="Admin",
758-
public_email="[email protected]",
759-
)
760-
user = pretend.stub(
761-
id="user-id",
762-
username="example",
763-
name="Example",
764-
public_email="[email protected]",
656+
def test_detail_is_approved_false(self, db_request):
657+
organization_application = OrganizationApplicationFactory.create(
658+
is_approved=False
765659
)
766-
user_service = pretend.stub(
767-
get_user=lambda userid, **kw: {admin.id: admin, user.id: user}[userid],
768-
)
769-
organization_application = pretend.stub(
770-
id=pretend.stub(),
771-
name="example",
772-
display_name="Example",
773-
orgtype=pretend.stub(name="Company"),
774-
link_url="https://www.example.com/",
775-
description=(
776-
"This company is for use in illustrative examples in documents "
777-
"You may use this company in literature without prior "
778-
"coordination or asking for permission."
779-
),
780-
is_approved=False,
781-
submitted_by_id=user.id,
782-
submitted_by=user,
783-
)
784-
organization_service = pretend.stub(
785-
get_organization_application=lambda *a, **kw: organization_application,
786-
)
787-
request = pretend.stub(
788-
flags=pretend.stub(enabled=lambda *a: False),
789-
find_service=lambda iface, **kw: {
790-
IUserService: user_service,
791-
IOrganizationService: organization_service,
792-
}[iface],
793-
matchdict={"organization_application_id": pretend.stub()},
660+
db_request.matchdict["organization_application_id"] = (
661+
organization_application.id
794662
)
663+
assert views.organization_application_detail(db_request) == {
664+
"user": organization_application.submitted_by,
665+
"conflicting_applications": [],
666+
"organization_application": organization_application,
667+
}
795668

796-
assert views.organization_application_detail(request) == {
797-
"user": user,
669+
@pytest.mark.usefixtures("_enable_organizations")
670+
@pytest.mark.parametrize(
671+
("name", "conflicts"),
672+
[
673+
("pypi", ["PyPI", "pypi"]),
674+
("py-pi", ["Py-PI", "PY-PI"]),
675+
],
676+
)
677+
def test_detail_conflicting_applications(self, db_request, name, conflicts):
678+
organization_application = OrganizationApplicationFactory.create(
679+
name=name, is_approved=False
680+
)
681+
conflicting_applications = sorted(
682+
[
683+
OrganizationApplicationFactory.create(name=conflict)
684+
for conflict in conflicts
685+
],
686+
key=lambda o: o.submitted,
687+
)
688+
db_request.matchdict["organization_application_id"] = (
689+
organization_application.id
690+
)
691+
assert views.organization_application_detail(db_request) == {
692+
"user": organization_application.submitted_by,
693+
"conflicting_applications": conflicting_applications,
798694
"organization_application": organization_application,
799695
}
800696

warehouse/admin/static/js/warehouse.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,19 @@ if (malwareReportsTable.length) {
198198
table.buttons().container().appendTo($(".col-md-6:eq(0)", table.table().container()));
199199
}
200200

201+
// Organization Applications
202+
let OrganizationApplicationsTable = $("#organization-applications");
203+
if (OrganizationApplicationsTable.length) {
204+
let table = OrganizationApplicationsTable.DataTable({
205+
displayLength: 25,
206+
lengthChange: true,
207+
order: [[4, "asc"], [0, "asc"]], // created, alpha name
208+
responsive: true,
209+
});
210+
new $.fn.dataTable.Buttons(table, {buttons: ["copy", "csv", "colvis"]});
211+
table.buttons().container().appendTo($(".col-md-6:eq(0)", table.table().container()));
212+
}
213+
201214
// Link Checking
202215
const links = document.querySelectorAll("a[data-check-link-url]");
203216
links.forEach(function(link){

warehouse/admin/templates/admin/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
</a>
120120
</li>
121121
<li class="nav-item">
122-
<a href="{{ request.route_path('admin.organization_application.list') }}" class="nav-link">
122+
<a href="{{ request.route_path('admin.organization_application.list') }}?q=is%3Asubmitted" class="nav-link">
123123
<i class="fa fa-sitemap nav-icon"></i> <p>Organization Applications</p>
124124
</a>
125125
</li>

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ <h3 class="card-title">Organization Request</h3>
171171
<div class="col-sm-7">
172172
{% if user %}
173173
<a href="{{ request.route_url('admin.user.detail', username=user.username) }}" title="{{ user.username }}">{{ user.username }}</a><br>
174-
<a href="mailto:{{ user.email }}">{{ user.email }} <i class="fa-solid fa-envelope"></i></a>
174+
{% for email in user.emails %}
175+
<a href="mailto:{{ email.email }}">{{ email.email }}</a> {% if email.primary %}(primary){% endif %} {% if email.verified %}<i class="fa fa-check text-green"></i>{% else %}<i class="fa fa-times text-red"></i>{% endif %}
176+
{% endfor %}
175177
{% else %}
176178
<i>n/a</i>
177179
{% endif %}
@@ -185,6 +187,25 @@ <h3 class="card-title">Organization Request</h3>
185187
<input class="form-control" value="{{ organization_application.name }}" readonly>
186188
</div>
187189
</div>
190+
{% if conflicting_applications %}
191+
<div class="form-group">
192+
<label class="col-sm-5 control-label">
193+
Conflicting Applications <i class="fa fa-scale-unbalanced text-red"></i>
194+
</label>
195+
<div class="col-sm-7">
196+
<table class="table">
197+
<tr><th>Application</th><th>Submitted</th><th>Requestor</th></tr>
198+
{% for application in conflicting_applications %}
199+
<tr>
200+
<td><a target="_blank" href="{{ request.route_url('admin.organization_application.detail', organization_application_id=application.id) }}">{{ application.name }}</a></td>
201+
<td>{{ application.submitted|format_date() }}</td>
202+
<td><a target="_blank" href="{{ request.route_url('admin.user.detail', username=application.submitted_by.username) }}">{{ application.submitted_by.username }}</a></td>
203+
</tr>
204+
{% endfor %}
205+
</table>
206+
</div>
207+
</div>
208+
{% endif %}
188209
<div class="form-group">
189210
<label class="col-sm-5 control-label">
190211
Organization Name

0 commit comments

Comments
 (0)