Skip to content

Commit c968e58

Browse files
authored
surface conflicting prefixes when reviewing organizations (#18036)
* surface conflicting prefixes when reviewing organizations * surface all conflicts in one UI
1 parent 0093be2 commit c968e58

File tree

4 files changed

+78
-13
lines changed

4 files changed

+78
-13
lines changed

tests/unit/admin/views/test_organizations.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -701,30 +701,38 @@ def test_detail_is_approved_false(self, db_request):
701701

702702
@pytest.mark.usefixtures("_enable_organizations")
703703
@pytest.mark.parametrize(
704-
("name", "conflicts"),
704+
("name", "conflicts", "conflicting_prefixes", "not_conflicting"),
705705
[
706-
("pypi", ["PyPI", "pypi"]),
707-
("py-pi", ["Py-PI", "PY-PI"]),
706+
(
707+
"pypi",
708+
["PyPI", "pypi"],
709+
["pypi-common", "PyPi_rocks", "pypi-team-garbage"],
710+
["py-pi"],
711+
),
712+
("py-pi", ["Py-PI", "PY-PI"], ["py", "py-pi_dot-com"], ["pypi"]),
708713
],
709714
)
710-
def test_detail_conflicting_applications(self, db_request, name, conflicts):
715+
def test_detail_conflicting_applications(
716+
self, db_request, name, conflicts, conflicting_prefixes, not_conflicting
717+
):
711718
organization_application = OrganizationApplicationFactory.create(
712719
name=name, status=OrganizationApplicationStatus.Declined
713720
)
714721
conflicting_applications = sorted(
715722
[
716723
OrganizationApplicationFactory.create(name=conflict)
717-
for conflict in conflicts
724+
for conflict in conflicts + conflicting_prefixes
718725
],
719726
key=lambda o: o.submitted,
720727
)
728+
[OrganizationApplicationFactory.create(name=name) for name in not_conflicting]
721729
db_request.matchdict["organization_application_id"] = (
722730
organization_application.id
723731
)
724732
result = views.organization_application_detail(db_request)
725733
assert result["user"] == organization_application.submitted_by
726734
assert result["form"].name.data == organization_application.name
727-
assert result["conflicting_applications"] == conflicting_applications
735+
assert set(result["conflicting_applications"]) == set(conflicting_applications)
728736
assert result["organization_application"] == organization_application
729737

730738
@pytest.mark.usefixtures("_enable_organizations")

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,14 +384,18 @@ <h3 class="card-title">Organization Request{% if information_requests %}{% if ou
384384
{% if conflicting_applications %}
385385
<div class="form-group">
386386
<label class="col-12 control-label">
387-
Conflicting Applications <i class="fa fa-scale-unbalanced text-red"></i>
387+
Conflicting Applications <i class="fa fa-clipboard-question text-orange"></i>
388388
</label>
389389
<div class="col-12">
390390
<table class="table">
391-
<tr><th>Application</th><th>Submitted</th><th>Requestor</th></tr>
391+
<tr><th>Application</th><th>Status</th><th>Submitted</th><th>Requestor</th></tr>
392392
{% for application in conflicting_applications %}
393393
<tr>
394-
<td><a target="_blank" href="{{ request.route_url('admin.organization_application.detail', organization_application_id=application.id) }}">{{ application.name }}</a></td>
394+
<td>
395+
{% if application.normalized_name == organization_application.normalized_name %}<i class="fa fa-equals text-red"></i>{% endif %}
396+
<a target="_blank" href="{{ request.route_url('admin.organization_application.detail', organization_application_id=application.id) }}">{{ application.name }}</a>
397+
</td>
398+
<td>{{ application.status }}</td>
395399
<td>{{ application.submitted|format_date() }}</td>
396400
<td><a target="_blank" href="{{ request.route_url('admin.user.detail', username=application.submitted_by.username) }}">{{ application.submitted_by.username }}</a></td>
397401
</tr>

warehouse/admin/views/organizations.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from paginate_sqlalchemy import SqlalchemyOrmPage as SQLAlchemyORMPage
1616
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPSeeOther
1717
from pyramid.view import view_config
18-
from sqlalchemy import or_
18+
from sqlalchemy import desc, func, or_
1919
from sqlalchemy.orm import joinedload
2020

2121
from warehouse.accounts.interfaces import IUserService
@@ -361,14 +361,36 @@ def organization_application_detail(request):
361361
)
362362
return HTTPSeeOther(location=request.current_route_path())
363363

364+
parts = organization_application.normalized_name.split("-")
364365
conflicting_applications = (
365366
request.db.query(OrganizationApplication)
366367
.filter(
367-
OrganizationApplication.normalized_name
368-
== organization_application.normalized_name
368+
or_(
369+
*(
370+
[
371+
OrganizationApplication.normalized_name == parts[0],
372+
OrganizationApplication.normalized_name.startswith(
373+
parts[0] + "-"
374+
),
375+
]
376+
+ [
377+
OrganizationApplication.normalized_name.startswith(
378+
"-".join(parts[: i + 1])
379+
)
380+
for i in range(1, len(parts))
381+
]
382+
)
383+
)
369384
)
370385
.filter(OrganizationApplication.id != organization_application.id)
371-
.order_by(OrganizationApplication.submitted)
386+
.order_by(
387+
desc(
388+
func.similarity(
389+
OrganizationApplication.normalized_name,
390+
organization_application.normalized_name,
391+
)
392+
)
393+
)
372394
.all()
373395
)
374396

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
add pg_trgm extension
14+
15+
Revision ID: 13c1c0ac92e9
16+
Revises: c8384ca429fc
17+
Create Date: 2025-04-29 08:37:33.788528
18+
"""
19+
20+
from alembic import op
21+
22+
revision = "13c1c0ac92e9"
23+
down_revision = "c8384ca429fc"
24+
25+
26+
def upgrade():
27+
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
28+
29+
30+
def downgrade():
31+
pass

0 commit comments

Comments
 (0)