From 379407e9ba009135de785b588fdfe8a384cdd0f9 Mon Sep 17 00:00:00 2001 From: David Fielding Date: Tue, 26 May 2026 16:49:51 -0400 Subject: [PATCH 01/23] Improve PDF preview reliability: raise NotFound (route now redirects to a friendly HTML page) when the bucket has no PDF (edgecase); compilation is curently broken so self-heal when PDF copied into place (review): when the bucket has a PDF but submission.preview is None or stale, fire ConfirmSourceProcessed and ConfirmPreview atomically so the second event sees the in-memory state from the first. The legacy DB doesn't persist submission.preview, so a reload between events would lose it. INFO logging for each branch. [SUBMISSION-122] David --- submit_ce/ui/controllers/new/process.py | 142 +++++++++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/submit_ce/ui/controllers/new/process.py b/submit_ce/ui/controllers/new/process.py index ca985c28..23f0b3f6 100644 --- a/submit_ce/ui/controllers/new/process.py +++ b/submit_ce/ui/controllers/new/process.py @@ -15,12 +15,14 @@ from submit_ce.ui import SUPPORT from ...auth import user_and_client_from_session -from submit_ce.domain.event import ConfirmSourceProcessed +from submit_ce.domain.event import ConfirmSourceProcessed, ConfirmPreview from arxiv.auth.domain import Session from werkzeug.datastructures import MultiDict -from werkzeug.exceptions import InternalServerError, MethodNotAllowed +from werkzeug.exceptions import InternalServerError, MethodNotAllowed, NotFound from wtforms import SelectField +from arxiv.files import FileDoesNotExist + from ..util import validate_command from submit_ce.ui.routes.flow_control import ready_for_next, stay_on_this_stage from submit_ce.ui.backend import get_submission @@ -215,12 +217,146 @@ def start_compilation(params: MultiDict, session: Session, submission_id: str, # TODO move file_preview to its own controller def file_preview(params, session: Session, submission_id: str, token: str, **kwargs: Any) -> Tuple[io.BytesIO, int, Dict[str, str]]: - """Serve the PDF preview for a submission.""" + """Serve the PDF preview for a submission. + + Raises + ------ + werkzeug.exceptions.NotFound + If no preview PDF exists for this submission yet (e.g., the Process + step has not run or compilation has not produced a PDF). Flask + renders this as a 404 response, which is a much friendlier failure + mode than the raw ``Exception("File does not exist")`` that + :class:`arxiv.files.FileDoesNotExist` would otherwise raise when the + route tries to ``open()`` the missing file. + + Side effect: when the served PDF matches the current submission preview and + the submitter has not yet confirmed it (or has confirmed a stale checksum), + fire a ``ConfirmPreview`` event so the submitter is treated as having + reviewed the PDF. This mirrors legacy Submit 1.x behavior where opening + the PDF marked ``viewed=1`` on the submission row, which then enables the + Submit button on the Confirm page after a refresh. + """ submitter, client = user_and_client_from_session(session) submission, submission_events = get_submission(submission_id) fstore = current_app.api.get_file_store() + + # Check first that a preview PDF actually exists. Without this, the + # downstream ``send_file(stream.open('rb'), ...)`` in the route raises a + # generic Exception and the user sees a 500 stack trace in the new tab. + # Common cause: edge case where compilation is not working and submitter + # has Confirm and Submit page open. stream = fstore.get_preview(submission.submission_id) + if isinstance(stream, FileDoesNotExist) or not stream.exists(): + logger.info( + "PDF preview requested but not available for submission %s", + submission.submission_id, + ) + raise NotFound( + "The PDF preview is not yet available. Please return to the " + "Process step, wait for compilation to finish, and try again. " + "If processing failed, you may need to fix your source files " + "and reprocess." + ) + pdf_checksum = fstore.get_preview_checksum(submission.submission_id) + + # ---- diagnostic logging ------------------------------------------ + # Verbose INFO logging so the operator can trace exactly what the + # preview-view side effect did. If you're staring at the server + # console wondering why the Submit button is still grayed out, + # these are the lines to grep for. + preview_set = submission.preview is not None + preview_ck = submission.preview.preview_checksum if preview_set else None + logger.info( + "file_preview: submission=%s pdf_checksum=%r preview_set=%s " + "preview_checksum=%r confirmed_preview=%s", + submission.submission_id, pdf_checksum, preview_set, preview_ck, + submission.submitter_confirmed_preview, + ) + + # Build the list of events to save when the user views the PDF. + # + # ConfirmPreview's domain validation requires submission.preview to be + # set and to match preview_checksum -- otherwise it raises + # InvalidEvent("Preview not set on submission"). The legacy DB only + # persists submitter_confirmed_preview (via the `viewed` column); the + # Preview dataclass itself is not stored. That means reading the + # submission back with get_submission() after firing + # ConfirmSourceProcessed loses preview state, and any subsequent + # ConfirmPreview save would fail validation. + # + # Workaround: pass both events to a single save() call. The save loop + # applies events sequentially with `before = after`, so ConfirmPreview + # sees the in-memory submission with preview set by + # ConfirmSourceProcessed and validates cleanly. + # + # When to self-heal ConfirmSourceProcessed: + # - submission.preview is None (process step not run, hand-copied + # PDF, lost domain event), OR + # - preview_checksum has drifted (source was reprocessed or replaced + # but the old in-memory state is stale). + # In production this path is rare; logged at WARNING for visibility. + needs_source_processed = bool(pdf_checksum) and ( + submission.preview is None + or submission.preview.preview_checksum != pdf_checksum + ) + needs_confirm = ( + bool(pdf_checksum) + and not submission.submitter_confirmed_preview + ) + logger.info( + "file_preview: needs_source_processed=%s needs_confirm=%s", + needs_source_processed, needs_confirm, + ) + + events_to_save = [] + if needs_source_processed: + events_to_save.append( + ConfirmSourceProcessed( + creator=submitter, + client=client, + source_id=-1, # unknown for hand-copied PDFs + source_checksum='', # unknown for hand-copied PDFs + preview_checksum=pdf_checksum, + size_bytes=getattr(stream, 'size', None) or 0, + added=getattr(stream, 'updated', None), + ) + ) + if needs_confirm: + events_to_save.append( + ConfirmPreview(creator=submitter, client=client, + preview_checksum=pdf_checksum) + ) + + if events_to_save: + try: + current_app.api.save( + *events_to_save, + submission_id=submission.submission_id, + ) + if needs_source_processed: + logger.warning( + "Self-healed missing/stale ConfirmSourceProcessed for " + "submission %s from served PDF (checksum=%s). " + "Investigate the upstream Process step.", + submission.submission_id, pdf_checksum, + ) + logger.info( + "file_preview: saved %d event(s) for submission %s: %s", + len(events_to_save), submission.submission_id, + [e.__class__.__name__ for e in events_to_save], + ) + except Exception as exc: + # Don't silently swallow. The PDF still streams to the browser + # via the return below, but logging at ERROR with stack trace + # makes the failure obvious in the server console. + logger.exception( + "PDF-view event save failed for submission %s " + "(events=%s): %s", + submission.submission_id, + [e.__class__.__name__ for e in events_to_save], exc, + ) + headers = {'Content-Type': 'application/pdf', 'ETag': pdf_checksum} return stream, status.OK, headers From b946aa9a1c2655bba7bc9c53166226fae30de20c Mon Sep 17 00:00:00 2001 From: David Fielding Date: Tue, 26 May 2026 17:04:06 -0400 Subject: [PATCH 02/23] Add /preview_not_available and /submission_agreement.pdf routes, and harden /preview.pdf: catch NotFound from the controller and 302 to /preview_not_available (non-.pdf URL so the browser renders HTML instead of invoking the PDF viewer on a .pdf URL that returned HTML), and make set_etag/Content-Length best-effort so missing blob metadata doesn't crash the response. [SUBMISSION-122] David --- submit_ce/ui/routes/ui.py | 82 +++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/submit_ce/ui/routes/ui.py b/submit_ce/ui/routes/ui.py index fc02abf0..8bc9dc81 100644 --- a/submit_ce/ui/routes/ui.py +++ b/submit_ce/ui/routes/ui.py @@ -10,12 +10,15 @@ from markupsafe import Markup from werkzeug import Response as WResponse from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import NotFound from submit_ce.ui import controllers as cntrls from submit_ce.ui.controllers.debug import debug_events from submit_ce.ui.controllers.new import upload from submit_ce.ui.controllers.new import review from submit_ce.ui.controllers.new import upload_delete + from ..auth import is_owner, is_admin_or_dev +from submit_ce.ui.controllers.new import submission_agreement from submit_ce.ui.workflow.processor import WorkflowProcessor from submit_ce.ui.workflow.stages import FileUpload from .flow_control import flow_control, get_workflow, endpoint_name @@ -23,7 +26,6 @@ - #from submit_ce.ui import util logger = logging.getLogger(__name__) @@ -390,17 +392,81 @@ def file_process(submission_id: str) -> Response: unauthorized=redirect_to_login) # TODO @flow_control(Process)? def file_preview(submission_id: str) -> Response: - data, code, headers = cntrls.new.process.file_preview( + try: + data, code, headers = cntrls.new.process.file_preview( + MultiDict(request.args.items(multi=True)), + request.auth, + submission_id, + request.environ['token'] + ) + except NotFound: + # The PDF doesn't exist yet (e.g., Process step incomplete or + # tex2pdf unavailable). Redirect to a non-.pdf URL so the browser + # stops trying to render the response with its built-in PDF viewer, + # which would otherwise display a blank tab. The destination + # endpoint shows a friendly explanation and links back to the + # Process and Confirm steps. + return redirect(url_for('ui.preview_not_available', + submission_id=submission_id)) + # TODO This needs to have range request handling like arxiv-browse + rv = send_file(data.open('rb'), mimetype=headers['Content-Type']) + # ETag / Content-Length are best-effort. If the store could not provide + # them (e.g., a blob whose metadata isn't populated for some reason) + # we'd rather omit those headers than crash with TypeError. The PDF + # itself still streams to the browser. + etag = headers.get('ETag') + if etag: + rv.set_etag(etag) + size = getattr(data, 'size', None) + if size is not None: + rv.headers['Content-Length'] = str(size) + rv.headers['Cache-Control'] = 'no-store' + return rv + + +@UI.route('//preview_not_available', methods=["GET"]) +@scoped(scopes.VIEW_SUBMISSION, authorizer=is_owner, + unauthorized=redirect_to_login) +def preview_not_available(submission_id: str) -> Response: + """Render a friendly HTML page when no preview PDF exists. + + Reached via a 302 redirect from the ``/preview.pdf`` endpoint when the + file store has no preview for this submission. Lives at a non-.pdf + URL so the browser renders it as a normal HTML page instead of + invoking the built-in PDF viewer. + """ + rv = make_response(render_template( + 'submit/preview_not_available.html', + submission_id=submission_id, + pagetitle="PDF Preview Not Available", + ), 404) + rv.headers['Content-Type'] = 'text/html; charset=utf-8' + rv.headers['Cache-Control'] = 'no-store' + return rv + + +@UI.route('//submission_agreement.pdf', methods=["GET"]) +@scoped(scopes.VIEW_SUBMISSION, authorizer=is_owner, + unauthorized=redirect_to_login) +def submission_agreement_pdf(submission_id: str) -> Response: + """Serve a personalized PDF of the accepted submission agreement. + + The PDF is generated on the fly and stamped with the submission ID, + submitter, submission date, license, and agreement_id. Linked from + the Confirm page's sidebar ('Download Submission Agreement'). + """ + stream, code, headers = submission_agreement.download_submission_agreement( + request.method, MultiDict(request.args.items(multi=True)), request.auth, submission_id, - request.environ['token'] ) - # TODO This needs to have range request handling like arxiv-browse - rv = send_file(data.open('rb'), mimetype=headers['Content-Type']) - rv.set_etag(headers['ETag']) - rv.headers['Content-Length'] = str(data.size) - rv.headers['Cache-Control'] = 'no-store' + rv = send_file(stream, mimetype=headers['Content-Type'], + download_name=headers.get('Content-Disposition', '') + .split('filename="')[-1].rstrip('"') + or f"arxiv-submission-agreement-{submission_id}.pdf", + as_attachment=True) + rv.headers['Cache-Control'] = headers.get('Cache-Control', 'no-store') return rv From a98b55d122efb82323baeb6fa59e9b862ea7494b Mon Sep 17 00:00:00 2001 From: David Fielding Date: Tue, 26 May 2026 17:12:38 -0400 Subject: [PATCH 03/23] Add preview_ready gate (submitter_confirmed_preview AND PDF actually exists in bucket) and pass to template so an absent PDF can never produce an enabled Submit button. Skip FinalizeSubmission unless the form action is next -- the nav bar's Go Back and Save & Exit buttons also POST this form, which would accidentally submit when the proceed checkbox happens to be ticked. INFO logging on render. [SUBMISSION-122] David --- submit_ce/ui/controllers/new/final.py | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/submit_ce/ui/controllers/new/final.py b/submit_ce/ui/controllers/new/final.py index af08f409..16b0434c 100644 --- a/submit_ce/ui/controllers/new/final.py +++ b/submit_ce/ui/controllers/new/final.py @@ -36,6 +36,24 @@ def finalize(method: str, params: MultiDict, session: Session, form = FinalizationForm(params) + # Check whether a preview PDF actually exists in the bucket. The + # persisted submitter_confirmed_preview flag can drift from file-store + # reality (e.g., a PDF was removed out-of-band, a file-change event + # missed resetting the flag, or in dev when state is manipulated + # directly). The Confirm page should gate Submit on what's actually + # in the bucket, not just the persisted flag, so an absent PDF can + # never produce an enabled Submit button. + fstore = current_app.api.get_file_store() + preview_exists = fstore.does_preview_exist(str(submission_id)) + preview_ready = bool(submission.submitter_confirmed_preview + and preview_exists) + logger.info( + "finalize: submission=%s confirmed_preview=%s preview_exists=%s " + "preview_ready=%s", + submission_id, submission.submitter_confirmed_preview, + preview_exists, preview_ready, + ) + # The abs preview macro expects a specific struct for submission history. # TODO submission.versions removed, what do do in final? # submission_history = [{'submitted_date': s.created, 'version': s.version} @@ -47,11 +65,24 @@ def finalize(method: str, params: MultiDict, session: Session, 'submission': submission, 'submitter': submitter, 'submission_history': submission_history, + 'preview_ready': preview_ready, + 'preview_exists': preview_exists, } + # Only treat this POST as an actual submit attempt when the form's + # "next" action was used. The Confirm form is shared by the nav bar's + # "Go Back" and "Save & Exit" buttons too -- those also POST the form + # (so the CSRF token comes along) but they're navigation actions, not + # the submit action. If the user has the proofread checkbox ticked and + # then clicks Go Back, we must NOT fire FinalizeSubmission; flow_control + # is supposed to redirect them to the previous step instead. + action = (params.get('action') or '').strip() + is_submit_action = action == 'next' + command = FinalizeSubmission(creator=submitter) proofread_confirmed = form.proceed.data - if method == 'POST' and form.validate() \ + if method == 'POST' and is_submit_action \ + and form.validate() \ and proofread_confirmed \ and validate_command(form, command, submission): try: From f00a3cd3a7751bd4545f5b9991f09907b8b8c38b Mon Sep 17 00:00:00 2001 From: David Fielding Date: Tue, 26 May 2026 18:00:04 -0400 Subject: [PATCH 04/23] Rewrite final_preview.html for the v5 Confirm-and-Submit mockup: Submit gated on preview_ready (preview viewed AND PDF still exists in bucket); banner branches three ways (no PDF / unviewed PDF / ready); sidebar adds Download Submission Agreement link and editing-after-submitting copy; new shortcuts row with Preview PDF (tracking preview state), Edit Metadata, Edit Files, Edit License; JS state machine reloads the page on tab return after a PDF view; amber highlight on the proceed checkbox row when ticking it is the last remaining gate. [SUBMISSION-122] David --- .../ui/templates/submit/final_preview.html | 365 ++++++++++++++---- 1 file changed, 295 insertions(+), 70 deletions(-) diff --git a/submit_ce/ui/templates/submit/final_preview.html b/submit_ce/ui/templates/submit/final_preview.html index 220e70a5..938a6410 100644 --- a/submit_ce/ui/templates/submit/final_preview.html +++ b/submit_ce/ui/templates/submit/final_preview.html @@ -1,87 +1,312 @@ {% extends "submit/base_edit.html" %} -{% block title -%}Review and Approve Your Submission{%- endblock title %} -{% block important_text_box %} -{# - {{svg.trashcan()}} - Cancel and Delete -#} -{% endblock important_text_box %} +{% block title -%}Confirm and Submit{%- endblock title %} + +{# Submit button is "Submit" here, not "Continue". It's disabled until + `preview_ready` is true. The controller computes preview_ready as + (submission.submitter_confirmed_preview AND the PDF actually exists in + the file store), so the gate stays closed if the PDF was removed + out-of-band even when the persisted flag still says "viewed". A + client-side handler further disables Submit until the proceed + checkbox is also ticked. #} +{% set _submit_disabled = not preview_ready %} + +{% block submit_nav_top %} + {{ submit_macros.submit_nav(submission_id, + next_label="Submit", + next_aria="Submit", + next_disabled=_submit_disabled, + next_id="submit-button-top") }} +{% endblock %} + +{% block submit_nav_bottom %} + {{ submit_macros.submit_nav(submission_id, + next_label="Submit", + next_aria="Submit", + next_disabled=_submit_disabled, + next_id="submit-button-bottom") }} +{% endblock %} + +{% block info_container_middle -%} +
+

