Skip to content

Commit 0709a56

Browse files
authored
Merge pull request #90 from arXiv/bdc34/directives-current-condition
mail debug, submit debug, current directives as the condition
2 parents 1b40278 + 34c47dc commit 0709a56

13 files changed

Lines changed: 537 additions & 46 deletions

File tree

submit_ce/ui/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ def get_device_type(user_agent):
4242
return user_agent[:25]
4343

4444

45+
# Bits of the "classic" capability code produced by
46+
# arxiv.auth.legacy.util.compute_capabilities:
47+
# flag_edit_users -> 2 (admin)
48+
# flag_email_verified -> 4
49+
# flag_edit_system -> 8 (system/"god", i.e. dev)
4550
# TODO move these to somewhere under arxiv.auth.auth
46-
ADMIN_MASK = 1
47-
DEV_MASK = 1<<2
51+
ADMIN_MASK = 1 << 1
52+
DEV_MASK = 1 << 3
4853

4954
def is_admin(session: Session)->bool:
5055
return bool(getattr(session.authorizations, "classic", 0) & ADMIN_MASK)

submit_ce/ui/auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ def user_and_client_from_session(session: auth_domian.Session) -> Tuple[User, Cl
268268
if is_admin(session):
269269
user = StaffUser(
270270
user_id=session.user.user_id,
271+
username=session.user.username,
271272
name=name,
272273
email=session.user.email,
273274
endorsements = get_endorsements(session.user),

submit_ce/ui/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,26 @@ def authorized_client(app, authorized_user_session):
188188
yield app.test_client(jwt=jwt)
189189

190190

191+
@pytest.fixture
192+
def admin_client(app, authorized_user_session):
193+
"""Authorized client whose user has the classic dev/system capability.
194+
195+
Needed for the ``/debug/<submission_id>`` routes, which are gated by
196+
``is_admin_or_dev``. ``request_auth`` recomputes the ``classic`` capability
197+
code from the database user (ignoring whatever is in the JWT), so we flip
198+
``flag_edit_system`` on the underlying TapirUser rather than editing the
199+
token. Reuses the same user as ``authorized_client`` so ownership-based
200+
fixtures still line up.
201+
"""
202+
session, jwt = authorized_user_session
203+
with app.app_context():
204+
db_user = Session.get(classic.TapirUser, int(session.user.user_id))
205+
db_user.flag_edit_system = 1
206+
Session.commit()
207+
app.test_client_class = ClientArxivAuth
208+
yield app.test_client(jwt=jwt)
209+
210+
191211
#################### submissions in different stages ####################
192212
@pytest.fixture(scope="function")
193213
def sub_created(app, authorized_user):

submit_ce/ui/controllers/__init__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#ruff: noqa: F401
44

55
from http import HTTPStatus as status
6+
from typing import Optional
67

78
from arxiv.auth.domain import Session
9+
from flask import current_app
810
from werkzeug.datastructures import MultiDict
911

1012
from submit_ce.ui.routes.flow_control import advance_to_current
@@ -23,16 +25,40 @@
2325
from .new.verify_user import verify
2426
from .util import Response
2527

28+
_GS_CONSOLE = "https://console.cloud.google.com/storage/browser/"
29+
30+
31+
def _gs_links(submission_id: str) -> tuple[Optional[str], Optional[str]]:
32+
"""Return ``(gs_path, console_url)`` for the submission dir.
33+
34+
``gs_path`` is the ``gs://{bucket}/{path}`` URI (used as the link text)
35+
and ``console_url`` is the matching GCS console browser URL (the href).
36+
Both are None unless the file store reports a gs:// path (i.e. the GS
37+
store); local/null stores return "" and get ``(None, None)``.
38+
"""
39+
try:
40+
path = current_app.api.get_file_store().get_full_submission_path(
41+
str(submission_id))
42+
except Exception:
43+
return None, None
44+
if path and path.startswith("gs://"):
45+
return path, _GS_CONSOLE + path[len("gs://"):]
46+
return None, None
47+
48+
2649
def submission_status(method: str, params: MultiDict, session: Session,
2750
submission_id: str) -> Response:
2851
#user, client = util.user_and_client_from_session(session)
2952

3053
# Will raise NotFound if there is no such submission.
3154
submission, submission_events = get_submission(submission_id)
55+
gs_path, gs_console_url = _gs_links(submission_id)
3256
response_data = {
3357
'submission': submission,
3458
'submission_id': submission_id,
35-
'events': submission_events
59+
'events': submission_events,
60+
'gs_path': gs_path,
61+
'gs_console_url': gs_console_url,
3662
}
3763
return response_data, status.OK, {}
3864

@@ -41,9 +67,12 @@ def submission_edit(method: str, params: MultiDict, session: Session,
4167
submission_id: str) -> Response:
4268
"""Cause flow_control to go to the current_stage of the Submission."""
4369
submission, submission_events = get_submission(submission_id)
70+
gs_path, gs_console_url = _gs_links(submission_id)
4471
response_data = {
4572
'submission': submission,
4673
'submission_id': submission_id,
4774
'events': submission_events,
75+
'gs_path': gs_path,
76+
'gs_console_url': gs_console_url,
4877
}
4978
return advance_to_current((response_data, status.OK, {}))

submit_ce/ui/routes/ui.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from arxiv.auth.auth import scopes
77
from arxiv.auth.auth.decorators import scoped
88
from arxiv.base import logging, alerts
9-
from flask import Blueprint, make_response, redirect, request, render_template, url_for, send_file
9+
from flask import Blueprint, make_response, redirect, request, render_template, url_for, send_file, current_app
1010
from flask import Response as FResponse
1111
from markupsafe import Markup
1212
from werkzeug import Response as WResponse
@@ -223,21 +223,13 @@ def create_replacement(submission_id: str):
223223

224224

225225
@UI.route('/<submission_id>', methods=["GET"])
226-
@scoped(scopes.VIEW_SUBMISSION, authorizer=is_owner,
227-
unauthorized=redirect_to_login)
228-
def submission_status(submission_id: str) -> Response:
229-
"""Display the current state of the submission."""
230-
return handle(cntrls.submission_status, 'submit/status.html',
231-
'Submission status', submission_id)
232-
233-
234226
@UI.route('/<submission_id>/edit', methods=['GET'])
235227
@scoped(scopes.VIEW_SUBMISSION, authorizer=is_owner,
236228
unauthorized=redirect_to_login)
237229
@flow_control()
238230
def submission_edit(submission_id: str) -> Response:
239231
"""Redirects to current edit stage of the submission."""
240-
return handle(cntrls.submission_edit, 'submit/status.html',
232+
return handle(cntrls.submission_edit, 'debug/status.html',
241233
'Submission status', submission_id, flow_controlled=True)
242234

243235
# # TODO: remove me!!
@@ -651,6 +643,39 @@ def debug_logout() -> Response:
651643
return response
652644

653645

646+
@UI.route('/debug/mail', methods=["GET"])
647+
@scoped(scopes.VIEW_SUBMISSION, authorizer=is_admin_or_dev,
648+
unauthorized=redirect_to_login)
649+
def get_debug_mail() -> Response:
650+
"""Dev-only: show email captured by the in-memory email service.
651+
652+
Only available when ``EMAIL_MODE`` is ``TESTING`` and the configured
653+
email service is the in-memory ``EmailInMemory`` capture. In any other
654+
mode real mail was dispatched and there is nothing held in process to
655+
show, so this returns 404.
656+
"""
657+
from submit_ce.implementations.email.email_in_memory import EmailInMemory
658+
if settings.EMAIL_MODE != "TESTING":
659+
raise NotFound()
660+
service = current_app.api.get_email_service()
661+
if not isinstance(service, EmailInMemory):
662+
raise NotFound()
663+
return make_response(
664+
render_template('debug/debug_mail.html',
665+
pagetitle='Debug Mail',
666+
emails=service.sent),
667+
200)
668+
669+
670+
@UI.route('/debug/<submission_id>', methods=["GET"])
671+
@scoped(scopes.VIEW_SUBMISSION, authorizer=is_admin_or_dev,
672+
unauthorized=redirect_to_login)
673+
def get_debug_submission(submission_id: Optional[str] = None) -> Response:
674+
"""Display the current state of the submission."""
675+
return handle(cntrls.submission_status, 'debug/status.html',
676+
'Submission status', submission_id)
677+
678+
654679
@UI.route('/debug/<submission_id>/events', methods=["GET"])
655680
@scoped(scopes.VIEW_SUBMISSION, authorizer=is_admin_or_dev,
656681
unauthorized=redirect_to_login)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{% extends "base/base.html" %}
2+
3+
{% block addl_head %}
4+
{% endblock addl_head %}
5+
6+
{% block content %}
7+
<h1>Debug Mail</h1>
8+
<p class="help">
9+
Email captured in memory (<code>EMAIL_MODE=TESTING</code>). Nothing here was
10+
actually dispatched. Newest first. {{ emails | length }} message(s).
11+
</p>
12+
13+
{% if emails %}
14+
<table class="table is-striped is-fullwidth">
15+
<thead>
16+
<tr>
17+
<th>To</th>
18+
<th>Subject</th>
19+
<th>Headers</th>
20+
<th>Body</th>
21+
</tr>
22+
</thead>
23+
<tbody>
24+
{% for m in emails | reverse %}
25+
<tr>
26+
<td>
27+
<div><b>To:</b> {{ m.to | join(', ') }}</div>
28+
{% if m.cc %}<div><b>Cc:</b> {{ m.cc | join(', ') }}</div>{% endif %}
29+
{% if m.bcc %}<div><b>Bcc:</b> {{ m.bcc | join(', ') }}</div>{% endif %}
30+
<div><b>Reply-To:</b> {{ m.reply_to }}</div>
31+
</td>
32+
<td>{{ m.subject }}</td>
33+
<td>
34+
{% if m.message_id %}<div><b>Message-ID:</b> {{ m.message_id }}</div>{% endif %}
35+
{% if m.references %}<div><b>References:</b> {{ m.references }}</div>{% endif %}
36+
</td>
37+
<td><pre>{{ m.body }}</pre></td>
38+
</tr>
39+
{% endfor %}
40+
</tbody>
41+
</table>
42+
{% else %}
43+
<p>No email has been captured yet.</p>
44+
{% endif %}
45+
{% endblock content %}

0 commit comments

Comments
 (0)