Skip to content

Commit 9002e57

Browse files
authored
feat(admin): add modal to delete an email address (#18039)
In some cases, removing an email address is desirable, such as when the user has already added a correct second address to their account, and editing the existing invalid one would be a duplicate record. Signed-off-by: Mike Fiedler <[email protected]>
1 parent f62af2a commit 9002e57

File tree

5 files changed

+122
-0
lines changed

5 files changed

+122
-0
lines changed

tests/unit/admin/test_routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ def test_includeme():
101101
factory="warehouse.accounts.models:UserFactory",
102102
traverse="/{username}",
103103
),
104+
pretend.call(
105+
"admin.user.delete_email",
106+
"/admin/users/{username}/delete_email/",
107+
domain=warehouse,
108+
factory="warehouse.accounts.models:UserFactory",
109+
traverse="/{username}",
110+
),
104111
pretend.call(
105112
"admin.user.delete",
106113
"/admin/users/{username}/delete/",

tests/unit/admin/views/test_users.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,3 +1562,41 @@ def test_user_email_domain_check(self, db_request):
15621562
]
15631563
assert user.primary_email.domain_last_checked is not None
15641564
assert user.primary_email.domain_last_status == ["active"]
1565+
1566+
1567+
class TestUserEmailDelete:
1568+
def test_user_email_delete(self, db_request):
1569+
user = UserFactory.create(with_verified_primary_email=True)
1570+
email = EmailFactory.create(user=user, primary=False, verified=False)
1571+
1572+
db_request.POST["email_address"] = email.email
1573+
db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/foobar")
1574+
db_request.session = pretend.stub(
1575+
flash=pretend.call_recorder(lambda *a, **kw: None)
1576+
)
1577+
1578+
result = views.user_email_delete(user, db_request)
1579+
1580+
assert isinstance(result, HTTPSeeOther)
1581+
assert result.headers["Location"] == "/foobar"
1582+
assert db_request.session.flash.calls == [
1583+
pretend.call(f"Email address '{email.email}' deleted", queue="success")
1584+
]
1585+
assert email.email not in user.emails
1586+
1587+
def test_user_email_delete_not_found(self, db_request):
1588+
user = UserFactory.create(with_verified_primary_email=True)
1589+
1590+
db_request.POST["email_address"] = "[email protected]"
1591+
db_request.route_path = pretend.call_recorder(lambda *a, **kw: "/foobar")
1592+
db_request.session = pretend.stub(
1593+
flash=pretend.call_recorder(lambda *a, **kw: None)
1594+
)
1595+
1596+
result = views.user_email_delete(user, db_request)
1597+
1598+
assert isinstance(result, HTTPSeeOther)
1599+
assert result.headers["Location"] == "/foobar"
1600+
assert db_request.session.flash.calls == [
1601+
pretend.call("Email not found", queue="error")
1602+
]

warehouse/admin/routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ def includeme(config):
9999
domain=warehouse,
100100
traverse="/{username}",
101101
)
102+
config.add_route(
103+
"admin.user.delete_email",
104+
"/admin/users/{username}/delete_email/",
105+
domain=warehouse,
106+
factory="warehouse.accounts.models:UserFactory",
107+
traverse="/{username}",
108+
)
102109
config.add_route(
103110
"admin.user.delete",
104111
"/admin/users/{username}/delete/",

warehouse/admin/templates/admin/users/detail.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,12 @@ <h3 class="card-title">Emails</h3>
556556
<i class="fa fa-times text-red"></i> Unverify Reason: {{ field.unverify_reason.data.value }}
557557
</div>
558558
{% endif %}
559+
<button type="button"
560+
class="btn btn-sm btn-danger"
561+
data-toggle="modal"
562+
data-target="#deleteEmail-{{ field.email.id }}">
563+
<i class="fa fa-trash"></i>
564+
</button>
559565
</div>
560566
<div class="row">
561567
<div class="col-sm">Domain Status: {{ field.domain_last_status.data }}</div>
@@ -1091,5 +1097,38 @@ <h4 class="modal-title" id="checkDomainStatusModalLabel">Check domain status?</h
10911097
</div>
10921098
</form>
10931099
</div>
1100+
1101+
<div class="modal fade"
1102+
id="deleteEmail-{{ email.id }}-email"
1103+
tabindex="-1"
1104+
role="dialog">
1105+
<form method="POST"
1106+
action="{{ request.route_path('admin.user.delete_email', username=user.username, email_address=email.email.data) }}">
1107+
<input name="csrf_token"
1108+
type="hidden"
1109+
value="{{ request.session.get_csrf_token() }}">
1110+
<input name="email_address" type="hidden" value="{{ email.email.data }}">
1111+
<div class="modal-dialog" role="document">
1112+
<div class="modal-content">
1113+
<div class="modal-header">
1114+
<h4 class="modal-title" id="deleteEmailModalLabel">Delete email?</h4>
1115+
<button type="button" class="close" data-dismiss="modal">
1116+
<span>&times;</span>
1117+
</button>
1118+
</div>
1119+
<div class="modal-body">
1120+
<p>Are you sure you want to delete this email address?</p>
1121+
<p>Email address:</p>
1122+
<code>{{ email.email.data }}</code>
1123+
</div>
1124+
<div class="modal-footer">
1125+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
1126+
<button type="submit" class="btn btn-danger">Delete</button>
1127+
</div>
1128+
</div>
1129+
</div>
1130+
</form>
1131+
</div>
1132+
10941133
{% endfor %}
10951134
{% endblock %}

warehouse/admin/views/users.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13+
from __future__ import annotations
14+
1315
import datetime
1416
import shlex
17+
import typing
1518

1619
from collections import defaultdict
1720
from secrets import token_urlsafe
@@ -49,6 +52,9 @@
4952
from warehouse.packaging.models import JournalEntry, Project, Release, Role
5053
from warehouse.utils.paginate import paginate_url_factory
5154

55+
if typing.TYPE_CHECKING:
56+
from pyramid.request import Request
57+
5258

5359
@view_config(
5460
route_name="admin.user.list",
@@ -301,6 +307,31 @@ def user_add_email(user, request):
301307
return HTTPSeeOther(request.route_path("admin.user.detail", username=user.username))
302308

303309

310+
@view_config(
311+
route_name="admin.user.delete_email",
312+
require_methods=["POST"],
313+
permission=Permissions.AdminUsersEmailWrite,
314+
uses_session=True,
315+
require_csrf=True,
316+
context=User,
317+
)
318+
def user_email_delete(user: User, request: Request) -> HTTPSeeOther:
319+
email = request.db.scalar(
320+
select(Email).where(
321+
Email.email == request.POST.get("email_address"), Email.user == user
322+
)
323+
)
324+
if not email:
325+
request.session.flash("Email not found", queue="error")
326+
return HTTPSeeOther(
327+
request.route_path("admin.user.detail", username=user.username)
328+
)
329+
330+
request.db.delete(email)
331+
request.session.flash(f"Email address {email.email!r} deleted", queue="success")
332+
return HTTPSeeOther(request.route_path("admin.user.detail", username=user.username))
333+
334+
304335
def _nuke_user(user, request):
305336
# Delete all the user's projects
306337
projects = request.db.query(Project).filter(

0 commit comments

Comments
 (0)