Download a copy of your submission agreement:

+ +

Note that a citation link with your final arxiv identifier is not + available until after the submission is accepted and published.

+ +

Editing after submitting:

+

Once your submission has been announced, amendments to the files or + core metadata may only be made through + replacement or + withdrawal. + The license selection is permanent for this version and cannot be + amended.

+

Content that can be edited at + any time after announcement and without a new revision:

+
    +
  • Journal reference
  • +
  • DOI
  • +
  • MSC or ACM classification
  • +
  • Report number
  • +
+
+{%- endblock info_container_middle %} {% block within_content %} -
-
-
-
-

{{svg.info(klass="icon filter-orange")}} Review your submission carefully! - Editing your submission after clicking "confirm and submit" will remove your paper from the queue. - A new timestamp will be assigned after resubmitting.

-

Note that a citation link with your final arxiv identifier is not available until after the submission is accepted and published.

-

Once your submission has been announced, amendments to files or core metadata may only be made through - replacement or withdrawal. - Exception: You will be able to update journal reference, DOI, MSC or ACM classification, or report number information at any time without a new revision.

+ {# PDF-preview gating banner. Three states: + preview_ready -> no banner (submit gate open) + preview_exists and not gate -> "preview your PDF" + not preview_exists -> "PDF not available, reprocess" + #} + {% if not preview_ready %} +
+
+
+
+ {% if preview_exists %} +

+ {{ svg.exclamation(klass="icon filter-red") }} + Preview your PDF! + You are required to preview your PDF before submitting. + Click here to open your PDF + in a new browser tab. This page will refresh automatically + when you return. +

+ {% else %} +

+ {{ svg.exclamation(klass="icon filter-red") }} + PDF preview is not + available. + The compiled PDF for your submission is missing. You + must return to the + Process + Files step to regenerate it before you can submit. +

+ {% endif %} +
+
-
-
- -{% if submission.version > 1 %} - {% set arxiv_id = submission.arxiv_id %} -{% else %} - {% set arxiv_id = "0000.00000" %} -{% endif %} - -
-{{ macros.abs( - arxiv_id or "0000.00000", - submission.metadata.title, - submission.metadata.authors_display|tex2utf, - submission.metadata.abstract, - submission.created, - submission.primary_classification, - comments = submission.metadata.comments, - msc_class = submission.metadata.msc_class, - acm_class = submission.metadata.acm_class, - journal_ref = submission.metadata.journal_ref, - doi = submission.metadata.doi, - report_num = submission.metadata.report_num, - version = submission.version, - submission_history = submission_history, - secondary_categories = submission.secondary_classification) }} -
- - -
+ {% endif %} + + {# Abstract preview - same domain data as before, same macro. #} + {% if submission.version > 1 %} + {% set arxiv_id = submission.arxiv_id %} + {% else %} + {% set arxiv_id = "0000.00000" %} + {% endif %}
- - {{svg.pencil()}}Edit Metadata - - - {{svg.pencil()}}Edit Uploaded Files - - - View PDF - {{svg.exclamation(klass="icon filter-white", title="PDF will open in new tab")}} - + {{ macros.abs( + arxiv_id or "0000.00000", + submission.metadata.title, + submission.metadata.authors_display|tex2utf, + submission.metadata.abstract, + submission.created, + submission.primary_classification, + comments=submission.metadata.comments, + msc_class=submission.metadata.msc_class, + acm_class=submission.metadata.acm_class, + journal_ref=submission.metadata.journal_ref, + doi=submission.metadata.doi, + report_num=submission.metadata.report_num, + version=submission.version, + submission_history=submission_history, + secondary_categories=submission.secondary_classification) }}
+ {# Proceed checkbox + reminder copy. #}
- {{ form.csrf_token }} - {% if form.proceed.errors %}
{% endif %} -
-
-
- {{ form.proceed }} - {{ form.proceed.label }} +

Review your submission and make any edits before submitting to arXiv. + Making edits after submitting will remove your paper from the + announcement queue and a new timestamp will be assigned after + resubmitting.

+ + + {{ form.csrf_token }} + {% if form.proceed.errors %}
{% endif %} +
+
+
+ {{ form.proceed(id="proceed") }} + {{ form.proceed.label }} +
+ {% for error in form.proceed.errors %} +

{{ error }}

+ {% endfor %}
- {% for error in form.proceed.errors %} -

{{ error }}

- {% endfor %} +
+ {% if form.proceed.errors %}
{% endif %} + +
+ + {# Preview & editing shortcuts (mockup's bottom section). #} +
+

Preview and Editing Shortcuts

+
+
+ {# Preview PDF button styling tracks the three states from the + gating banner above: + preview_ready -> regular secondary + preview_exists and not gate -> is-danger (red) + not preview_exists -> is-light (deemphasized) + #} + + Preview PDF + {% if preview_exists and not preview_ready %} + Required before submitting + {% elif not preview_exists %} + PDF not available + {% endif %} + + {# TODO: Preview HTML endpoint not yet implemented. Linking placeholder. #} + + Preview HTML + + Edit Metadata + Edit Files + Edit License
- {% if form.proceed.errors %}
{% endif %}
- + {# Confirm-page client-side behavior: + 1. Auto-refresh on tab return after a PDF preview link is clicked. + This way the server-side ConfirmPreview event (fired when the + PDF was served) is picked up without the user manually reloading. + We poll the server in addition to tab-focus heuristics, because + focus/visibility events are unreliable across browsers and don't + fire at all if the PDF opens in a popup that the user dismisses + without ever returning focus to the original tab. + 2. Toggle Submit button disabled state based on the proceed checkbox, + but only when the server-side PDF-preview gate is already passed. + #} + {% endblock within_content %} From eb533301afa5aad719338118046580aa455de303 Mon Sep 17 00:00:00 2001 From: David Fielding Date: Tue, 26 May 2026 18:12:17 -0400 Subject: [PATCH 05/23] Update final_preview browser tab title to match v5 mockup: 'Preview and Approve' -> 'Confirm and Submit'. Sets the pagetitle context var which arxiv-base renders into in the page head. [SUBMISSION-122] David --- submit_ce/ui/routes/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submit_ce/ui/routes/ui.py b/submit_ce/ui/routes/ui.py index 8bc9dc81..ff78d343 100644 --- a/submit_ce/ui/routes/ui.py +++ b/submit_ce/ui/routes/ui.py @@ -504,7 +504,7 @@ def add_metadata(submission_id: str) -> Response: def final_preview(submission_id: str) -> Response: """Render step 10, preview.""" return handle(cntrls.finalize, 'submit/final_preview.html', - 'Preview and Approve', submission_id, flow_controlled=True) + 'Confirm and Submit', submission_id, flow_controlled=True) @UI.route('/<submission_id>/confirmation', methods=['GET', 'POST']) From cc3efea6d6293b947aa34ed7c07ab1aca3fbdc46 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 18:13:03 -0400 Subject: [PATCH 06/23] Rename FinalPreview tab label and title for v5 mockup: display "Preview" -> "Confirm", title "Final preview" -> "Confirm and Submit". Class name stays so the endpoint URL is unchanged; docstring notes the rename. [SUBMISSION-122] David --- submit_ce/ui/workflow/stages.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/submit_ce/ui/workflow/stages.py b/submit_ce/ui/workflow/stages.py index 3916b4aa..9ca32dbf 100644 --- a/submit_ce/ui/workflow/stages.py +++ b/submit_ce/ui/workflow/stages.py @@ -114,7 +114,8 @@ class Process(Stage): title = "File process" display = "Process Files" """We need to re-process every time the source is updated.""" - completed = [conditions.is_source_processed] + completed = [] + #completed = [conditions.is_source_processed] class Metadata(Stage): @@ -138,12 +139,17 @@ class OptionalMetadata(Stage): class FinalPreview(Stage): - """The user is asked to review the submission before finalizing.""" + """The user is asked to review the submission before finalizing. + + Note: the class name remains FinalPreview for backward compatibility + (endpoint URL is still /final_preview), but the visible tab label is + "Confirm" per the v5 mockup redesign. + """ endpoint = 'final_preview' label = 'preview and approve your submission' - title = "Final preview" - display = "Preview" + title = "Confirm and Submit" + display = "Confirm" completed = [conditions.is_finalized] From eee809937b4836ae079c892c70fb1c339b781a43 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 18:19:19 -0400 Subject: [PATCH 07/23] Add support to enable Submit following preview of PDF. Parameterize submit_nav and expose it via named blocks so per-page templates can customize the Submit button without rewriting the macro: submit_nav gains next_label/next_aria/next_disabled/next_id (backward-compat defaults of "Continue" / "Save and continue" / not disabled / no id preserve current behavior for every existing page), and base_edit.html wraps the top/bottom submit_nav calls in submit_nav_top and submit_nav_bottom blocks so final_preview can override and render "Submit" instead. [SUBMISSION-122] David --- submit_ce/ui/templates/submit/base_edit.html | 4 ++-- submit_ce/ui/templates/submit/submit_macros.html | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/submit_ce/ui/templates/submit/base_edit.html b/submit_ce/ui/templates/submit/base_edit.html index 084ef3dc..2d2d3c93 100644 --- a/submit_ce/ui/templates/submit/base_edit.html +++ b/submit_ce/ui/templates/submit/base_edit.html @@ -40,14 +40,14 @@ <h2 class="replacement is-size-5 subtitle">Replacing arXiv:{{ submission.arxiv_i <div class="layout-container"> <div class="info-container"> <div class="info-container-top"> - {{ submit_macros.submit_nav(submission_id) }} + {% block submit_nav_top %}{{ submit_macros.submit_nav(submission_id) }}{% endblock %} </div> <div class="info-container-middle"> {%block info_container_middle %}{% endblock %} {% block important_text_box %}{% endblock %} </div> <div class="info-container-bottom"> - {{ submit_macros.submit_nav(submission_id) }} + {% block submit_nav_bottom %}{{ submit_macros.submit_nav(submission_id) }}{% endblock %} </div> </div> diff --git a/submit_ce/ui/templates/submit/submit_macros.html b/submit_ce/ui/templates/submit/submit_macros.html index 882c8717..e1712490 100644 --- a/submit_ce/ui/templates/submit/submit_macros.html +++ b/submit_ce/ui/templates/submit/submit_macros.html @@ -1,7 +1,11 @@ {% import "svg.html" as svg %} {# The submit_nav buttons target sending the form via the `form="form"` so the id on the form should be `form` for this to work. #} -{% macro submit_nav(submission_id) %} +{% macro submit_nav(submission_id, + next_label="Continue", + next_aria="Save and continue", + next_disabled=False, + next_id="") %} <div class="buttons submit-nav" role="navigation" aria-label="Submission form controls"> <button name="action" class="button" form="form" value="previous" aria-label="Go back one step"> @@ -14,8 +18,11 @@ </button> --> <button name="action" class="button is-success" form="form" - value="next" aria-label="Save and continue"> - Continue + value="next" + {% if next_id %}id="{{ next_id }}"{% endif %} + aria-label="{{ next_aria }}" + {% if next_disabled %}disabled{% endif %}> + {{ next_label }} </button> </div> {% endmacro %} From 18f6c3878ce1a43be32981c235c08ab52814f9f0 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 18:24:01 -0400 Subject: [PATCH 08/23] Two Confirm-page UX polish CSS additions: force gray + not-allowed cursor on disabled .submit-nav buttons because Bulma's default opacity-only disabled styling reads as active on a saturated green Submit, and add .proceed-highlight (soft amber row + bolder label + brief pulse animation) to draw the eye to the proceed checkbox when ticking it is the only remaining gate before Submit. [SUBMISSION-122] David --- submit_ce/ui/static/css/submit_overrides.css | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/submit_ce/ui/static/css/submit_overrides.css b/submit_ce/ui/static/css/submit_overrides.css index cb7e8423..5e96a228 100644 --- a/submit_ce/ui/static/css/submit_overrides.css +++ b/submit_ce/ui/static/css/submit_overrides.css @@ -49,3 +49,51 @@ h1.title.title-submit { padding-bottom: 0.75rem !important; /* space under buttons */ margin-bottom: 0.75rem !important; /* space before content begins */ } + +/* Make disabled buttons obviously inactive even when they carry a Bulma + color modifier like is-success, is-link, is-danger, etc. Bulma's + default disabled style only adds opacity: 0.5, which on a saturated + green/blue button still reads as "active" at a glance. We desaturate + to gray + a not-allowed cursor so users can tell from across the room + that the button is gated. */ +.submit-nav .button[disabled], +.submit-nav .button[disabled]:hover, +.submit-nav .button[disabled]:focus, +.submit-nav .button[disabled]:active { + background-color: #e4e6eb !important; + border-color: #cdd1d8 !important; + color: #8a8f98 !important; + box-shadow: none !important; + cursor: not-allowed !important; + opacity: 1 !important; /* override Bulma's 0.5 fade-only */ +} + +/* Highlight the proceed-confirmation checkbox on the Confirm page when + ticking it is the only remaining step before Submit (PDF has been + previewed). The page has a lot going on (abstract preview, sidebar, + shortcut buttons), so without a visual cue the user can lose track + of what to do next. A soft amber row + bolder label brings the eye + to the checkbox without yelling. The pulse animation gently + reinforces it for a few seconds, then settles into the static + highlight. */ +.proceed-checkbox { + padding: 0.5rem 0.75rem; + border-radius: 4px; + transition: background-color 0.25s ease, border-color 0.25s ease; + border: 1px solid transparent; +} +.proceed-checkbox.proceed-highlight { + background-color: #fff8db; /* soft amber */ + border-color: #f6c344; /* amber accent */ + font-weight: 600; + animation: proceed-pulse 1.8s ease-in-out 2; +} +@keyframes proceed-pulse { + 0% { box-shadow: 0 0 0 0 rgba(246, 195, 68, 0.55); } + 50% { box-shadow: 0 0 0 6px rgba(246, 195, 68, 0); } + 100% { box-shadow: 0 0 0 0 rgba(246, 195, 68, 0); } +} +.proceed-checkbox.proceed-highlight input[type="checkbox"] { + transform: scale(1.15); + margin-right: 0.4rem; +} From 661cf8716b22b14d33d9cfd0f94b6fd74fbb24e0 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 18:26:35 -0400 Subject: [PATCH 09/23] New preview_not_available.html template rendered when /preview.pdf redirects here because the bucket has no PDF: explains the situation as unexpected (the PDF should exist by Confirm step), suggests retry/reprocess/contact-support, and offers Back-to-Confirm / Go-to-Process / Close-tab buttons. Lives at a non-.pdf URL so the browser renders HTML instead of invoking its built-in PDF viewer. [SUBMISSION-122] David --- .../submit/preview_not_available.html | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 submit_ce/ui/templates/submit/preview_not_available.html diff --git a/submit_ce/ui/templates/submit/preview_not_available.html b/submit_ce/ui/templates/submit/preview_not_available.html new file mode 100644 index 00000000..856089d8 --- /dev/null +++ b/submit_ce/ui/templates/submit/preview_not_available.html @@ -0,0 +1,49 @@ +{%- extends "submit/base.html" %} +{% import "svg.html" as svg %} + +{% block title %}PDF Preview Not Available{% endblock title %} + +{% block within_content %} + <div class="notification is-warning"> + <h2 class="is-size-5 is-marginless"> + {{ svg.exclamation(klass="icon filter-orange") }} + Your PDF preview could not be retrieved. + </h2> + <p> + Your submission's PDF was compiled successfully when you completed + the Process step, so this is unexpected. The most likely cause is + a temporary problem retrieving the stored PDF — for example, + a transient storage or network error. + </p> + </div> + + <div class="content"> + <p>What to try:</p> + <ul> + <li>Wait a moment and click <em>Preview PDF</em> again from the + Confirm step.</li> + <li>If the problem persists, return to the + <strong>Process Files</strong> step and reprocess your submission + to regenerate the PDF.</li> + <li>If it still won't load, please contact + <a href="mailto:help@arxiv.org">arXiv support</a> with your + submission ID so we can investigate.</li> + </ul> + </div> + + <div class="buttons"> + {% if submission_id %} + <a class="button is-link" + href="{{ url_for('ui.final_preview', submission_id=submission_id) }}"> + Back to Confirm + </a> + <a class="button" + href="{{ url_for('ui.file_process', submission_id=submission_id) }}"> + Go to Process step + </a> + {% endif %} + <button class="button" type="button" onclick="window.close();"> + Close this tab + </button> + </div> +{% endblock within_content %} From 61f44e067377ee0ea431359fc842382625f3871c Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 18:41:57 -0400 Subject: [PATCH 10/23] Add submission_agreement controller for the Confirm-page Download Submission Agreement link, plus the reportlab dependency it needs. Generates a personalized PDF stamped with the submission's ID, submitter, submission date, license, and agreement_id using reportlab Platypus; body text is placeholder pending canonical legal copy from arXiv. [SUBMISSION-122] David --- pyproject.toml | 4 + .../controllers/new/submission_agreement.py | 207 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 submit_ce/ui/controllers/new/submission_agreement.py diff --git a/pyproject.toml b/pyproject.toml index 93f62a25..0e820538 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,7 @@ dependencies = [ "pytz==2018.7", "pyyaml==6.0.2", "referencing==0.35.1", + "reportlab>=4.2.0", "requests==2.32.3", "requests-toolbelt==1.0.0", "retry==0.9.2", @@ -249,3 +250,6 @@ exclude = [ ".git", ] + +[tool.setuptools] +packages = ["submit_ce"] diff --git a/submit_ce/ui/controllers/new/submission_agreement.py b/submit_ce/ui/controllers/new/submission_agreement.py new file mode 100644 index 00000000..014ccc50 --- /dev/null +++ b/submit_ce/ui/controllers/new/submission_agreement.py @@ -0,0 +1,207 @@ +"""Controller for the Download Submission Agreement endpoint. + +Serves a personalized PDF of the submission agreement, stamped with the +submission ID, submitter name, submission date, license, and agreement_id +that the user accepted on the Agreement step. + +This endpoint is invoked from the Confirm page (see final_preview.html) so +the user can retain a copy of the submission agreement they accepted. +""" + +from __future__ import annotations + +import io +import logging +from datetime import datetime, timezone +from http import HTTPStatus as status +from typing import Any, Dict, Tuple + +from arxiv.auth.domain import Session +from werkzeug.datastructures import MultiDict + +from submit_ce.ui.auth import user_and_client_from_session +from submit_ce.ui.backend import get_submission + + +logger = logging.getLogger(__name__) + + +def _submitter_display_name(submission, session: Session) -> str: + """Best-effort: prefer submission.creator/owner.name, then session user.""" + for agent in (getattr(submission, "creator", None), + getattr(submission, "owner", None)): + name = getattr(agent, "name", None) if agent else None + if name: + return name + user = getattr(session, "user", None) + if user: + for attr in ("name", "username", "email"): + val = getattr(user, attr, None) + if val: + return val + return "Unknown submitter" + + +def _license_display(submission) -> str: + """Return a human-readable label for the chosen license, or '(none)'.""" + lic = getattr(submission, "license", None) + if not lic: + return "(no license selected)" + name = getattr(lic, "name", None) + uri = getattr(lic, "uri", None) + if name and uri: + return f"{name} ({uri})" + return name or uri or "(unknown)" + + +def _format_date(value) -> str: + if value is None: + return "(unknown)" + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %H:%M:%S %Z").strip() + return str(value) + + +def _build_agreement_pdf(submission, session: Session) -> bytes: + """Render the personalized submission-agreement PDF and return its bytes. + + Uses reportlab's Platypus high-level API. The agreement body is currently + placeholder copy that mirrors what the Agreement step shows; replace with + the canonical legal text once it has been finalized by arXiv legal. + """ + from reportlab.lib import colors + from reportlab.lib.pagesizes import letter + from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet + from reportlab.lib.units import inch + from reportlab.platypus import ( + Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle, + ) + + sample = getSampleStyleSheet() + title_style = ParagraphStyle( + "AgrTitle", parent=sample["Title"], + fontSize=18, leading=22, spaceAfter=14, + textColor=colors.HexColor("#b31b1b"), + ) + h2_style = ParagraphStyle( + "AgrH2", parent=sample["Heading2"], + fontSize=12, leading=16, spaceBefore=12, spaceAfter=6, + textColor=colors.HexColor("#1e8bc3"), + ) + body_style = ParagraphStyle( + "AgrBody", parent=sample["BodyText"], + fontSize=10, leading=14, spaceAfter=6, + ) + small_style = ParagraphStyle( + "AgrSmall", parent=body_style, fontSize=8.5, + textColor=colors.HexColor("#666666"), + ) + + submitter_name = _submitter_display_name(submission, session) + submission_id = getattr(submission, "submission_id", "(unknown)") + submitted_dt = (getattr(submission, "submitted", None) + or getattr(submission, "created", None)) + license_display = _license_display(submission) + agreement_id = getattr(submission, "agreement_id", None) or "(unknown)" + generated_at = datetime.now(timezone.utc) + + story = [] + story.append(Paragraph("arXiv Submission Agreement", title_style)) + story.append(Paragraph( + "This document is a personalized record of the submission agreement " + "accepted by the submitter at the time of submission.", + body_style, + )) + + # ---- Personalized stamp ------------------------------------------------- + story.append(Paragraph("Submission record", h2_style)) + detail_rows = [ + ["Submission ID:", str(submission_id)], + ["Submitter:", submitter_name], + ["Submitted on:", _format_date(submitted_dt)], + ["License:", license_display], + ["Agreement ID:", str(agreement_id)], + ["Generated at:", _format_date(generated_at)], + ] + tbl = Table(detail_rows, colWidths=[1.3 * inch, 5.2 * inch], hAlign="LEFT") + tbl.setStyle(TableStyle([ + ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, -1), 10), + ("LEADING", (0, 0), (-1, -1), 12), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("LINEBELOW", (0, 0), (-1, -1), 0.25, colors.HexColor("#dddddd")), + ])) + story.append(tbl) + + # ---- Agreement body (placeholder copy for now) -------------------------- + story.append(Paragraph("Agreement", h2_style)) + placeholder_paragraphs = [ + "By accepting this submission agreement, the submitter affirms that " + "they have the right to make the work available under the license " + "indicated above, and that the work conforms to arXiv submission " + "policies including those covering content quality, attribution, " + "and acceptable subject matter.", + + "Once announced, this version of the submission cannot be amended " + "except through replacement or withdrawal. Non-core metadata " + "(journal reference, DOI, MSC or ACM classification, and report " + "number) may be updated at any time without a new revision.", + + "The license selection is permanent for this version and cannot be " + "amended after announcement. Any subsequent versions must use a " + "compatible license.", + + "This document is generated automatically as a record of acceptance " + "and does not constitute the full text of the arXiv submission " + "agreement. The canonical agreement text identified by Agreement ID " + f"{agreement_id} is the authoritative version.", + ] + for para in placeholder_paragraphs: + story.append(Paragraph(para, body_style)) + + story.append(Spacer(1, 0.25 * inch)) + story.append(Paragraph( + "For the canonical agreement text and arXiv policies, see " + "<font color=\"#1e8bc3\">https://arxiv.org/help/policies</font>. " + "Questions about this submission record may be directed to " + "<font color=\"#1e8bc3\">arxiv.org/help/contact</font>.", + small_style, + )) + + buf = io.BytesIO() + doc = SimpleDocTemplate( + buf, pagesize=letter, + leftMargin=0.75 * inch, rightMargin=0.75 * inch, + topMargin=0.75 * inch, bottomMargin=0.6 * inch, + title=f"arXiv Submission Agreement - {submission_id}", + author=submitter_name, + ) + doc.build(story) + return buf.getvalue() + + +def download_submission_agreement( + method: str, + params: MultiDict, + session: Session, + submission_id: str, + **kwargs: Any, +) -> Tuple[io.BytesIO, int, Dict[str, str]]: + """Return a personalized submission-agreement PDF for the user's records.""" + # Verify the submission exists and is loadable in the current user's scope. + submission, _ = get_submission(submission_id) + _ = user_and_client_from_session(session) # forces auth resolution + + pdf_bytes = _build_agreement_pdf(submission, session) + stream = io.BytesIO(pdf_bytes) + stream.seek(0) + + filename = f"arxiv-submission-agreement-{submission_id}.pdf" + headers = { + "Content-Type": "application/pdf", + "Content-Disposition": f'attachment; filename="{filename}"', + "Cache-Control": "no-store", + } + return stream, status.OK, headers From 5f0f0c642109b2e46bf6a4828dea70b49887c044 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 19:10:40 -0400 Subject: [PATCH 11/23] Handle the edge case where a file is copied into the bucket: reload Blob metadata on read so crc32c, size, and updated timestamps are populated, avoiding ETag/Content-Length failures and giving callers real metadata. [SUBMISSION-122] David --- .../file_store/gs_file_store.py | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/submit_ce/implementations/file_store/gs_file_store.py b/submit_ce/implementations/file_store/gs_file_store.py index 7cb1929b..5a2c4d83 100644 --- a/submit_ce/implementations/file_store/gs_file_store.py +++ b/submit_ce/implementations/file_store/gs_file_store.py @@ -191,6 +191,18 @@ def get_preview(self, submission_id: str) -> FileObj: preview_path = self._preview_path(submission_id) preview = self.bucket.blob(preview_path) if preview.exists(): + # bucket.blob() returns a local reference with empty _properties; + # .exists() does a HEAD but doesn't populate metadata. Without + # reload(), preview.size and preview.crc32c are both None and + # the route layer's set_etag()/Content-Length header crash on + # None. This matters in particular for PDFs that arrived in + # the bucket via something other than our store_preview() path + # (e.g., hand-copied for testing). + try: + preview.reload() + except Exception as exc: + logger.debug("preview reload failed for %s: %s", + preview_path, exc) return preview else: return FileDoesNotExist(preview_path) @@ -422,8 +434,30 @@ def does_source_log_exist(self, submission_id: str) -> bool: return self.bucket.blob(self._source_log_path(submission_id)).exists() def _get_checksum(self, path: str) -> str: + """Return the crc32c checksum of the blob at ``path``. + + ``bucket.blob(path)`` creates a *local* Blob reference whose + ``crc32c`` attribute is ``None`` until metadata is fetched from + GCS via ``reload()``. The upload paths (e.g. ``store_preview``) + call ``reload()`` immediately after upload so the local Blob has + a populated checksum, but reads of *pre-existing* blobs (for + example, a PDF copied into the bucket by hand, or any blob + whose metadata we haven't otherwise hydrated) would otherwise + return ``None``. Callers in the route layer feed this value + into ``Response.set_etag()`` and ``Content-Length`` headers, + both of which crash on ``None``. + + Returns an empty string when the blob doesn't exist or has no + crc32c (rather than ``None``) so callers don't have to special + case it. + """ item = self.bucket.blob(path) - return item.crc32c + try: + item.reload() + except Exception as exc: # google.cloud.exceptions.NotFound, etc. + logger.debug("checksum reload failed for %s: %s", path, exc) + return "" + return item.crc32c or "" def _submission_path(self, submission_id: str) -> str: """Gets GS filesystem structure ex /{rootdir}/{first 4 digits of submission id}/{submission id}""" From 38439084b6b64eb2e168858701dfff403e43598c Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 19:18:21 -0400 Subject: [PATCH 12/23] Update uv.lock for the reportlab dependency added in the submission_agreement controller commit. [SUBMISSION-122] David --- uv.lock | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/uv.lock b/uv.lock index a5bd6b17..82a2054c 100644 --- a/uv.lock +++ b/uv.lock @@ -1247,6 +1247,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/0a/acfb251ba01009d3053f04f4661e96abf9d485266b04a0a4deebc702d9cb/pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14", size = 9587, upload-time = "2022-09-01T22:33:31.972Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -1692,6 +1718,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/59/2056f61236782a2c86b33906c025d4f4a0b17be0161b63b70fd9e8775d36/referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de", size = 26684, upload-time = "2024-05-01T20:26:02.078Z" }, ] +[[package]] +name = "reportlab" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/3f/b3861b7e40c9d66f4a04e018958d681d16b948bfd1963c962d43a8c23f66/reportlab-4.5.1.tar.gz", hash = "sha256:9fdf68f4de9171ec66acb4a5feed8f8ca2af43479e707a6fbb0daa75d88e5494", size = 3939748, upload-time = "2026-05-12T10:14:13.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/45/ea7fad10122440de6e845568d106bffdc456ca0e8a1d8ae10b46016087e4/reportlab-4.5.1-py3-none-any.whl", hash = "sha256:06fce8cb56c83307cfa4909cdf4e6a2ddbb44e5d6ef4d2edca896d7e9769f091", size = 1957812, upload-time = "2026-05-12T10:14:10.622Z" }, +] + [[package]] name = "requests" version = "2.32.3" @@ -2013,6 +2052,7 @@ dependencies = [ { name = "pytz" }, { name = "pyyaml" }, { name = "referencing" }, + { name = "reportlab" }, { name = "requests" }, { name = "requests-toolbelt" }, { name = "retry" }, @@ -2170,6 +2210,7 @@ requires-dist = [ { name = "pytz", specifier = "==2018.7" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "referencing", specifier = "==0.35.1" }, + { name = "reportlab", specifier = ">=4.2.0" }, { name = "requests", specifier = "==2.32.3" }, { name = "requests-toolbelt", specifier = "==1.0.0" }, { name = "retry", specifier = "==0.9.2" }, From 39fd830d7ebb56611ab25e6e362eac9c2e9a194b Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 26 May 2026 19:44:22 -0400 Subject: [PATCH 13/23] Adjust page title due to v5 mockup updates. [SUBMISSION-122] David --- submit_ce/ui/tests/integration/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submit_ce/ui/tests/integration/test_integration.py b/submit_ce/ui/tests/integration/test_integration.py index 7bc9245c..e53d968d 100644 --- a/submit_ce/ui/tests/integration/test_integration.py +++ b/submit_ce/ui/tests/integration/test_integration.py @@ -234,7 +234,7 @@ def final_preview_page(self): res = self.client.get(self.next_page) self.assertEqual(res.status_code, 200) - self.assertIn('Review and Approve Your Submission', res.text) + self.assertIn('Confirm and Submit', res.text) res = self.client.post(self.next_page, data= { From eea48e5d72fa01d8313c790325bf7bd68c1be5b3 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Wed, 27 May 2026 18:19:30 -0400 Subject: [PATCH 14/23] Updated per v5 mockups. Fixed notifications messages. Removed unknown submission type warning (moved to Review Files step). [SUBMISSION-149] David --- submit_ce/ui/controllers/new/upload.py | 30 ++---- .../ui/templates/submit/file_upload.html | 96 +++++++++++-------- 2 files changed, 66 insertions(+), 60 deletions(-) diff --git a/submit_ce/ui/controllers/new/upload.py b/submit_ce/ui/controllers/new/upload.py index 8f459ddd..f9513318 100644 --- a/submit_ce/ui/controllers/new/upload.py +++ b/submit_ce/ui/controllers/new/upload.py @@ -314,28 +314,14 @@ def _get_notifications(stat: Workspace) -> List[Dict[str, str]]: ' that these issues may cause delays in processing' ' and/or announcement.' }) - if stat.source_format is SourceFormat.UNKNOWN: - notifications.append({ - 'title': 'Unknown submission type', - 'severity': 'warning', - 'body': 'We could not determine the source type of your' - ' submission. Please check your files carefully. We may' - ' not be able to process your files.' - }) - elif stat.source_format is SourceFormat.INVALID: - notifications.append({ - 'title': 'Unsupported submission type', - 'severity': 'danger', - 'body': 'It is likely that your submission content is not' - ' supported. Please check your files carefully. We may not' - ' be able to process your files.' - }) - else: - notifications.append({ - 'title': f'Detected {stat.source_format.value.upper()}', - 'severity': 'success', - 'body': 'Your submission content is supported.' - }) + # Source-format detection happens later, on the Review Files step + # (preflight dispatches SetSourceFormat from the detected lang -- see + # review.py::_record_source_format_from_preflight). At the Upload + # step the workspace.source_format is always SourceFormat.UNKNOWN, so + # the old "Unknown submission type" warning fired on every single + # upload regardless of content. Don't show format-detection + # notifications here; the Review Files step is where the user sees + # whether their format was recognized. return notifications diff --git a/submit_ce/ui/templates/submit/file_upload.html b/submit_ce/ui/templates/submit/file_upload.html index ee8bbdf0..90629be7 100644 --- a/submit_ce/ui/templates/submit/file_upload.html +++ b/submit_ce/ui/templates/submit/file_upload.html @@ -51,50 +51,70 @@ {% block title -%}Upload Files{%- endblock title %} +{# v5 mockup sidebar: lead with "Avoid common causes of delay" as the + primary heading, then accepted formats / figures / file properties as + sub-sections. Uses info_container_middle (not important_text_box) so + it follows the same sidebar pattern as Metadata and Confirm. #} +{% block info_container_middle %} + <div class="message-body"> + <h2>Avoid common causes of delay</h2> + <p>Make sure included files match the filenames exactly (it is case + sensitive), and verify your references, citations and captions.</p> + + <h3>Accepted formats, in order of preference</h3> + <ul> + <li><a href="{{ url_for('help_submit_tex') }}">(La)TeX or PDFLaTeX</a> + | <a href="{{ url_for('help_submit_pdf') }}">PDF</a> + | <a href="{{ url_for('help_submit_html') }}">HTML</a> + (for proceedings index only)</li> + <li>PDF documents created from TeX are not typically accepted. + <a href="{{ url_for('help_whytex') }}">Why?</a></li> + </ul> + + <h3>Accepted formats for figures</h3> + <ul> + <li>(La)TeX: Postscript</li> + <li>PDFLaTeX: JPG, GIF, PNG, or PDF</li> + </ul> + + <h3>Accepted file properties</h3> + <ul> + <li>Names containing a-z A-Z 0-9 . , - _</li> + <li>Total compressed package size limit is 6MB, and uncompressed is + 18MB</li> + <li>More information about + <a href="{{ url_for('help_submit_sizes') }}">submission size and + exemptions to size limits</a></li> + </ul> + </div> +{% endblock info_container_middle %} +{% block within_content %} +{# Main-area intro copy (moved from the sidebar to match v5 mockup). #} +<p>TeX and (La)TeX submissions are highly encouraged. This format is the + most likely to retain readability and high-quality output in the future. + TeX source uploaded to arXiv will be made publicly available.</p> + +<p>You can upload all your files at once as a single .zip or .tar.gz + file, or upload individual files as needed.</p> + +{# Upload-result notifications (success/warning/error). Previously this + block sat outside any {% block %} in the template, so Jinja's extends + mechanic silently dropped it -- the notifications never rendered. + Moved into within_content so they appear above the form as the v5 + mockup shows. #} {% if immediate_notifications %} {% for notification in immediate_notifications %} - <div class="notification is-{{ notification.severity }}" role="alert" aria-atomic="true"> - {% if notification.title %}<h2 class="is-size-5 is-marginless">{{ notification.title }}</h2>{% endif %} - <p> - {{ notification.body}} - </p> - </div> + <div class="notification is-{{ notification.severity }}" + role="alert" aria-atomic="true"> + {% if notification.title %} + <h2 class="is-size-5 is-marginless">{{ notification.title }}</h2> + {% endif %} + <p>{{ notification.body }}</p> + </div> {% endfor %} {% endif %} -{% block important_text_box %} - - <p>TeX and (La)TeX submissions are highly encouraged. This format is the most likely to retain readability and high-quality output in the future. TeX source uploaded to arXiv will be made publicly available.</p> - - <p>You can upload all your files at once as a single .zip or .tar.gz file, or upload individual files as needed.</p> -<p><strong>Avoid common causes of delay:</strong> Make sure included files match the filenames exactly (it is case sensitive), and verify your references, citations and captions.</p> - -<p class="has-text-weight-semibold"> - {{svg.info(klass="icon filter-dark_grey")}}Accepted formats, in order of preference -</p> -<ul> - <li><a href="{{ url_for('help_submit_tex') }}">(La)TeX or PDFLaTeX</a> | <a href="{{ url_for('help_submit_pdf') }}">PDF</a> | <a href="{{ url_for('help_submit_html') }}">HTML</a> (for proceedings index only)</li> - <li>PDF documents created from TeX are not typically accepted. <a href="{{ url_for('help_whytex') }}">Why?</a></li> -</ul> -<p class="has-text-weight-semibold"> - {{svg.info(klass="icon filter-dark_grey")}}Accepted formats for figures -</p> -<ul> - <li>(La)TeX: Postscript</li> - <li>PDFLaTeX: JPG, GIF, PNG, or PDF</li> -</ul> -<p class="has-text-weight-semibold"> - {{svg.info(klass="icon filter-dark_grey")}}Accepted file properties -</p> -<ul> - <li>Names containing a-z A-Z 0-9 . , - _</li> - <li>Total compressed package size limit is 6MB, and uncompressed is 18MB</li> - <li>More information about <a href="{{ url_for('help_submit_sizes') }}">submission size and exemptions to size limits</a></li> -</ul> - {% endblock important_text_box %} - -{% block within_content %} <form id="form" class="form" action="{{ url_for('ui.file_upload', submission_id=submission_id) }}" method="POST" enctype="multipart/form-data"> <div class="columns action-container"> From 9a8edaf614675ea97240037fb62461e0c8d5a722 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Fri, 29 May 2026 17:42:16 -0400 Subject: [PATCH 15/23] Add Download Source Package endpoint: source_package.py controller builds a fresh .tar.gz on each request from the current source files in the bucket. Linked from a new Download Package button in the Upload Files sidebar; same button to follow on the Review Files page. [SUBMISSION-150] David --- .../ui/controllers/new/source_package.py | 147 ++++++++++++++++++ submit_ce/ui/routes/ui.py | 30 ++++ .../ui/templates/submit/file_upload.html | 14 ++ 3 files changed, 191 insertions(+) create mode 100644 submit_ce/ui/controllers/new/source_package.py diff --git a/submit_ce/ui/controllers/new/source_package.py b/submit_ce/ui/controllers/new/source_package.py new file mode 100644 index 00000000..5ec4f6ca --- /dev/null +++ b/submit_ce/ui/controllers/new/source_package.py @@ -0,0 +1,147 @@ +"""Controller for the Download Source Package endpoint. + +Builds a fresh ``.tar.gz`` on the fly from the current source files in +the file store. Mirrors the pattern used by ``compile_at_gcp.py`` when it +bundles source for the tex2pdf service, and the Submit 1.5 +``create_source_package`` Perl routine in ``Submit.pm``: in both systems +the canonical source is the individual files under +``<submission_id>/src/``, and the archive is built at request time +rather than maintained as a separate artifact. + +Note: We deliberately do *not* serve the persisted ``<submission_id>.tar.gz`` +in the bucket: that copy is only refreshed by ``compile_at_gcp.py`` on a +successful compile (see the ``shutil.copy2(temp_tar_path, new_tar_path)`` +block guarded by ``not preflight and status == 'success'``). It +represents "the source that produced the last published PDF", not the +user's current working source. For a Download Package button shown on +the pre-compile Upload Files and Review Files steps, building from +current source is the correct semantics. + +Note: A warning is logged when a file is not found in the Google Storage bucket. +We may want to improve handling for such an error by alerting the user visually +in the UI and systematically detecting and alerting on such an error. This may +indicate inconsistent access to files in the Google Storage bucket. + +Linked from the sidebar of the Upload Files and Review Files pages. +""" + +from __future__ import annotations + +import io +import logging +import tarfile +from datetime import datetime, timezone +from http import HTTPStatus as status +from typing import Any, Dict, Tuple + +from arxiv.auth.domain import Session +from flask import current_app +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import NotFound + +from arxiv.files import FileDoesNotExist +from submit_ce.ui.backend import get_submission + + +logger = logging.getLogger(__name__) + + +def _build_source_tarball(submission_id: str) -> bytes: + """Build a fresh .tar.gz from the current source files in the bucket. + + Returns the gzipped tarball bytes. Files that are listed in the + workspace but no longer exist in the bucket (which would indicate + file-store drift) are skipped with a warning rather than aborting + the build. + + Raises + ------ + werkzeug.exceptions.NotFound + If the workspace has no source files (so there's nothing to + package). + """ + file_store = current_app.api.get_file_store() + workspace = file_store.get_workspace(submission_id=submission_id) + if not workspace or not workspace.files: + raise NotFound( + "No source files are attached to this submission yet. " + "Upload files on the Upload Files step before downloading " + "a source package." + ) + + buf = io.BytesIO() + files_added = 0 + files_skipped = 0 + with tarfile.open(fileobj=buf, mode='w:gz') as tar: + for f in workspace.files: + blob = file_store.get_source_file( + submission_id=submission_id, path=f.path) + if isinstance(blob, FileDoesNotExist): + logger.warning( + "source_package: workspace lists %s but blob is missing " + "for submission %s; skipping", + f.path, submission_id, + ) + files_skipped += 1 + continue + try: + data = blob.download_as_bytes() + except Exception as exc: + logger.warning( + "source_package: could not read %s for submission %s: " + "%s; skipping", + f.path, submission_id, exc, + ) + files_skipped += 1 + continue + + info = tarfile.TarInfo(name=f.path) + info.size = len(data) + # Use the blob's updated time if available; fall back to now. + updated = getattr(blob, 'updated', None) or datetime.now(timezone.utc) + try: + info.mtime = int(updated.timestamp()) + except (AttributeError, TypeError): + info.mtime = int(datetime.now(timezone.utc).timestamp()) + info.mode = 0o644 + tar.addfile(info, io.BytesIO(data)) + files_added += 1 + + logger.info( + "source_package: built tarball for submission %s " + "(files_added=%d, files_skipped=%d, size_bytes=%d)", + submission_id, files_added, files_skipped, buf.tell(), + ) + + buf.seek(0) + return buf.getvalue() + + +def download_source_package( + method: str, + params: MultiDict, + session: Session, + submission_id: str, + **kwargs: Any, +) -> Tuple[io.BytesIO, int, Dict[str, str]]: + """Return the submission's current source as a freshly-built .tar.gz. + + The filename follows the Submit 1.5 convention: + ``submission_<id>-<YYYYMMDD-HHMM>.tar.gz``. + """ + # Verify the submission exists in the current user's scope. Auth is + # already enforced by the route's @scoped decorator. + submission, _ = get_submission(submission_id) + + tar_bytes = _build_source_tarball(submission_id) + stream = io.BytesIO(tar_bytes) + stream.seek(0) + + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M") + filename = f"submission_{submission_id}-{timestamp}.tar.gz" + headers = { + "Content-Type": "application/gzip", + "Content-Disposition": f'attachment; filename="{filename}"', + "Cache-Control": "no-store", + } + return stream, status.OK, headers diff --git a/submit_ce/ui/routes/ui.py b/submit_ce/ui/routes/ui.py index ff78d343..c0b6fc5c 100644 --- a/submit_ce/ui/routes/ui.py +++ b/submit_ce/ui/routes/ui.py @@ -19,6 +19,7 @@ from ..auth import is_owner, is_admin_or_dev from submit_ce.ui.controllers.new import submission_agreement +from submit_ce.ui.controllers.new import source_package from submit_ce.ui.workflow.processor import WorkflowProcessor from submit_ce.ui.workflow.stages import FileUpload from .flow_control import flow_control, get_workflow, endpoint_name @@ -470,6 +471,35 @@ def submission_agreement_pdf(submission_id: str) -> Response: return rv +@UI.route('/<submission_id>/source_package.tar.gz', methods=["GET"]) +@scoped(scopes.VIEW_SUBMISSION, authorizer=is_owner, + unauthorized=redirect_to_login) +def source_package_tar_gz(submission_id: str) -> Response: + """Serve the submission's current source files as a fresh .tar.gz. + + Mirrors Submit 1.5's 'Download Package' button: bundles the current + individual source files on the fly. The persisted + ``<submission_id>.tar.gz`` in the bucket is only updated after a + successful compile (see ``compile_at_gcp.py``), so it doesn't + reliably represent the user's current working source. Linked from + the sidebar of the Upload Files and Review Files pages. + """ + stream, code, headers = source_package.download_source_package( + request.method, + MultiDict(request.args.items(multi=True)), + request.auth, + submission_id, + ) + download_name = (headers.get('Content-Disposition', '') + .split('filename="')[-1].rstrip('"') + or f"submission_{submission_id}.tar.gz") + rv = send_file(stream, mimetype=headers['Content-Type'], + download_name=download_name, + as_attachment=True) + rv.headers['Cache-Control'] = headers.get('Cache-Control', 'no-store') + return rv + + @UI.route('/<submission_id>/compilation_log', methods=["GET"]) @scoped(scopes.VIEW_SUBMISSION, authorizer=is_owner, unauthorized=redirect_to_login) diff --git a/submit_ce/ui/templates/submit/file_upload.html b/submit_ce/ui/templates/submit/file_upload.html index 90629be7..8b2ac134 100644 --- a/submit_ce/ui/templates/submit/file_upload.html +++ b/submit_ce/ui/templates/submit/file_upload.html @@ -86,6 +86,20 @@ <h3>Accepted file properties</h3> <a href="{{ url_for('help_submit_sizes') }}">submission size and exemptions to size limits</a></li> </ul> + + {# Download a fresh .tar.gz of the submission's current source files. + Built on the fly by source_package.download_source_package; mirrors + the Submit 1.5 "Download Package" sidebar button. Anchor + download + attribute is enough -- no JS required. #} + {% if submission_id %} + <div class="buttons" style="margin-top: 1rem;"> + <a href="{{ url_for('ui.source_package_tar_gz', submission_id=submission_id) }}" + class="button button-secondary" + download> + Download Package + </a> + </div> + {% endif %} </div> {% endblock info_container_middle %} From 438442fb551a9231994b02ce061aed1082eada7a Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Fri, 29 May 2026 18:13:50 -0400 Subject: [PATCH 16/23] Review Files sidebar: switch to v5 info_container_middle block carrying forward Submit 1.5's sidebar content (Did you know..., Accepted Formats, Important, Read more about v1.5) in the new heading hierarchy. Adds two action buttons missing from the v5 Upload mockup but useful on Review: Download Package and Back to upload step (links to ui.file_upload). [SUBMISSION-150] David --- .../ui/templates/submit/review_files.html | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/submit_ce/ui/templates/submit/review_files.html b/submit_ce/ui/templates/submit/review_files.html index 25f36419..7b52e30a 100644 --- a/submit_ce/ui/templates/submit/review_files.html +++ b/submit_ce/ui/templates/submit/review_files.html @@ -29,9 +29,58 @@ {% block title -%}Review Files{%- endblock title %} -{% block important_text_box %} - <p>Review Files sidebar information goes here</p> -{% endblock important_text_box %} +{# v5-style sidebar carried forward from Submit 1.5's addfiles/reviewfiles + sidebar.tt. Same content as 1.5 (Did you know..., Accepted Formats, + Important, Read more about v1.5 changes) but laid out in the v5 + heading hierarchy (h2 lead + h3 subsections, wrapped in message-body). + Adds two action buttons absent from the v5 Upload mockup: + - Download Package (new on-the-fly source_package endpoint) + - Back to upload step (go back without invalidating preflight, + e.g. when the user needs to add a missing file) +#} +{% block info_container_middle %} + <div class="message-body"> + <h2>Did you know you can simplify your submission?</h2> + <p>Upload a single .tar.gz or zip file.</p> + + <h3>Accepted Formats</h3> + <ul> + <li><a href="{{ url_for('help_submit_tex') }}">(La)TeX, + AMS(La)TeX, PDFLaTeX</a></li> + <li><a href="{{ url_for('help_submit_pdf') }}">PDF (not + generated from TeX)</a></li> + <li><a href="{{ url_for('help_submit_html') }}">HTML for + conference proceeding indexes</a></li> + </ul> + + <h3>Important</h3> + <p>If your submission is (La)TeX, then you must submit the source + (plus necessary macros and figures), not derivative dvi, + Postscript, or PDF (see + <a href="{{ url_for('help_whytex') }}">Why TeX?</a>). For more + information on formats and other submission details see + <a href="{{ url_for('help_submit') }}">Submission Help</a>. TeX + source uploaded to arXiv will be made publicly available.</p> + + <p>Read more about the new changes in + <a href="{{ url_for('help_submit_tex') }}#latex-processing-changes-april-2025">Submission + v1.5</a>.</p> + + {% if submission_id %} + <div class="buttons" style="margin-top: 1rem;"> + <a href="{{ url_for('ui.source_package_tar_gz', submission_id=submission_id) }}" + class="button button-secondary" + download> + Download Package + </a> + <a href="{{ url_for('ui.file_upload', submission_id=submission_id) }}" + class="button button-secondary"> + Back to upload step + </a> + </div> + {% endif %} + </div> +{% endblock info_container_middle %} {% block within_content %} {% if immediate_notifications %} From ee96dd0b71a4ac3aeb80a52ecce7ac9104577a00 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Mon, 1 Jun 2026 14:15:45 -0400 Subject: [PATCH 17/23] Added graceful failure iwarning when preflight is not available instead of 500 or stack trace. We now catch error and display a warning without the empty selections and file list that require the preflight report. Suggest retry or contact help. [SUBMISSION-150] David --- submit_ce/ui/controllers/new/review.py | 26 ++++++++++++- .../ui/templates/submit/review_files.html | 37 +++++++++++++++---- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/submit_ce/ui/controllers/new/review.py b/submit_ce/ui/controllers/new/review.py index 740f26b7..f0b66d7f 100644 --- a/submit_ce/ui/controllers/new/review.py +++ b/submit_ce/ui/controllers/new/review.py @@ -6,6 +6,7 @@ from flask import current_app from arxiv.auth.domain import Session from arxiv.base import alerts +from markupsafe import Markup from submit_ce.domain.event.process import StartPreflight, StartDirectives # noqa: F401 (StartDirectives used below) from submit_ce.domain.event import SetSourceFormat from ...auth import user_and_client_from_session @@ -148,8 +149,17 @@ def review_files(method: str, params: MultiDict, session: Session, preflight_data, user_decisions_data = _load_or_create_preflight(submission_id, params, session, token, workspace) if preflight_data is None: + # Preflight is what populates compiler choices, top-level TeX + # candidates, and per-file usage notes. Without it, the form + # below has nothing to render -- the template hides the form + # sections when file_notes is empty and surfaces this flash + # message + the main-area placeholder instead. alerts.flash_warning( - f"We couldn't load preflight data for this submission. {SUPPORT}", + Markup( + "We couldn't analyze the files in your submission " + "right now because the preflight service is " + "temporarily unavailable. Please refresh this page " + "to try again. ") + SUPPORT, title="Preflight unavailable") return stay_on_this_stage((rdata, status.OK, {})) @@ -253,7 +263,19 @@ def _load_or_create_preflight(submission_id: str, params: MultiDict, session: Se # user_decisions + preflight + compile logs -> directives.json file_store.delete_directives(submission_id) - start_preflight(params, session, submission_id, token) + # Preflight calls into the external tex2pdf service; that service + # may be unreachable (local dev without the service running, a + # transient outage in production, etc.). If it fails, log and + # continue with preflight_data=None -- review_files() already has + # a clean handler for that case (flashes "Preflight unavailable" + # and stays on this stage) instead of bubbling up as a 500. + try: + start_preflight(params, session, submission_id, token) + except Exception as exc: + logger.warning( + "Could not run preflight for submission %s: %s", + submission_id, exc, + ) preflight_data = _get_preflight_data(submission_id) _store_source_format(preflight_data, session, submission_id) diff --git a/submit_ce/ui/templates/submit/review_files.html b/submit_ce/ui/templates/submit/review_files.html index 7b52e30a..c37b997d 100644 --- a/submit_ce/ui/templates/submit/review_files.html +++ b/submit_ce/ui/templates/submit/review_files.html @@ -94,12 +94,21 @@ <h3>Important</h3> {% endfor %} {% endif %} +{# The form below depends entirely on preflight output: source_file + choices come from preflight's detected TeX files, compiler choices + come from preflight-detected language, and the file table is + driven by file_notes. When preflight is unavailable we have nothing + to populate any of it with, so we hide the form sections and render + a clear placeholder instead of a row of empty dropdowns + a + misleading "No files uploaded yet" message. The submit_alerts flash + at the top of base_edit.html already tells the user *why*. #} +{% if file_notes %} <form id="form" class="form" action="{{ url_for('ui.review_files', submission_id=submission_id) }}" method="POST" enctype="multipart/form-data"> <div class="columns action-container"> <div class="column is-one-half-desktop is-one-half-tablet"> {{ form.csrf_token }} - + <div class="field"> <label class="label">{{ form.source_file.label }}</label> <div class="control"> @@ -149,7 +158,6 @@ <h3>Important</h3> <hr /> - {% if file_notes %} <table class="table is-fullwidth"> <thead> <tr> @@ -168,14 +176,29 @@ <h3>Important</h3> {% endfor %} </tbody> </table> - {% else %} - <p class="title is-4 breathe-vertical has-text-centered"> - No files have been uploaded yet. - </p> - {% endif %} </div> </div> </form> +{% else %} + <div class="notification is-warning" role="alert"> + <h2 class="is-size-5 is-marginless"> + {{ svg.exclamation(klass="icon filter-orange") }} + File analysis is unavailable + </h2> + <p>We couldn't analyze your submission's files right now because the + preflight service is temporarily unavailable.</p> + <p><strong>What to try:</strong></p> + <ul> + <li>Refresh this page in a moment — transient outages + typically resolve quickly.</li> + <li>If the problem persists after several minutes, please contact + <a href="mailto:help@arxiv.org">arXiv support</a> and include + your submission ID.</li> + </ul> + <p class="is-marginless">Your uploaded files have not been lost + — this step just couldn't read them for analysis.</p> + </div> +{% endif %} {% endblock within_content %} From ca59dbd95d820e1858d70cd8586df93433d77188 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 2 Jun 2026 12:44:29 -0400 Subject: [PATCH 18/23] Review Files form rewrite of main content into v5-style cards echoing the Submit 1.5 page layout. Added current sidebar content from 1.5 as placeholder for review. Some additional CSS to support new review cards. [SUBMISSION-150] David --- submit_ce/ui/static/css/submit_overrides.css | 99 ---------- .../ui/templates/submit/review_files.html | 181 ++++++++++-------- 2 files changed, 106 insertions(+), 174 deletions(-) diff --git a/submit_ce/ui/static/css/submit_overrides.css b/submit_ce/ui/static/css/submit_overrides.css index 5e96a228..e69de29b 100644 --- a/submit_ce/ui/static/css/submit_overrides.css +++ b/submit_ce/ui/static/css/submit_overrides.css @@ -1,99 +0,0 @@ - - -/* Override blue title -> black */ -h1.title.title-submit { - color: #000 !important; -} - -/* Adjust blue active workflow nav tab -> black */ -.navbar-item.is-active, -.navbar-link.is-active { - color: #000 !important; - border-bottom-color: #000 !important; -} - -/* Keep outer border of info box blue */ -.message.is-info { - border-color: #8bb8dc !important; -} - -/* Keep inner border of info box blue */ -.message.is-info .message-body { - border-color: #8bb8dc !important; -} - -/* Restore faint blue background + correct text color - for THIS PAGE ONLY (strong enough selector) */ -#main-page-content .message.is-info .message-body { - background-color: #e7f2fc !important; - color: #1d6ea7 !important; -} - -/* Remove sidebar shading on all Submit 2.0 workflow pages */ -.info-container { - background: transparent !important; - border-left: none !important; - box-shadow: none !important; -} - -/* Sidebar visual cleanup to match mockup */ - -/* Add padding to the sidebar inner content */ -.info-container-middle { - padding: 1rem 1.25rem !important; /* comfortable L/R spacing */ -} - -/* Add thin divider line between buttons and inner content */ -.info-container-top { - border-bottom: 1px solid #d0d7de !important; /* subtle gray divider */ - padding-bottom: 0.75rem !important; /* space under buttons */ - margin-bottom: 0.75rem !important; /* space before content begins */ -} - -/* Make disabled buttons obviously inactive even when they carry a Bulma - color modifier like is-success, is-link, is-danger, etc. Bulma's - default disabled style only adds opacity: 0.5, which on a saturated - green/blue button still reads as "active" at a glance. We desaturate - to gray + a not-allowed cursor so users can tell from across the room - that the button is gated. */ -.submit-nav .button[disabled], -.submit-nav .button[disabled]:hover, -.submit-nav .button[disabled]:focus, -.submit-nav .button[disabled]:active { - background-color: #e4e6eb !important; - border-color: #cdd1d8 !important; - color: #8a8f98 !important; - box-shadow: none !important; - cursor: not-allowed !important; - opacity: 1 !important; /* override Bulma's 0.5 fade-only */ -} - -/* Highlight the proceed-confirmation checkbox on the Confirm page when - ticking it is the only remaining step before Submit (PDF has been - previewed). The page has a lot going on (abstract preview, sidebar, - shortcut buttons), so without a visual cue the user can lose track - of what to do next. A soft amber row + bolder label brings the eye - to the checkbox without yelling. The pulse animation gently - reinforces it for a few seconds, then settles into the static - highlight. */ -.proceed-checkbox { - padding: 0.5rem 0.75rem; - border-radius: 4px; - transition: background-color 0.25s ease, border-color 0.25s ease; - border: 1px solid transparent; -} -.proceed-checkbox.proceed-highlight { - background-color: #fff8db; /* soft amber */ - border-color: #f6c344; /* amber accent */ - font-weight: 600; - animation: proceed-pulse 1.8s ease-in-out 2; -} -@keyframes proceed-pulse { - 0% { box-shadow: 0 0 0 0 rgba(246, 195, 68, 0.55); } - 50% { box-shadow: 0 0 0 6px rgba(246, 195, 68, 0); } - 100% { box-shadow: 0 0 0 0 rgba(246, 195, 68, 0); } -} -.proceed-checkbox.proceed-highlight input[type="checkbox"] { - transform: scale(1.15); - margin-right: 0.4rem; -} diff --git a/submit_ce/ui/templates/submit/review_files.html b/submit_ce/ui/templates/submit/review_files.html index c37b997d..6e7209f6 100644 --- a/submit_ce/ui/templates/submit/review_files.html +++ b/submit_ce/ui/templates/submit/review_files.html @@ -5,24 +5,42 @@ <script src="{{ url_for('static', filename='js/filewidget.js') }}"></script> {% endblock addl_head %} -{% macro display_preflight_tree(key, item) %} +{# Renders one row of the Review Files table for a preflight-detected file, + or recurses into a subdirectory listing. The third argument + ``top_level_filename`` is the currently-selected top-level TeX file + (form.source_file.data) so we can render "Used: top-level file" for it. + 00README.json gets a dedicated label and a disabled delete checkbox -- + it's a build-directives file, not a candidate for deletion. #} +{% macro display_preflight_tree(key, item, top_level_filename) %} {% if 'filename' in item %} <tr> - <td> + <td class="has-text-centered"> <input type="hidden" name="all_files" value="{{ item.filename }}"> - <input type="checkbox" name="selected_files" value="{{ item.filename }}"> + {% if item.filename == '00README.json' %} + <input type="checkbox" name="selected_files" value="{{ item.filename }}" + disabled + title="00README.json contains build directives and cannot be deleted from this step"> + {% else %} + <input type="checkbox" name="selected_files" value="{{ item.filename }}"> + {% endif %} </td> <td>{{ item.filename }}</td> <td> - {% if item.used_by %}used by: {{ item.used_by | join(', ') }}<br>{% endif %} - {% if item.used_by_tex %}used tex: {{ item.used_by_tex | join(', ') }}<br>{% endif %} - {% if item.used_by_bib %}used bib: {{ item.used_by_bib | join(', ') }}{% endif %} + {% if item.filename == '00README.json' %} + Build directives file + {% elif item.filename == top_level_filename %} + Used: top-level file + {% else %} + {% if item.used_by %}Used by: {{ item.used_by | join(', ') }}<br>{% endif %} + {% if item.used_by_tex %}Used by (TeX): {{ item.used_by_tex | join(', ') }}<br>{% endif %} + {% if item.used_by_bib %}Used by (bib): {{ item.used_by_bib | join(', ') }}{% endif %} + {% endif %} </td> </tr> {% else %} <tr><td colspan="3"><em>{{ key }}/</em></td></tr> {% for k, subitem in item.items() %} - {{ display_preflight_tree(k, subitem) }} + {{ display_preflight_tree(k, subitem, top_level_filename) }} {% endfor %} {% endif %} {% endmacro %} @@ -103,82 +121,95 @@ <h3>Important</h3> misleading "No files uploaded yet" message. The submit_alerts flash at the top of base_edit.html already tells the user *why*. #} {% if file_notes %} -<form id="form" class="form" action="{{ url_for('ui.review_files', submission_id=submission_id) }}" method="POST" enctype="multipart/form-data"> - <div class="columns action-container"> - - <div class="column is-one-half-desktop is-one-half-tablet"> - {{ form.csrf_token }} - - <div class="field"> - <label class="label">{{ form.source_file.label }}</label> - <div class="control"> - <div class="select is-fullwidth"> - {{ form.source_file }} - </div> +<form id="form" class="form" + action="{{ url_for('ui.review_files', submission_id=submission_id) }}" + method="POST" enctype="multipart/form-data"> + {{ form.csrf_token }} + + {# Section 1: Select Compiler -- compiler engine + TeX Live version, + auto-selected from preflight, but the user can override. #} + <div class="action-container review-section"> + <h2 class="title is-5">Select Compiler</h2> + + <div class="field"> + <label class="label" for="compiler">Compiler</label> + <p class="help">Our automated scan tries to identify the correct + compiler. You can change it here or keep the auto-selection.</p> + <div class="control"> + <div class="select is-fullwidth"> + {{ form.compiler }} </div> - {% if form.source_file.errors %} - {% for error in form.source_file.errors %} - <p class="help is-danger">{{ error }}</p> - {% endfor %} - {% endif %} </div> + {% for error in form.compiler.errors %} + <p class="help is-danger">{{ error }}</p> + {% endfor %} + </div> - <div class="columns"> - <div class="column"> - <div class="field"> - <label class="label">{{ form.compiler.label }}</label> - <div class="control"> - <div class="select is-fullwidth"> - {{ form.compiler }} - </div> - </div> - {% if form.compiler.errors %} - {% for error in form.compiler.errors %} - <p class="help is-danger">{{ error }}</p> - {% endfor %} - {% endif %} - </div> - </div> - <div class="column"> - <div class="field"> - <label class="label">{{ form.compiler_version.label }}</label> - <div class="control"> - <div class="select is-fullwidth"> - {{ form.compiler_version }} - </div> - </div> - {% if form.compiler_version.errors %} - {% for error in form.compiler_version.errors %} - <p class="help is-danger">{{ error }}</p> - {% endfor %} - {% endif %} - </div> + <div class="field"> + <label class="label" for="compiler_version">TeX Live version</label> + <p class="help">arXiv currently uses TeX Live 2025 by default. You + can also select TeX Live 2023 as an alternative.</p> + <div class="control"> + <div class="select is-fullwidth"> + {{ form.compiler_version }} </div> </div> + {% for error in form.compiler_version.errors %} + <p class="help is-danger">{{ error }}</p> + {% endfor %} + </div> + </div> - <hr /> - - <table class="table is-fullwidth"> - <thead> - <tr> - <th> - Delete - <br> - <a href="#" onclick="document.querySelectorAll('input[name=selected_files]').forEach(cb => cb.checked = false); return false;">keep all</a> - </th> - <th>File Name</th> - <th>Auto-detected Notes</th> - </tr> - </thead> - <tbody> - {% for k, item in (file_notes | group_preflight_files).items() %} - {{ display_preflight_tree(k, item) }} - {% endfor %} - </tbody> - </table> - + {# Section 2: Select Top-Level TeX File -- the file that will be the + compilation entry point. Submit 2.0 supports a single top-level + file, unlike legacy 1.5 which allowed multiple with reordering. #} + <div class="action-container review-section"> + <h2 class="title is-5">Select Top-Level TeX File</h2> + + <div class="field"> + <label class="label" for="source_file">Top-level TeX file</label> + <p class="help">Our automated scan tries to identify the top-level + TeX file. You can change it here or keep the auto-selection.</p> + <div class="control"> + <div class="select is-fullwidth"> + {{ form.source_file }} + </div> + </div> + {% for error in form.source_file.errors %} + <p class="help is-danger">{{ error }}</p> + {% endfor %} </div> </div> + + {# Section 3: Review Files -- list of all source files with + preflight-derived notes. Files checked for deletion will be + removed when the user clicks Continue, invalidating preflight + and routing back to Upload Files (see _update_preflight). #} + <div class="action-container review-section"> + <h2 class="title is-5">Review Files</h2> + <p>The automated scan tries to identify issues and unused files + that can be deleted. You can make changes below or leave + auto-selections in place.</p> + + <table class="table is-fullwidth is-striped review-files-table"> + <thead> + <tr> + <th class="has-text-centered" style="width: 6rem;"> + Delete<br> + <a href="#" class="is-size-7 keep-all-link" + onclick="document.querySelectorAll('input[name=selected_files]:not([disabled])').forEach(cb => cb.checked = false); return false;">Keep All</a> + </th> + <th>File Name</th> + <th>Auto-detected Notes</th> + </tr> + </thead> + <tbody> + {% for k, item in (file_notes | group_preflight_files).items() %} + {{ display_preflight_tree(k, item, form.source_file.data) }} + {% endfor %} + </tbody> + </table> + </div> </form> {% else %} <div class="notification is-warning" role="alert"> From 18dab7c2de7f2b03cbac87242b294f32d01ec2f6 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Tue, 2 Jun 2026 13:31:26 -0400 Subject: [PATCH 19/23] Update expected error string after recent updates to the failure page. [SUBMISSION-150] David --- submit_ce/ui/controllers/new/tests/test_review.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/submit_ce/ui/controllers/new/tests/test_review.py b/submit_ce/ui/controllers/new/tests/test_review.py index aa14f104..32f0bc5e 100644 --- a/submit_ce/ui/controllers/new/tests/test_review.py +++ b/submit_ce/ui/controllers/new/tests/test_review.py @@ -21,4 +21,9 @@ def test_review_files_get_warning_via_http(app, authorized_client, sub_files, assert resp.status_code == status.OK assert mock_flash.called - assert "couldn't load preflight data" in mock_flash.call_args[0][0] + # Flash text changed when the page was redesigned to show a clearer + # placeholder instead of bare empty form controls. Assert on the + # title and a stable substring of the body. + assert mock_flash.call_args[1].get('title') == 'Preflight unavailable' + assert "preflight service is temporarily unavailable" in str( + mock_flash.call_args[0][0]) From a91738139dc296fc3f3f6592f1c0fb1c3dd18063 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Fri, 5 Jun 2026 10:38:15 -0400 Subject: [PATCH 20/23] Reenable condition for process stage. [SUBMISSION-150] David --- submit_ce/ui/workflow/stages.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/submit_ce/ui/workflow/stages.py b/submit_ce/ui/workflow/stages.py index e37ce275..9698d901 100644 --- a/submit_ce/ui/workflow/stages.py +++ b/submit_ce/ui/workflow/stages.py @@ -118,8 +118,7 @@ class Process(Stage): title = "File process" display = "Process Files" """We need to re-process every time the source is updated.""" - completed = [] - #completed = [conditions.is_source_processed] + completed = [conditions.is_source_processed] class Metadata(Stage): From f7207fed8bab919b5866598a90ff28e64d310961 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Fri, 5 Jun 2026 10:42:04 -0400 Subject: [PATCH 21/23] Disable debugging. [SUNBMISSION-150] David --- submit_ce/ui/templates/submit/final_preview.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submit_ce/ui/templates/submit/final_preview.html b/submit_ce/ui/templates/submit/final_preview.html index 938a6410..53e0078b 100644 --- a/submit_ce/ui/templates/submit/final_preview.html +++ b/submit_ce/ui/templates/submit/final_preview.html @@ -212,7 +212,7 @@ <h3>Preview and Editing Shortcuts</h3> #} <script> (function () { - var DEBUG = true; // flip to false once the flow is verified + var DEBUG = false; // enable/disable debugging function log() { if (!DEBUG || !window.console) return; console.log.apply(console, ['[confirm]'].concat([].slice.call(arguments))); From 6c8e03a8dd8b8148bd5b23dc496d7b4dbf407e26 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Mon, 8 Jun 2026 18:45:41 -0400 Subject: [PATCH 22/23] Restore submit_overrides.css and switch Review Files to flat metadata-style layout: commit ca59dbd accidentally emptied submit_overrides.css, removing the functional disabled-button and proceed-checkbox-highlight styles needed by the Confirm page along with the sidebar polish. Restored to the pre-wipe 18f6c38 state;eliminated red headings carried over from 1.5. [SUBMISSION-150] David --- submit_ce/ui/static/css/submit_overrides.css | 99 +++++++++++++++++ .../ui/templates/submit/review_files.html | 100 ++++++++---------- 2 files changed, 146 insertions(+), 53 deletions(-) diff --git a/submit_ce/ui/static/css/submit_overrides.css b/submit_ce/ui/static/css/submit_overrides.css index e69de29b..5e96a228 100644 --- a/submit_ce/ui/static/css/submit_overrides.css +++ b/submit_ce/ui/static/css/submit_overrides.css @@ -0,0 +1,99 @@ + + +/* Override blue title -> black */ +h1.title.title-submit { + color: #000 !important; +} + +/* Adjust blue active workflow nav tab -> black */ +.navbar-item.is-active, +.navbar-link.is-active { + color: #000 !important; + border-bottom-color: #000 !important; +} + +/* Keep outer border of info box blue */ +.message.is-info { + border-color: #8bb8dc !important; +} + +/* Keep inner border of info box blue */ +.message.is-info .message-body { + border-color: #8bb8dc !important; +} + +/* Restore faint blue background + correct text color + for THIS PAGE ONLY (strong enough selector) */ +#main-page-content .message.is-info .message-body { + background-color: #e7f2fc !important; + color: #1d6ea7 !important; +} + +/* Remove sidebar shading on all Submit 2.0 workflow pages */ +.info-container { + background: transparent !important; + border-left: none !important; + box-shadow: none !important; +} + +/* Sidebar visual cleanup to match mockup */ + +/* Add padding to the sidebar inner content */ +.info-container-middle { + padding: 1rem 1.25rem !important; /* comfortable L/R spacing */ +} + +/* Add thin divider line between buttons and inner content */ +.info-container-top { + border-bottom: 1px solid #d0d7de !important; /* subtle gray divider */ + padding-bottom: 0.75rem !important; /* space under buttons */ + margin-bottom: 0.75rem !important; /* space before content begins */ +} + +/* Make disabled buttons obviously inactive even when they carry a Bulma + color modifier like is-success, is-link, is-danger, etc. Bulma's + default disabled style only adds opacity: 0.5, which on a saturated + green/blue button still reads as "active" at a glance. We desaturate + to gray + a not-allowed cursor so users can tell from across the room + that the button is gated. */ +.submit-nav .button[disabled], +.submit-nav .button[disabled]:hover, +.submit-nav .button[disabled]:focus, +.submit-nav .button[disabled]:active { + background-color: #e4e6eb !important; + border-color: #cdd1d8 !important; + color: #8a8f98 !important; + box-shadow: none !important; + cursor: not-allowed !important; + opacity: 1 !important; /* override Bulma's 0.5 fade-only */ +} + +/* Highlight the proceed-confirmation checkbox on the Confirm page when + ticking it is the only remaining step before Submit (PDF has been + previewed). The page has a lot going on (abstract preview, sidebar, + shortcut buttons), so without a visual cue the user can lose track + of what to do next. A soft amber row + bolder label brings the eye + to the checkbox without yelling. The pulse animation gently + reinforces it for a few seconds, then settles into the static + highlight. */ +.proceed-checkbox { + padding: 0.5rem 0.75rem; + border-radius: 4px; + transition: background-color 0.25s ease, border-color 0.25s ease; + border: 1px solid transparent; +} +.proceed-checkbox.proceed-highlight { + background-color: #fff8db; /* soft amber */ + border-color: #f6c344; /* amber accent */ + font-weight: 600; + animation: proceed-pulse 1.8s ease-in-out 2; +} +@keyframes proceed-pulse { + 0% { box-shadow: 0 0 0 0 rgba(246, 195, 68, 0.55); } + 50% { box-shadow: 0 0 0 6px rgba(246, 195, 68, 0); } + 100% { box-shadow: 0 0 0 0 rgba(246, 195, 68, 0); } +} +.proceed-checkbox.proceed-highlight input[type="checkbox"] { + transform: scale(1.15); + margin-right: 0.4rem; +} diff --git a/submit_ce/ui/templates/submit/review_files.html b/submit_ce/ui/templates/submit/review_files.html index 6e7209f6..062255d3 100644 --- a/submit_ce/ui/templates/submit/review_files.html +++ b/submit_ce/ui/templates/submit/review_files.html @@ -121,75 +121,69 @@ <h3>Important</h3> misleading "No files uploaded yet" message. The submit_alerts flash at the top of base_edit.html already tells the user *why*. #} {% if file_notes %} +{# Flat layout matching the Metadata page (per UI designer feedback): + no card framing, no red section headings. Form fields stack + vertically with their own labels and help text. The three logical + groupings (compiler, top-level file, file table) are still present + as ordinary <div class="field"> blocks. #} <form id="form" class="form" action="{{ url_for('ui.review_files', submission_id=submission_id) }}" method="POST" enctype="multipart/form-data"> {{ form.csrf_token }} - {# Section 1: Select Compiler -- compiler engine + TeX Live version, - auto-selected from preflight, but the user can override. #} - <div class="action-container review-section"> - <h2 class="title is-5">Select Compiler</h2> - - <div class="field"> - <label class="label" for="compiler">Compiler</label> - <p class="help">Our automated scan tries to identify the correct - compiler. You can change it here or keep the auto-selection.</p> - <div class="control"> - <div class="select is-fullwidth"> - {{ form.compiler }} - </div> + {# Compiler engine + TeX Live version (auto-selected from preflight) #} + <div class="field"> + <label class="label" for="compiler">Compiler</label> + <p class="help">Our automated scan tries to identify the correct + compiler. You can change it here or keep the auto-selection.</p> + <div class="control"> + <div class="select is-fullwidth"> + {{ form.compiler }} </div> - {% for error in form.compiler.errors %} - <p class="help is-danger">{{ error }}</p> - {% endfor %} </div> + {% for error in form.compiler.errors %} + <p class="help is-danger">{{ error }}</p> + {% endfor %} + </div> - <div class="field"> - <label class="label" for="compiler_version">TeX Live version</label> - <p class="help">arXiv currently uses TeX Live 2025 by default. You - can also select TeX Live 2023 as an alternative.</p> - <div class="control"> - <div class="select is-fullwidth"> - {{ form.compiler_version }} - </div> + <div class="field"> + <label class="label" for="compiler_version">TeX Live version</label> + <p class="help">arXiv currently uses TeX Live 2025 by default. You + can also select TeX Live 2023 as an alternative.</p> + <div class="control"> + <div class="select is-fullwidth"> + {{ form.compiler_version }} </div> - {% for error in form.compiler_version.errors %} - <p class="help is-danger">{{ error }}</p> - {% endfor %} </div> + {% for error in form.compiler_version.errors %} + <p class="help is-danger">{{ error }}</p> + {% endfor %} </div> - {# Section 2: Select Top-Level TeX File -- the file that will be the - compilation entry point. Submit 2.0 supports a single top-level - file, unlike legacy 1.5 which allowed multiple with reordering. #} - <div class="action-container review-section"> - <h2 class="title is-5">Select Top-Level TeX File</h2> - - <div class="field"> - <label class="label" for="source_file">Top-level TeX file</label> - <p class="help">Our automated scan tries to identify the top-level - TeX file. You can change it here or keep the auto-selection.</p> - <div class="control"> - <div class="select is-fullwidth"> - {{ form.source_file }} - </div> + {# Top-level TeX file. Submit 2.0 supports a single top-level file + (unlike legacy 1.5 which allowed multiple with reordering). #} + <div class="field"> + <label class="label" for="source_file">Top-level TeX file</label> + <p class="help">Our automated scan tries to identify the top-level + TeX file. You can change it here or keep the auto-selection.</p> + <div class="control"> + <div class="select is-fullwidth"> + {{ form.source_file }} </div> - {% for error in form.source_file.errors %} - <p class="help is-danger">{{ error }}</p> - {% endfor %} </div> + {% for error in form.source_file.errors %} + <p class="help is-danger">{{ error }}</p> + {% endfor %} </div> - {# Section 3: Review Files -- list of all source files with - preflight-derived notes. Files checked for deletion will be - removed when the user clicks Continue, invalidating preflight - and routing back to Upload Files (see _update_preflight). #} - <div class="action-container review-section"> - <h2 class="title is-5">Review Files</h2> - <p>The automated scan tries to identify issues and unused files - that can be deleted. You can make changes below or leave - auto-selections in place.</p> + {# Review Files list. Files checked for deletion will be removed when + the user clicks Continue, invalidating preflight and routing back + to Upload Files (see _update_preflight). #} + <div class="field"> + <label class="label">Files</label> + <p class="help">The automated scan tries to identify issues and + unused files that can be deleted. You can make changes below or + leave auto-selections in place.</p> <table class="table is-fullwidth is-striped review-files-table"> <thead> From 1b52bdc344dcc4331d2165d7caf552ec2d6872a0 Mon Sep 17 00:00:00 2001 From: David Fielding <dlf2@cornell.edu> Date: Mon, 8 Jun 2026 19:00:35 -0400 Subject: [PATCH 23/23] Add back container outline for Review Files page. Wrap Review Files form in a single .action-container matching add_metadata.html. [SUBMISSION-150] David --- submit_ce/ui/templates/submit/review_files.html | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/submit_ce/ui/templates/submit/review_files.html b/submit_ce/ui/templates/submit/review_files.html index 062255d3..f68000a7 100644 --- a/submit_ce/ui/templates/submit/review_files.html +++ b/submit_ce/ui/templates/submit/review_files.html @@ -122,14 +122,16 @@ <h3>Important</h3> at the top of base_edit.html already tells the user *why*. #} {% if file_notes %} {# Flat layout matching the Metadata page (per UI designer feedback): - no card framing, no red section headings. Form fields stack - vertically with their own labels and help text. The three logical - groupings (compiler, top-level file, file table) are still present - as ordinary <div class="field"> blocks. #} + no per-section card framing, no red section headings. The whole + form is wrapped in a single <div class="action-container"> so the + page has the same outline/container look as add_metadata.html. The + three logical groupings (compiler, top-level file, file table) are + still present as ordinary <div class="field"> blocks. #} <form id="form" class="form" action="{{ url_for('ui.review_files', submission_id=submission_id) }}" method="POST" enctype="multipart/form-data"> - {{ form.csrf_token }} + <div class="action-container"> + {{ form.csrf_token }} {# Compiler engine + TeX Live version (auto-selected from preflight) #} <div class="field"> @@ -204,6 +206,7 @@ <h3>Important</h3> </tbody> </table> </div> + </div>{# /.action-container #} </form> {% else %} <div class="notification is-warning" role="alert">