From 0a01f893d67be7e188202a1d2505161a74eb3d93 Mon Sep 17 00:00:00 2001 From: Brian Maltzan Date: Tue, 9 Jun 2026 09:12:14 -0400 Subject: [PATCH 1/5] update readme --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1391a8e4..3ae910a6 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,22 @@ arXiv paper submission system ## Install & use ```bash -# Start the compiler api: -gcloud run services proxy tex2pdf-api-default --project arxiv-development --region us-central1 --port=9001 - -# Install gcld3 dependencies needed by arxiv-base metadata checks -# On linux: +# On linux, Install gcld3 dependencies needed by arxiv-base metadata checks sudo apt-get install cmake libprotobuf-dev protobuf-compiler -uv sync -# On mac, you need a version of protobuf <= 21: -brew search protobuf + +# On mac, you need a version of protobuf <= 21 brew install protobuf@21 pyenv shell 3.11 # or similar source .venv/bin/activate uv sync -# this will give you an Authorization token, save that and use a browser extension -# like modheader to add Authorization=eyJhb... +# Generate an Authorization token: uv run python submit_ce/make_test_db.py bootstrap_db +# Use a browser extension like modheader to send the token for localhost +# add Authorization=eyJhb... + uv run python local_dev.py open http://localhost:8000 ``` @@ -39,4 +36,4 @@ open http://localhost:8000 gcloud auth configure-docker gcr.io # only needed once docker build . -t gcr.io/arxiv-development/submit-ce/submit-ce-ui docker push gcr.io/arxiv-development/submit-ce/submit-ce-ui -``` \ No newline at end of file +``` From c4eb5bed6d33f52ad3eb63ee7126dbb9db18738b Mon Sep 17 00:00:00 2001 From: Brian Maltzan Date: Tue, 9 Jun 2026 09:20:00 -0400 Subject: [PATCH 2/5] minor --- submit_ce/ui/controllers/new/process.py | 4 ++-- submit_ce/ui/workflow/stages.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/submit_ce/ui/controllers/new/process.py b/submit_ce/ui/controllers/new/process.py index e1f71901..13252de6 100644 --- a/submit_ce/ui/controllers/new/process.py +++ b/submit_ce/ui/controllers/new/process.py @@ -211,7 +211,7 @@ def start_compilation(params: MultiDict, session: Session, submission_id: str, # if 'reason' in result.extra and "produced from TeX source" in result.extra['reason']: # alerts.flash_failure(TEX_PRODUCED_MARKUP) # elif 'reason' in result.extra and 'docker' in result.extra['reason']: - # alerts.flash_failure(DOCKER_ERROR_MARKUOP) + # alerts.flash_failure(DOCKER_ERROR_MARKUP) # else: # alerts.flash_failure(f"Processing failed") # else: @@ -262,7 +262,7 @@ class CompilationForm(csrf.CSRFForm): "submission is TeX produced is incorrect, you should send " \ "e-mail with your submission ID to " \ 'arXiv administrators.

') -DOCKER_ERROR_MARKUOP = \ +DOCKER_ERROR_MARKUP = \ Markup("Our automatic TeX processing system has failed to launch. " \ "There is a good chance we are aware of the issue, but if the " \ "problem persists you should send e-mail with your submission " \ diff --git a/submit_ce/ui/workflow/stages.py b/submit_ce/ui/workflow/stages.py index bbd2cbdb..fc306728 100644 --- a/submit_ce/ui/workflow/stages.py +++ b/submit_ce/ui/workflow/stages.py @@ -102,7 +102,6 @@ class ReviewFiles(Stage): """The user is asked to review files for their submission with input from preflight analysis. """ - #endpoint = 'review_files' endpoint = 'review_files' label = 'review your submission files' title = "Review Files" From c6ef9b667c3a97c8126781c6f8748602f436c126 Mon Sep 17 00:00:00 2001 From: Brian Maltzan Date: Tue, 9 Jun 2026 11:53:31 -0400 Subject: [PATCH 3/5] Missing: block more_notifications --- submit_ce/ui/templates/submit/file_upload.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/submit_ce/ui/templates/submit/file_upload.html b/submit_ce/ui/templates/submit/file_upload.html index ee8bbdf0..fad71115 100644 --- a/submit_ce/ui/templates/submit/file_upload.html +++ b/submit_ce/ui/templates/submit/file_upload.html @@ -52,6 +52,7 @@ {% block title -%}Upload Files{%- endblock title %} +{% block more_notifications %} {% if immediate_notifications %} {% for notification in immediate_notifications %} {% endfor %} {% endif %} +{% endblock more_notifications %} {% block important_text_box %} From 0c76aa3b015fced7318ff2070b06c889ec4971a2 Mon Sep 17 00:00:00 2001 From: Brian Maltzan Date: Wed, 10 Jun 2026 06:35:08 -0400 Subject: [PATCH 4/5] SUBMISSION-128: pdf only uploads --- submit_ce/domain/uploads.py | 1 - ...est_pubusb_impl.py => test_pubsub_impl.py} | 0 submit_ce/ui/conftest.py | 16 ++++ submit_ce/ui/controllers/new/process.py | 9 +- submit_ce/ui/controllers/new/review.py | 22 ++++- .../ui/controllers/new/tests/test_review.py | 18 ++-- .../ui/controllers/new/tests/test_upload.py | 11 ++- submit_ce/ui/controllers/new/upload.py | 88 +++++++++++++++---- submit_ce/ui/controllers/new/upload_delete.py | 21 ++++- submit_ce/ui/routes/flow_control.py | 13 ++- submit_ce/ui/routes/ui.py | 13 ++- submit_ce/ui/workflow/conditions.py | 8 +- 12 files changed, 180 insertions(+), 40 deletions(-) rename submit_ce/implementations/pubsub/tests/{test_pubusb_impl.py => test_pubsub_impl.py} (100%) diff --git a/submit_ce/domain/uploads.py b/submit_ce/domain/uploads.py index 80f9eacb..6c495f97 100644 --- a/submit_ce/domain/uploads.py +++ b/submit_ce/domain/uploads.py @@ -102,7 +102,6 @@ class Workspace(BaseModel): lifecycle: UploadLifecycleStates locked: bool identifier: str - source_format: SourceFormat = SourceFormat.UNKNOWN checksum: Optional[str] = None size: Optional[int] = None """Size in bytes of the uncompressed upload workspace.""" diff --git a/submit_ce/implementations/pubsub/tests/test_pubusb_impl.py b/submit_ce/implementations/pubsub/tests/test_pubsub_impl.py similarity index 100% rename from submit_ce/implementations/pubsub/tests/test_pubusb_impl.py rename to submit_ce/implementations/pubsub/tests/test_pubsub_impl.py diff --git a/submit_ce/ui/conftest.py b/submit_ce/ui/conftest.py index d7a352c6..d8335dbe 100644 --- a/submit_ce/ui/conftest.py +++ b/submit_ce/ui/conftest.py @@ -298,6 +298,22 @@ class _FakePdf: return submission +@pytest.fixture(scope="function") +def sub_files_tex(app, authorized_user, sub_files): + """sub_files with source_format set to TEX, so review_files runs its + normal preflight/review flow rather than redirecting on the source_format + guard.""" + with app.app_context(): + user = authorized_user + ua = InternalClient(name=f"test_client_{__file__}") + submission, _ = current_app.api.save( + SetSourceFormat(creator=user, client=ua, + source_format=SourceFormat.TEX.value), + submission_id=sub_files.submission_id, + ) + return submission + + @pytest.fixture(scope="function") def sub_reviewfiles(app, authorized_user, sub_files): """A submission that has passed through the review-files stage.""" diff --git a/submit_ce/ui/controllers/new/process.py b/submit_ce/ui/controllers/new/process.py index 13252de6..c0311c97 100644 --- a/submit_ce/ui/controllers/new/process.py +++ b/submit_ce/ui/controllers/new/process.py @@ -10,6 +10,7 @@ from submit_ce.domain.event.process import StartCompileSource from submit_ce.domain.exceptions import SaveError +from submit_ce.domain.uploads import SourceFormat from submit_ce.api.file_store import SubmissionFileStore from submit_ce.ui import SUPPORT @@ -21,7 +22,9 @@ from wtforms import SelectField from ..util import validate_command -from submit_ce.ui.routes.flow_control import ready_for_next, stay_on_this_stage +from submit_ce.ui.routes.flow_control import ( + ready_for_next, stay_on_this_stage, advance_to_current, +) from submit_ce.ui.backend import get_submission @@ -60,6 +63,10 @@ def file_process(method: str, params: MultiDict, session: Session, applicable. """ + submission, _ = get_submission(submission_id) + if submission.source_format == SourceFormat.PDF: + return advance_to_current(({}, status.OK, {})) + if method == "GET": return compile_status(params, session, submission_id, token) elif method == "POST": diff --git a/submit_ce/ui/controllers/new/review.py b/submit_ce/ui/controllers/new/review.py index affe2b5d..a87d8109 100644 --- a/submit_ce/ui/controllers/new/review.py +++ b/submit_ce/ui/controllers/new/review.py @@ -3,6 +3,7 @@ from http import HTTPStatus as status from typing import Tuple, Dict, Any, Optional, List +import httpx from flask import current_app from arxiv.auth.domain import Session from arxiv.base import alerts @@ -24,10 +25,13 @@ from wtforms import SelectField from wtforms.validators import DataRequired -from submit_ce.domain.uploads import Workspace +from submit_ce.domain.uploads import Workspace, SourceFormat from submit_ce.domain.exceptions import InvalidEvent, SaveError from submit_ce.ui.controllers.util import validate_command -from submit_ce.ui.routes.flow_control import stay_on_this_stage, ready_for_next, return_to_parent_stage +from submit_ce.ui.routes.flow_control import ( + stay_on_this_stage, ready_for_next, return_to_parent_stage, + return_to_previous_stage, advance_to_current, +) from submit_ce.ui.backend import get_submission from submit_ce.ui import SUPPORT @@ -153,6 +157,12 @@ def review_files(method: str, params: MultiDict, session: Session, if not workspace: return return_to_parent_stage((rdata, status.OK, {})) + if submission.source_format == SourceFormat.PDF: + return advance_to_current((rdata, status.OK, {})) + + if submission.source_format != SourceFormat.TEX: + return return_to_previous_stage((rdata, status.OK, {})) + if method == 'GET': preflight_data, user_decisions_data = _load_or_create_preflight( submission_id, params, session, token, workspace, submitter, client @@ -418,3 +428,11 @@ def start_directives(params: MultiDict, session: Session, submission_id: str, except SaveError as e: alerts.flash_failure(f"We couldn't start directives. {SUPPORT}", title="Directives failed") raise InternalServerError(response_data) from e + except httpx.HTTPError as e: + logger.error('Compile service error during StartDirectives for %s: %s', + submission_id, e) + alerts.flash_failure( + f"We couldn't start directives because the compile service" + f" is unavailable. {SUPPORT}", + title="Directives failed") + raise InternalServerError(response_data) from e diff --git a/submit_ce/ui/controllers/new/tests/test_review.py b/submit_ce/ui/controllers/new/tests/test_review.py index ee0a3053..37bb903d 100644 --- a/submit_ce/ui/controllers/new/tests/test_review.py +++ b/submit_ce/ui/controllers/new/tests/test_review.py @@ -14,7 +14,7 @@ from submit_ce.ui.controllers.new import review -def test_review_files_get_warning_via_http(app, authorized_client, sub_files, +def test_review_files_get_warning_via_http(app, authorized_client, sub_files_tex, mocker): """End-to-end: GET //review_files triggers flash_warning when _load_or_create_preflight yields no preflight data.""" @@ -22,7 +22,7 @@ def test_review_files_get_warning_via_http(app, authorized_client, sub_files, return_value=(None, None)) mock_flash = mocker.patch.object(review.alerts, 'flash_warning') - url = f"/{sub_files.submission_id}/review_files" + url = f"/{sub_files_tex.submission_id}/review_files" resp = authorized_client.get(url) assert resp.status_code == status.OK @@ -65,10 +65,10 @@ def _get_csrf(authorized_client, url, mocker): def test_review_files_post_with_changes_redirects_to_parent( - app, authorized_client, sub_files, mocker): + app, authorized_client, sub_files_tex, mocker): """End-to-end POST: when _update_preflight reports changes, the controller marks STAGE_PARENT and the flow redirects (303 SEE_OTHER).""" - url = f"/{sub_files.submission_id}/review_files" + url = f"/{sub_files_tex.submission_id}/review_files" csrf = _get_csrf(authorized_client, url, mocker) mock_update = mocker.patch.object(review, '_update_preflight', @@ -83,10 +83,10 @@ def test_review_files_post_with_changes_redirects_to_parent( def test_review_files_post_no_changes_no_preflight_flashes( - app, authorized_client, sub_files, mocker): + app, authorized_client, sub_files_tex, mocker): """End-to-end POST: when there are no changes but preflight is still unavailable, the controller flashes a warning and stays on the stage.""" - url = f"/{sub_files.submission_id}/review_files" + url = f"/{sub_files_tex.submission_id}/review_files" csrf = _get_csrf(authorized_client, url, mocker) mocker.patch.object(review, '_update_preflight', return_value=False) @@ -105,10 +105,10 @@ def test_review_files_post_no_changes_no_preflight_flashes( def test_review_files_post_no_changes_stores_zzrm_and_advances( - app, authorized_client, sub_files, mocker): + app, authorized_client, sub_files_tex, mocker): """End-to-end POST: when there are no changes and preflight is present, the controller stores the merged zzrm and advances to the next stage.""" - url = f"/{sub_files.submission_id}/review_files" + url = f"/{sub_files_tex.submission_id}/review_files" csrf = _get_csrf(authorized_client, url, mocker) mocker.patch.object(review, '_update_preflight', return_value=False) @@ -131,7 +131,7 @@ def test_review_files_post_no_changes_stores_zzrm_and_advances( fake_zzrm.from_dict.assert_called_once_with({'sources': []}) fake_zzrm.update_from_preflight.assert_called_once() mock_store.store_zzrm.assert_called_once_with( - str(sub_files.submission_id), {'merged': True} + str(sub_files_tex.submission_id), {'merged': True} ) diff --git a/submit_ce/ui/controllers/new/tests/test_upload.py b/submit_ce/ui/controllers/new/tests/test_upload.py index a891f30c..52b9b35a 100644 --- a/submit_ce/ui/controllers/new/tests/test_upload.py +++ b/submit_ce/ui/controllers/new/tests/test_upload.py @@ -32,6 +32,7 @@ def test_upload(app, authorized_client, sub_cross): b"Upload" in resp.data \ and b"<form " in resp.data + class TestUpload(CtrlBase): """Tests for :func:`submit_ce.controllers.upload`.""" @@ -160,8 +161,8 @@ def test_post_upload(self): data, code, _ = upload.upload_files('POST', params, self.session, submission_id, files=files, token='footoken') - self.assertEqual(mock_api.save.call_count, 1, - 'Saves the upload command via the api') + self.assertEqual(mock_api.save.call_count, 2, + 'Saves UploadFiles and SetSourceFormat via the api') self.assertEqual(code, status.OK) self.assertEqual(get_controllers_desire(data), STAGE_RESHOW, 'Successful upload and reshow form') @@ -229,8 +230,10 @@ def test_post_delete_confirmed(self): self.session, submission_id, 'footoken') - self.assertEqual(mock_api.save.call_count, 1, - 'Saves the remove-files command via the api') + self.assertEqual(mock_api.save.call_count, 2, + 'Saves RemoveFiles and SetSourceFormat via the api') self.assertEqual(code, status.OK) self.assertEqual(get_controllers_desire(data), STAGE_PARENT, 'Confirmed delete returns to the parent stage') + + diff --git a/submit_ce/ui/controllers/new/upload.py b/submit_ce/ui/controllers/new/upload.py index b2a62e29..4aaa6002 100644 --- a/submit_ce/ui/controllers/new/upload.py +++ b/submit_ce/ui/controllers/new/upload.py @@ -34,6 +34,7 @@ from wtforms import BooleanField, FileField from submit_ce.domain import Client, Event, User +from submit_ce.domain.event import SetSourceFormat from submit_ce.domain.event.file import UploadArchive, UploadFiles from submit_ce.domain.submission import Submission from submit_ce.domain.uploads import SourceFormat @@ -74,6 +75,25 @@ def _single_file_archive(files: MultiDict) -> bool: return is_file_tgz(pointer) or is_file_zip(pointer) +def _infer_source_format(files: List["FileStatus"]) -> Optional[SourceFormat]: + """Infer source_format from workspace files. + + If any .tex file is present, the submission is TEX (legacy arXiv permits + .pdf files, e.g. figures, inside a TeX submission, including in + subdirectories). If the workspace is a single lone .pdf, the submission + is PDF. Any other non-empty file set falls back to TEX, matching the + legacy default for multi-file submissions. Returns None for an empty + workspace. + """ + if not files: + return None + if any(f.name.lower().endswith('.tex') for f in files): + return SourceFormat.TEX + if len(files) == 1 and files[0].name.lower().endswith('.pdf'): + return SourceFormat.PDF + return SourceFormat.TEX + + class AddfilesForm(csrf.CSRFForm): """Form for uploading files.""" @@ -135,6 +155,9 @@ def upload_files(method: str, params: MultiDict, session: Session, 'submission': submission, 'form': AddfilesForm()}) + logger.error(f'BRIANM: Submission {submission.submission_id}, {submission.source_format}, {type(submission.source_format)}') + + if method not in ['GET', 'POST']: raise MethodNotAllowed() elif method == 'GET': @@ -149,7 +172,7 @@ def upload_files(method: str, params: MultiDict, session: Session, form = AddfilesForm(params) rdata.update({'form': form, 'submission': submission}) if not form.validate(): - logger.error('Submission %s Invalid upload form: %s %s', submission.submission_id, form.errors) + logger.error('Submission %s Invalid upload form: %s', submission.submission_id, form.errors) alerts.flash_failure("No file was uploaded; please try again.") return stay_on_this_stage((rdata, status.OK, {})) @@ -157,6 +180,12 @@ def upload_files(method: str, params: MultiDict, session: Session, try: match (file, params.get('action'), is_archive): case (_, 'next', _): + if submission.source_format is None: + alerts.flash_warning( + "Cannot proceed: please upload a single PDF, or" + " one or more .tex files.", + title='Source format required') + return stay_on_this_stage(_get_upload((rdata, status.OK, {}, token))) return ready_for_next((rdata, status.OK, {})) case (_, action, _) if action: # trying to go back to previous page return {}, status.SEE_OTHER, {} @@ -219,13 +248,14 @@ def _get_upload(params: MultiDict, session: Session, submission: Submission, else: workspace = current_app.api.get_file_store().get_workspace(submission_id=str(submission.submission_id)) + logger.error(f'BRIANM.2: Submission {submission.submission_id}, {submission.source_format}, {type(submission.source_format)}') + rdata.update({'status': workspace}) if workspace: - rdata.update({'immediate_notifications': _get_notifications(workspace)}) + rdata.update({'immediate_notifications': _get_notifications(submission, workspace)}) return rdata, status.OK, {} - def _upload_archive(form: AddfilesForm, file: FileStorage, submitter: User, client: Client, submission: Submission, rdata: Dict[str, Any], token: str) \ @@ -236,6 +266,14 @@ def _upload_archive(form: AddfilesForm, file: FileStorage, submission, _ = current_app.api.save(command, submission_id=submission.submission_id) workspace = current_app.api.get_file_store().get_workspace(submission_id=str(submission.submission_id)) converted_size = tidy_filesize(workspace.size) + + inferred = _infer_source_format(workspace.files) + if submission.source_format != inferred: + target = inferred.value if inferred is not None else None + submission, _ = current_app.api.save( + SetSourceFormat(creator=submitter, client=client, source_format=target), + submission_id=submission.submission_id, + ) if workspace.status is UploadStatus.READY: alerts.flash_success( f'Unpacked {workspace.file_count} files. Total submission' @@ -270,6 +308,15 @@ def _upload_files(form: AddfilesForm, file: FileStorage, submission, _ = current_app.api.save(command, submission_id=submission.submission_id) workspace = current_app.api.get_file_store().get_workspace(submission_id=str(submission.submission_id)) converted_size = tidy_filesize(workspace.size) + + inferred = _infer_source_format(workspace.files) + if submission.source_format != inferred: + target = inferred.value if inferred is not None else None + submission, _ = current_app.api.save( + SetSourceFormat(creator=submitter, client=client, source_format=target), + submission_id=submission.submission_id, + ) + if workspace.status is UploadStatus.READY: alerts.flash_success( f'Uploaded file. Total submission' @@ -294,11 +341,11 @@ def _upload_files(form: AddfilesForm, file: FileStorage, return stay_on_this_stage((rdata, status.OK, {})) -def _get_notifications(stat: Workspace) -> List[Dict[str, str]]: +def _get_notifications(submission: Submission, workspace: Workspace) -> List[Dict[str, str]]: notifications = [] - if not stat.files: # Nothing in the upload workspace. + if not workspace.files: # Nothing in the upload workspace. return notifications - if stat.status is UploadStatus.ERRORS: + if workspace.status is UploadStatus.ERRORS: notifications.append({ 'title': 'Unresolved errors', 'severity': 'danger', @@ -306,7 +353,7 @@ def _get_notifications(stat: Workspace) -> List[Dict[str, str]]: ' files. Please correct the errors below before' ' proceeding.' }) - elif stat.status is UploadStatus.READY_WITH_WARNINGS: + elif workspace.status is UploadStatus.READY_WITH_WARNINGS: notifications.append({ 'title': 'Warnings', 'severity': 'warning', @@ -315,15 +362,20 @@ 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: + has_tex = any(f.name.lower().endswith('.tex') for f in workspace.files) + if has_tex: 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.' + 'title': 'Detected TEX', + 'severity': 'success', + 'body': 'Your submission content is supported.' + }) + elif submission.source_format == SourceFormat.PDF: + notifications.append({ + 'title': 'Detected PDF', + 'severity': 'success', + 'body': 'Your submission content is supported.' }) - elif stat.source_format is SourceFormat.INVALID: + elif submission.source_format == SourceFormat.INVALID: notifications.append({ 'title': 'Unsupported submission type', 'severity': 'danger', @@ -333,9 +385,11 @@ def _get_notifications(stat: Workspace) -> List[Dict[str, str]]: }) else: notifications.append({ - 'title': f'Detected {stat.source_format.value.upper()}', - 'severity': 'success', - 'body': 'Your submission content is supported.' + '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.' }) return notifications diff --git a/submit_ce/ui/controllers/new/upload_delete.py b/submit_ce/ui/controllers/new/upload_delete.py index d4cfe85c..1d7281bd 100644 --- a/submit_ce/ui/controllers/new/upload_delete.py +++ b/submit_ce/ui/controllers/new/upload_delete.py @@ -9,8 +9,10 @@ from arxiv.forms import csrf from flask import current_app +from submit_ce.domain.event import SetSourceFormat from submit_ce.domain.event.file import RemoveAllFiles, RemoveFiles from submit_ce.ui.auth import user_and_client_from_session +from submit_ce.ui.controllers.new.upload import _infer_source_format from arxiv.auth.domain import Session from werkzeug.datastructures import MultiDict from wtforms import BooleanField, HiddenField @@ -77,7 +79,12 @@ def delete_all(method: str, params: MultiDict, session: Session, command = RemoveAllFiles(creator=submitter, client=client) if validate_command(form, command, submission, 'add_files'): - current_app.api.save(command, submission_id=submission.submission_id) + submission, _ = current_app.api.save(command, submission_id=submission.submission_id) + if submission.source_format is not None: + current_app.api.save( + SetSourceFormat(creator=submitter, client=client, source_format=None), + submission_id=submission.submission_id, + ) return return_to_parent_stage((rdata, status.OK, {})) return return_to_parent_stage((rdata, status.BAD_REQUEST, {})) @@ -146,7 +153,17 @@ def delete_file(method: str, params: MultiDict, session: Session, command = RemoveFiles(creator=submitter, client=client, files=[form.file_path.data]) if validate_command(form, command, submission, 'add_files'): - current_app.api.save(command, submission_id=submission.submission_id) + submission, _ = current_app.api.save(command, submission_id=submission.submission_id) + workspace = current_app.api.get_file_store().get_workspace( + submission_id=str(submission.submission_id)) + if workspace is not None: + inferred = _infer_source_format(workspace.files) + if submission.source_format != inferred: + target = inferred.value if inferred is not None else None + current_app.api.save( + SetSourceFormat(creator=submitter, client=client, source_format=target), + submission_id=submission.submission_id, + ) return return_to_parent_stage((rdata, status.OK, {})) return return_to_parent_stage((rdata, status.BAD_REQUEST, {})) diff --git a/submit_ce/ui/routes/flow_control.py b/submit_ce/ui/routes/flow_control.py index 45fe8b0d..02cea96c 100644 --- a/submit_ce/ui/routes/flow_control.py +++ b/submit_ce/ui/routes/flow_control.py @@ -43,11 +43,12 @@ FlowAction = Literal['prevous','next','save_exit'] FlowResponse = Tuple[FlowAction, Response] -ControllerDesires = Literal['stage_success', 'stage_reshow', 'stage_current', 'stage_parent'] +ControllerDesires = Literal['stage_success', 'stage_reshow', 'stage_current', 'stage_parent', 'stage_previous'] STAGE_SUCCESS: ControllerDesires = 'stage_success' STAGE_RESHOW: ControllerDesires = 'stage_reshow' STAGE_CURRENT: ControllerDesires = 'stage_current' STAGE_PARENT: ControllerDesires = 'stage_parent' +STAGE_PREVIOUS: ControllerDesires = 'stage_previous' def ready_for_next(response: CResponse) -> CResponse: """Mark the result from a controller being ready to move to the @@ -75,6 +76,14 @@ def return_to_parent_stage(response: CResponse) -> CResponse: return response +def return_to_previous_stage(response: CResponse) -> CResponse: + """Mark the result from a main-stage controller as needing to redirect to + the previous stage in the workflow (e.g., review_files back to file_upload + when prerequisites aren't met).""" + response[0].update({'flow_control_from_controller': STAGE_PREVIOUS}) + return response + + def get_controllers_desire(data: Dict) -> Optional[ControllerDesires]: return data.get('flow_control_from_controller', None) @@ -257,6 +266,8 @@ def flow_decision(method: str, controller_action: Optional[ControllerDesires], last_stage: bool)\ -> FlowDecision: + if controller_action == STAGE_PREVIOUS: + return 'REDIRECT_PREVIOUS' # For now with GET we do the same sort of things if method == 'GET' and controller_action == STAGE_CURRENT: return 'REDIRECT_NEXT' diff --git a/submit_ce/ui/routes/ui.py b/submit_ce/ui/routes/ui.py index 1ebf7d20..34644790 100644 --- a/submit_ce/ui/routes/ui.py +++ b/submit_ce/ui/routes/ui.py @@ -39,6 +39,14 @@ def redirect_to_login(*args, **kwargs) -> Response: return redirect(url_for('login')) +_SUB_ROUTE_PARENT_STAGE = { + 'file_delete': 'file_upload', + 'file_delete_all': 'file_upload', +} +"""Auxiliary routes that don't have their own workflow stage but live under one. +Used to highlight the parent stage in the progress nav sidebar.""" + + @UI.before_request def load_submission() -> None: """Load the submission before the request is processed.""" @@ -53,7 +61,10 @@ def load_submission() -> None: request.events = events request.workflow = wfp request.current_stage = wfp.current_stage() - request.this_stage = wfp.workflow[endpoint_name()] + endpoint = endpoint_name() + request.this_stage = wfp.workflow[ + _SUB_ROUTE_PARENT_STAGE.get(endpoint, endpoint) + ] @UI.context_processor diff --git a/submit_ce/ui/workflow/conditions.py b/submit_ce/ui/workflow/conditions.py index da208328..c4448ef2 100644 --- a/submit_ce/ui/workflow/conditions.py +++ b/submit_ce/ui/workflow/conditions.py @@ -42,7 +42,9 @@ def has_comment(submission: Submission, events: List[Event]) -> bool: def has_files(submission: Submission, events: List[Event]) -> bool: """Determine if the submission has any files.""" - return submission.uncompressed_size > 0 + return (submission.uncompressed_size > 0 + and submission.source_format not in [SourceFormat.INVALID, SourceFormat.UNKNOWN] + ) def has_valid_content(submission: Submission, events: List[Event]) -> bool: """Determine whether the submitter has uploaded files.""" @@ -90,4 +92,6 @@ def has_directives_started(submission: Submission, events: List[Event]) -> bool: saved and the compile service writes `directives.json`. The event history is the authoritative record that this happened. """ - return any(isinstance(e, StartDirectives) for e in events) + return (submission.source_format == SourceFormat.PDF + or any(isinstance(e, StartDirectives) for e in events) + ) From 172cd918410e76a2ae0cb86200afce4a48dcb17b Mon Sep 17 00:00:00 2001 From: Brian Maltzan <bgm37@cornell.edu> Date: Wed, 10 Jun 2026 06:36:32 -0400 Subject: [PATCH 5/5] minor --- submit_ce/ui/controllers/new/upload.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/submit_ce/ui/controllers/new/upload.py b/submit_ce/ui/controllers/new/upload.py index 4aaa6002..51a1c8f5 100644 --- a/submit_ce/ui/controllers/new/upload.py +++ b/submit_ce/ui/controllers/new/upload.py @@ -155,9 +155,6 @@ def upload_files(method: str, params: MultiDict, session: Session, 'submission': submission, 'form': AddfilesForm()}) - logger.error(f'BRIANM: Submission {submission.submission_id}, {submission.source_format}, {type(submission.source_format)}') - - if method not in ['GET', 'POST']: raise MethodNotAllowed() elif method == 'GET': @@ -248,8 +245,6 @@ def _get_upload(params: MultiDict, session: Session, submission: Submission, else: workspace = current_app.api.get_file_store().get_workspace(submission_id=str(submission.submission_id)) - logger.error(f'BRIANM.2: Submission {submission.submission_id}, {submission.source_format}, {type(submission.source_format)}') - rdata.update({'status': workspace}) if workspace: rdata.update({'immediate_notifications': _get_notifications(submission, workspace)})