diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
new file mode 100644
index 00000000..eeafc4d1
--- /dev/null
+++ b/.github/workflows/python-app.yml
@@ -0,0 +1,40 @@
+# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
+
+name: Python application
+
+on:
+ push:
+ branches: [ "develop" ]
+ pull_request:
+ branches: [ "develop" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install protobuf compiler
+ run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
+
+ - name: Install the project
+ run: uv sync --locked --all-extras --dev
+
+ - name: Test with pytest
+ run: |
+ uv run pytest --ignore=submit_ce/implementations/pubsub/tests \
+ --cov=submit_ce --cov-report=term-missing --cov-fail-under=80
+
+ - name: Ruff lint check
+ run: |
+ uv pip install ruff
+ uv run ruff check --output-format=github submit_ce
+ continue-on-error: true
\ No newline at end of file
diff --git a/clitools.py b/clitools.py
index dbd6585a..742c54a8 100644
--- a/clitools.py
+++ b/clitools.py
@@ -1,15 +1,12 @@
import os
-import tempfile
import time
import subprocess
import random
-from typing import Optional
import fire
def gen_openapi_json(file:str = "openapi.json"):
"""Generate an openapi.yaml file for the current server code."""
- import uvicorn
import requests
port = random.randint(9000, 12000)
command = f"uvicorn submit_ce.api.app:app --host 127.0.0.1 --port {port}".split()
@@ -34,7 +31,7 @@ def gen_client(gen_spec:bool = True):
"""
if gen_spec:
- print(f"* Generating to openapi.json for current code")
+ print("* Generating to openapi.json for current code")
gen_openapi_json()
command = f"""
diff --git a/pyproject.toml b/pyproject.toml
index 64dd50e6..65a092db 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -189,12 +189,88 @@ dev = [
[tool.coverage.run]
branch = true
+relative_files = true
source = ["submit_ce"]
-omit = [ "*/tests/*" ]
+omit = [
+ # Ignore tests
+ "**/tests/**",
+ "**/test_*.py",
+ "**/*_test.py",
+
+ # Ignore questionable code or code in the process of reimplementation
+ # Fastly code not currently used
+ "submit_ce/fastapi/test_*.py",
+ "submit_ce/fastapi/auth.py",
+ "submit_ce/fastapi/routes.py",
+ #
+ "submit_ce/implementations/compile/common.py",
+ "submit_ce/implementations/compile/compile_at_gcp.py", # migrate existing tests or eliminate script
+ "submit_ce/implementations/file_store/gs_file_store.py",
+ "submit_ce/implementations/file_store/legacy_file_store.py",
+ "submit_ce/implementations/legacy_implementation/**",
+ "submit_ce/implementations/pubsub/**", # disabled - omit until feature is enabled
+ "submit_ce/ui/filters/**", # need rewrite
+ "submit_ce/ui/compile_sass.py",
+ "submit_ce/ui/controllers/new/create.py",
+ "submit_ce/ui/controllers/cross.py", # add later
+ "submit_ce/ui/controllers/delete.py", # add tests after reimplementation
+ "submit_ce/ui/controllers/new/process.py",
+ 'submit_ce/ui/controllers/new/review.py', # placeholder
+ "submit_ce/ui/controllers/new/upload.py", # add tests after reimplementation
+ "submit_ce/ui/controllers/new/upload_delete.py"
+
+]
[tool.coverage.report]
+
+omit = [
+ # Ignore tests
+ "**/tests/**",
+ "**/test_*.py",
+ "**/*_test.py",
+
+
+ # Ignore questionable code or code in the process of reimplementation
+ # Fastly code not currently used
+ "submit_ce/fastapi/test_*.py",
+ "submit_ce/fastapi/auth.py",
+ "submit_ce/fastapi/routes.py",
+ "submit_ce/implementations/compile/common.py", # migrate existing tests or eliminate script
+ "submit_ce/implementations/compile/compile_at_gcp.py", # migrate existing tests or eliminate script
+ "submit_ce/implementations/file_store/legacy_file_store.py", # add tests
+ "submit_ce/implementations/file_store/gs_file_store.py", # add tests
+ "submit_ce/implementations/legacy_implementation/**",
+ "submit_ce/implementations/pubsub/**", # disabled - omit until feature is enabled
+ "submit_ce/ui/filters/**", # need rewrite
+ "submit_ce/ui/compile_sass.py",
+ "submit_ce/ui/controllers/new/create.py",
+ "submit_ce/ui/controllers/cross.py",
+ "submit_ce/ui/controllers/delete.py", # add tests after reimplementation
+ "submit_ce/ui/controllers/new/process.py",
+ "submit_ce/ui/controllers/new/review.py", # placeholder
+ "submit_ce/ui/controllers/new/upload.py", # add tests after reimplementation
+ "submit_ce/ui/controllers/new/upload_delete.py"
+]
+
+exclude_lines = [
+ "pragma: no cover",
+ "if TYPE_CHECKING:",
+ "if __name__ == .__main__.:",
+ "raise NotImplementedError",
+ "except ImportError",
+]
+
# Regexes for lines to exclude from consideration
exclude_also = [
# Don't complain about abstract methods, they aren't run:
"@(abc\\.)?abstractmethod",
]
+
+
+# Fail threshold (tune after you see the new baseline)
+fail_under = 80
+skip_covered = true
+
+
+[tool.pytest.ini_options]
+addopts = "-q --maxfail=1 -ra --cov=submit_ce --cov-report=term-missing"
diff --git a/submit_ce/api/domain/annotation.py b/submit_ce/api/domain/annotation.py
index 0119fa0b..b78ffd86 100644
--- a/submit_ce/api/domain/annotation.py
+++ b/submit_ce/api/domain/annotation.py
@@ -7,7 +7,6 @@
from typing import Optional, Union, List, Dict, Type, Any
from dataclasses import field
-from mypy_extensions import TypedDict
from .agent import User, agent_factory
diff --git a/submit_ce/api/domain/compilation.py b/submit_ce/api/domain/compilation.py
index 7f773f32..d9b86fd7 100644
--- a/submit_ce/api/domain/compilation.py
+++ b/submit_ce/api/domain/compilation.py
@@ -1,6 +1,5 @@
"""Data structs related to compilation."""
-import io
from datetime import datetime
from enum import Enum
from typing import Optional
diff --git a/submit_ce/api/domain/event/__init__.py b/submit_ce/api/domain/event/__init__.py
index 775f06b6..88533574 100644
--- a/submit_ce/api/domain/event/__init__.py
+++ b/submit_ce/api/domain/event/__init__.py
@@ -61,6 +61,7 @@
from . import validators
from .base import Event
+from .base import event_factory as make_event
from .flag import AddMetadataFlag, AddUserFlag, AddContentFlag, RemoveFlag, \
AddHold, RemoveHold
from .proposal import AddProposal, RejectProposal, AcceptProposal
diff --git a/submit_ce/api/domain/event/base.py b/submit_ce/api/domain/event/base.py
index 6a23e49e..9bb347e3 100644
--- a/submit_ce/api/domain/event/base.py
+++ b/submit_ce/api/domain/event/base.py
@@ -3,7 +3,6 @@
import copy
import hashlib
from datetime import datetime
-from logging import root
from typing import Optional, Callable, Tuple, Iterable, List, ClassVar, \
Type, Any
diff --git a/submit_ce/api/domain/event/flag.py b/submit_ce/api/domain/event/flag.py
index 7e7df74a..343e0a13 100644
--- a/submit_ce/api/domain/event/flag.py
+++ b/submit_ce/api/domain/event/flag.py
@@ -4,7 +4,6 @@
from dataclasses import field
-from .util import dataclass
from .base import Event
from ..flag import ContentFlag, MetadataFlag, UserFlag
from ..submission import Submission, SubmissionMetadata, Hold, Waiver
diff --git a/submit_ce/api/domain/event/proposal.py b/submit_ce/api/domain/event/proposal.py
index ae36400e..84f6f5f9 100644
--- a/submit_ce/api/domain/event/proposal.py
+++ b/submit_ce/api/domain/event/proposal.py
@@ -1,12 +1,9 @@
"""Commands for working with :class:`.Proposal` instances on submissions."""
-import copy
-from typing import Optional, Iterable
+from typing import Optional
from dataclasses import field
-from .util import dataclass
import logging
-from ..agent import User
from ..submission import Submission
from ..proposal import Proposal
from ..annotation import Comment
diff --git a/submit_ce/api/domain/event/request.py b/submit_ce/api/domain/event/request.py
index 7f0a98cf..ad87473d 100644
--- a/submit_ce/api/domain/event/request.py
+++ b/submit_ce/api/domain/event/request.py
@@ -2,9 +2,7 @@
from typing import Optional, List, ClassVar
from dataclasses import field
-from .util import dataclass
-from arxiv import taxonomy
from . import validators
from .base import Event
diff --git a/submit_ce/api/domain/event/tests/test_branches.py b/submit_ce/api/domain/event/tests/test_branches.py
new file mode 100644
index 00000000..dc4abb05
--- /dev/null
+++ b/submit_ce/api/domain/event/tests/test_branches.py
@@ -0,0 +1,71 @@
+# Minimal branch-coverage tests for event creation and validation.
+
+from datetime import datetime
+from pytz import UTC
+import pytest
+
+
+from submit_ce.api.domain import submission as submod, agent
+from submit_ce.api.domain.event import (
+ make_event, # <- alias to base.event_factory
+ FinalizeSubmission, # used directly to hit validation error
+ Announce, # simple project() path
+ InvalidEvent, # domain exception
+)
+
+
+def _now():
+ # Keep consistent with existing tests that use pytz.UTC
+ return datetime.now(UTC)
+
+
+def _user(u="u1"):
+ # PublicUser requires: name, user_id, email.
+ # endorsements defaults to [] if not provided.
+ return agent.PublicUser(
+ name="Test User",
+ user_id=u,
+ email=f"{u}@example.org",
+ endorsements=[],
+ )
+
+
+def test_create_submission_round_trip():
+ """CreateSubmission via factory; apply(None) creates a new Submission."""
+ creator = _user("alice")
+ ev = make_event("CreateSubmission", created=_now(), creator=creator)
+ after = ev.apply(None) # special-case: creation works with submission=None
+ assert after.creator == creator
+ assert after.owner == creator
+ # ID may be None pre-persist; just assert it’s stable/typed if present.
+ assert after.submission_id is None or isinstance(after.submission_id, int)
+
+
+def test_finalize_submission_missing_required_fields_raises():
+ """FinalizeSubmission should raise when required fields are missing."""
+ creator = _user("bob")
+ sub = submod.Submission(creator=creator, owner=creator, created=_now())
+ ev = FinalizeSubmission(creator=creator, created=_now())
+ with pytest.raises(InvalidEvent):
+ ev.apply(sub)
+
+
+def test_announce_sets_status_and_id():
+ """Announce.project sets status to ANNOUNCED and arxiv_id to provided value."""
+ creator = _user("carol")
+ # A minimal submission that is not yet announced
+ sub = submod.Submission(creator=creator, owner=creator, created=_now())
+ # The versions field is commented out in Submission while Announce appends
+ # to it. [FIX ME]
+ sub.versions = []
+
+ ev = Announce(creator=creator, created=_now(), arxiv_id="2501.01234")
+ after = ev.apply(sub)
+ assert after.status == submod.Submission.ANNOUNCED
+ assert after.arxiv_id == "2501.01234"
+
+
+def test_factory_unknown_type_raises_runtime_error():
+ """Factory should error with an unknown event type name."""
+ with pytest.raises(RuntimeError):
+ make_event("NoSuchEventType", created=_now(), creator=_user("dave"))
\ No newline at end of file
diff --git a/submit_ce/api/domain/event/tests/test_event_edge_paths.py b/submit_ce/api/domain/event/tests/test_event_edge_paths.py
new file mode 100644
index 00000000..e6a1f6fb
--- /dev/null
+++ b/submit_ce/api/domain/event/tests/test_event_edge_paths.py
@@ -0,0 +1,294 @@
+"""
+Additional edge-path coverage for the event domain.
+
+Focus
+-----
+Hit validation/error branches that were still untested in
+submit_ce/api/domain/event/__init__.py:
+
+- ConfirmPreview: no preview / checksum mismatch / correct checksum
+- CreateSubmissionVersion: .validate requires an announced submission
+- Rollback: version==1 (delete), version>1 with history, invalid scenarios
+- SetReportNumber: invalid vs. valid formats
+"""
+
+from datetime import datetime
+from pytz import UTC
+import copy
+import pytest
+
+# Domain models and helpers
+from submit_ce.api.domain import submission as submod, agent
+from submit_ce.api.domain.preview import Preview
+from submit_ce.api.domain.submission import Submission
+
+# Event classes (and exception)
+from submit_ce.api.domain.event import (
+ ConfirmPreview,
+ CreateSubmissionVersion,
+ FinalizeSubmission,
+ RemoveSecondaryClassification,
+ Rollback,
+ SetAbstract,
+ SetLicense,
+ SetReportNumber,
+ SetTitle,
+ InvalidEvent,
+)
+
+
+# -----------------------------
+# Local helpers (tiny fixtures)
+# -----------------------------
+
+# UTC 'now' for created/submitted timestamps.
+def _now():
+ return datetime.now(UTC)
+
+# Minimal PublicUser
+def _user(uid: str = "u1"):
+ return agent.PublicUser(
+ name="Test User",
+ user_id=uid,
+ email=f"{uid}@example.org",
+ endorsements=[],
+ )
+
+# Minimal "working" submission (unannounced)
+def _working_submission(uid: str = "u1"):
+ u = _user(uid)
+ return submod.Submission(creator=u, owner=u, created=_now())
+
+# Minimal "announced" submission (has arxiv_id + ANNOUNCED status)
+def _announced_submission(uid: str = "u1", arxiv_id: str = "2501.01234"):
+ s = _working_submission(uid)
+ s.arxiv_id = arxiv_id
+ s.status = submod.Submission.ANNOUNCED
+ # event.Announce expects .versions to exist, but for these tests we only
+ # need to represent an already-announced state, not run Announce.
+ s.versions = [copy.deepcopy(s)]
+ return s
+
+# -------------------------------------------------------
+# ConfirmPreview: three cases (no preview / mismatch / ok)
+# -------------------------------------------------------
+
+def test_confirm_preview_fails_when_no_preview():
+ """
+ ConfirmPreview should fail if submission.preview is None.
+ """
+ s = _working_submission()
+ e = ConfirmPreview(creator=s.creator, created=_now(), preview_checksum="abc123")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+def test_confirm_preview_fails_on_checksum_mismatch():
+ """
+ ConfirmPreview should fail if provided preview_checksum != submission.preview.preview_checksum.
+ """
+ s = _working_submission()
+ s.preview = Preview(
+ source_id=1,
+ source_checksum="SRC",
+ preview_checksum="EXPECTED",
+ size_bytes=100,
+ added=_now(),
+ )
+ e = ConfirmPreview(creator=s.creator, created=_now(), preview_checksum="WRONG")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+def test_confirm_preview_succeeds_on_checksum_match_sets_flag():
+ """
+ ConfirmPreview should pass when checksums match and set submitter_confirmed_preview.
+ """
+ s = _working_submission()
+ s.preview = Preview(
+ source_id=1,
+ source_checksum="SRC",
+ preview_checksum="MATCH",
+ size_bytes=100,
+ added=_now(),
+ )
+ e = ConfirmPreview(creator=s.creator, created=_now(), preview_checksum="MATCH")
+ # validate should not raise
+ e.validate(s)
+ # apply should toggle the flag
+ after = e.apply(s)
+ assert after.submitter_confirmed_preview is True
+
+
+# -------------------------------------------------------
+# CreateSubmissionVersion: requires announced submission
+# -------------------------------------------------------
+
+def test_create_submission_version_rejects_unannounced():
+ """
+ CreateSubmissionVersion.validate requires submission.is_announced.
+ """
+ s = _working_submission()
+ e = CreateSubmissionVersion(creator=s.creator, created=_now())
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+def test_create_submission_version_succeeds_when_announced():
+ """
+ CreateSubmissionVersion should pass validate for announced submissions
+ and yield a new working version on apply().
+ """
+ s = _announced_submission()
+ e = CreateSubmissionVersion(creator=s.creator, created=_now())
+ # validate should not raise
+ e.validate(s)
+ # apply should move to a new version and set status to WORKING
+ after = e.apply(s)
+ assert after.version == s.version + 1
+ assert after.status == submod.Submission.WORKING
+ # and un-set fields like preview confirmation
+ assert after.submitter_confirmed_preview is False
+
+# -------------------------------------------------------
+# FinalizeSubmission
+# -------------------------------------------------------
+def test_finalize_missing_required_fields():
+ s = _working_submission()
+ e = FinalizeSubmission(creator=s.creator)
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # REQUIRED / REQUIRED_METADATA guard
+
+# -------------------------------------------------------
+# RemoveSecondaryClassification
+# -------------------------------------------------------
+def test_remove_secondary_missing_fails():
+ s = _working_submission()
+ # category not yet added → _must_already_be_present should fail
+ e = RemoveSecondaryClassification(creator=s.creator, category="cs.AI")
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # "No such category on submission"
+
+# -------------------------------------------------------
+# Rollback: version==1 -> delete; version>1 with history -> revert
+# -------------------------------------------------------
+
+def test_rollback_invalid_when_announced():
+ s = _announced_submission()
+ e = Rollback(creator=s.creator)
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # "Cannot already be announced"
+
+def test_rollback_on_first_version_deletes_submission():
+ """
+ Rollback.project with version==1 should set status=DELETED.
+ """
+ s = _working_submission()
+ s.version = 1
+ e = Rollback(creator=s.creator, created=_now())
+ # validate: requires unannounced (is true for working)
+ e.validate(s)
+ after = e.apply(s)
+ assert after.status == submod.Submission.DELETED
+
+def test_rollback_to_previous_announced_version():
+ """
+ Rollback.project on version>1 should step back to last snapshot in .versions.
+ """
+ s = _announced_submission()
+ # simulate we're now on version 2 with prior announced snapshot saved
+ s.status = submod.Submission.WORKING
+ s.version = 2
+ # Add a previous announced snapshot as in Announce
+ s.versions = [copy.deepcopy(s)]
+ s.versions[0].status = submod.Submission.ANNOUNCED
+ e = Rollback(creator=s.creator, created=_now())
+ e.validate(s)
+ after = e.apply(s)
+ # Should have decremented version and restored announced status
+ assert after.version == 1
+ assert after.status == submod.Submission.ANNOUNCED
+
+def test_rollback_version1_sets_deleted():
+ s = _working_submission()
+ s.version = 1
+ s.status = Submission.WORKING
+ e = Rollback(creator=s.creator)
+ e.validate(s)
+ out = e.project(s)
+ assert out.status == Submission.DELETED
+
+# -------------------------------------------------------
+# SetAbstract
+# -------------------------------------------------------
+def test_abstract_too_short_fails():
+ s = _working_submission()
+ e = SetAbstract(creator=s.creator, abstract="short")
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # MIN_LENGTH branch
+
+def test_abstract_valid_passes():
+ s = _working_submission()
+ e = SetAbstract(creator=s.creator, abstract="This abstract is just long enough")
+ e.validate(s)
+ s2 = e.project(s)
+ assert s2.metadata.abstract == "This abstract is just long enough"
+
+# -------------------------------------------------------
+# SetLicense
+# -------------------------------------------------------
+def test_license_requires_url():
+ s = _working_submission()
+ e = SetLicense(creator=s.creator, license_name="CC BY 4.0", license_uri="")
+ with pytest.raises(InvalidEvent):
+ e.validate(
+ s) # "License must have a URL"
+
+def test_license_valid_url():
+ # Use a URI present in LICENSES and current; pick one from your config
+ s = _working_submission()
+ e = SetLicense(creator=s.creator, license_name="CC BY 4.0",
+ license_uri="http://creativecommons.org/licenses/by/4.0/")
+ e.validate(s) # passes if LICENSES marks it current
+
+# -------------------------------------------------------
+# SetReportNumber: invalid vs. valid formats
+# -------------------------------------------------------
+
+def test_set_report_number_rejects_invalid_value():
+ """
+ SetReportNumber.validate requires at least two consecutive digits in the value.
+ """
+ s = _working_submission()
+ e = SetReportNumber(creator=s.creator, report_num="not a report number")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+def test_set_report_number_accepts_common_formats():
+ """
+ SetReportNumber.validate accepts values with consecutive digits (e.g. '1003.1130').
+ """
+ s = _working_submission()
+ e = SetReportNumber(creator=s.creator, report_num="CORNELL-1003-1130")
+ # Should not raise
+ e.validate(s)
+ after = e.apply(s)
+ assert after.metadata.report_num == "CORNELL-1003-1130"
+
+# -------------------------------------------------------
+# SetTitle
+# -------------------------------------------------------
+def test_title_allows_basic_tags():
+ s = _working_submission()
+ e = SetTitle(creator=s.creator, title="Hello
World")
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # No HTML tags are allowed
+
+def test_title_rejects_disallowed_html():
+ s = _working_submission()
+ e = SetTitle(creator=s.creator, title="")
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # _check_for_html branch
+
+def test_title_trailing_period_rule():
+ s = _working_submission()
+ e = SetTitle(creator=s.creator, title="Hello world.")
+ with pytest.raises(InvalidEvent):
+ e.validate(s) # validators.no_trailing_period
\ No newline at end of file
diff --git a/submit_ce/api/domain/event/tests/test_event_util.py b/submit_ce/api/domain/event/tests/test_event_util.py
new file mode 100644
index 00000000..218504d4
--- /dev/null
+++ b/submit_ce/api/domain/event/tests/test_event_util.py
@@ -0,0 +1,74 @@
+"""Test util.py under api/domain/event"""
+import pytest
+import dataclasses
+import submit_ce.api.domain.event.util as event_util
+
+#
+# 1) dataclass() with NO kwargs: should wrap base dataclass and then install __hash__/__eq__
+#
+
+def test_event_dataclass_without_kwargs_sets_hash_and_eq():
+ @event_util.dataclass() # no kwargs branch inside util.dataclass # [event.util]
+ class E:
+ event_id: str
+ x: int = 0
+
+ a = E(event_id="A", x=1)
+ b = E(event_id="A", x=99) # same event_id -> same hash, equal
+ c = E(event_id="C", x=1) # different event_id -> different hash, not equal
+
+ # __hash__ should be derived from event_id
+ assert hash(a) == hash(b)
+ assert hash(a) != hash(c)
+
+ # __eq__ uses event_util.event_eq, which compares hashes
+ assert a == b
+ assert a != c
+
+#
+# 2) dataclass() WITH kwargs: should honor kwargs (e.g., frozen=True) and still install __hash__/__eq__
+#
+
+def test_event_dataclass_with_kwargs_preserves_kwargs_and_sets_hash_eq():
+ @event_util.dataclass(frozen=True) # kwargs branch
+ class E:
+ event_id: str
+ y: int = 0
+
+ e1 = E(event_id="Z", y=1)
+ # frozen=True should make the instance immutable
+ with pytest.raises(dataclasses.FrozenInstanceError):
+ e1.y = 2 # type: ignore[attr-defined]
+
+ # __hash__ / __eq__ still installed
+ e2 = E(event_id="Z", y=999)
+ e3 = E(event_id="OTHER")
+ assert hash(e1) == hash(e2) and e1 == e2
+ assert hash(e1) != hash(e3) and e1 != e3
+
+#
+# 3) event_hash: explicitly uses instance.event_id
+#
+
+def test_event_hash_uses_event_id():
+ class Dummy:
+ def __init__(self, eid): self.event_id = eid
+ d1, d2 = Dummy("K"), Dummy("K")
+ assert event_util.event_hash(d1) == event_util.event_hash(d2) # same event_id
+
+#
+# 4) event_eq compares hashes, not types or fields
+#
+
+def test_event_eq_compares_hashes_not_types():
+ @event_util.dataclass()
+ class E:
+ event_id: str
+
+ class Other:
+ # Make it hash to the same value as E("SAME")
+ def __hash__(self): return hash("SAME")
+
+ e = E(event_id="SAME")
+ o = Other()
+ assert event_util.event_eq(e, o) # equal because hashes match
\ No newline at end of file
diff --git a/submit_ce/api/domain/event/tests/test_events.py b/submit_ce/api/domain/event/tests/test_events.py
index a3ed3ec5..1cdfa685 100644
--- a/submit_ce/api/domain/event/tests/test_events.py
+++ b/submit_ce/api/domain/event/tests/test_events.py
@@ -7,7 +7,6 @@
from pytz import UTC
from mimesis import Text
-from arxiv.metadata import metacheck
from submit_ce.api.domain import event, agent, submission, meta
from submit_ce.api.exceptions import InvalidEvent
diff --git a/submit_ce/api/domain/event/tests/test_more_event_branches.py b/submit_ce/api/domain/event/tests/test_more_event_branches.py
new file mode 100644
index 00000000..13b75d40
--- /dev/null
+++ b/submit_ce/api/domain/event/tests/test_more_event_branches.py
@@ -0,0 +1,187 @@
+"""
+Focused branch tests for the event domain.
+
+The event module is large and has many validation branches. This file contains
+additional unit tests for the event domain.
+
+Design notes
+------------
+- Use the package-level 'make_event' alias for factory-based creation.
+- Each test exercises *one* validation idea. When possible, we check both the
+ “reject” and the “accept” path for clarity.
+"""
+
+# Use same style as existing tests.
+from datetime import datetime
+from pytz import UTC
+
+# Core domain models used by these tests.
+from submit_ce.api.domain import submission as submod, meta, agent
+
+# Event classes (and exception) we target for branch coverage.
+from submit_ce.api.domain.event import (
+ SetTitle,
+ SetAbstract,
+ SetLicense,
+ RemoveSecondaryClassification,
+ FinalizeSubmission,
+ InvalidEvent,
+)
+
+import pytest
+
+
+# -----------------------------
+# Test utilities (tiny helpers)
+# -----------------------------
+
+# Return a timezone-aware 'now'.
+def _now():
+ # Using pytz.UTC.
+ return datetime.now(UTC)
+
+
+# Construct a minimal PublicUser acceptable to the domain.
+def _user(uid: str = "u1"):
+ # PublicUser requires name, user_id, email; endorsements defaults to [].
+ return agent.PublicUser(
+ name="Test User",
+ user_id=uid,
+ email=f"{uid}@example.org",
+ endorsements=[],
+ )
+
+
+# Construct a minimal Submission used by most tests below.
+def _blank_submission(uid: str = "u1"):
+ u = _user(uid)
+ return submod.Submission(
+ creator=u,
+ owner=u,
+ created=_now(),
+ )
+
+
+# -------------------------------------------------------
+# Tests: small, focused validations in event/__init__.py
+# -------------------------------------------------------
+
+def test_set_title_rejects_all_caps():
+ """
+ SetTitle should reject titles that are entirely uppercase.
+
+ Why: The event validation explicitly checks for all-caps titles.
+ Expectation: InvalidEvent is raised by .validate(submission).
+ """
+ s = _blank_submission()
+ e = SetTitle(creator=s.creator, title="ALL CAPS TITLE")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+
+def test_set_title_rejects_trailing_period():
+ """
+ SetTitle should reject titles ending with a trailing period.
+
+ Why: Title validation includes a "no trailing '.'" rule.
+ Expectation: InvalidEvent is raised by .validate(submission).
+ """
+ s = _blank_submission()
+ e = SetTitle(creator=s.creator, title="Ends with period.")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+
+def test_set_abstract_length_bounds_both_paths():
+ """
+ SetAbstract length rules: too short -> reject; reasonable -> accept.
+
+ Why: MIN_LENGTH and MAX_LENGTH constraints are enforced in validation.
+ Expectation:
+ - too short: InvalidEvent
+ - reasonable length: validate() does not raise
+ """
+ s = _blank_submission()
+
+ # Too short: MIN_LENGTH is 20, so this should fail.
+ e_short = SetAbstract(creator=s.creator, abstract="too short")
+ with pytest.raises(InvalidEvent):
+ e_short.validate(s)
+
+ # Reasonable: 25 chars satisfies the minimum.
+ ok_text = "This abstract is valid length."
+ e_ok = SetAbstract(creator=s.creator, abstract=ok_text)
+ e_ok.validate(s) # no exception means the branch was accepted
+
+
+def test_set_license_rejects_invalid_uri():
+ """
+ SetLicense should reject license URIs not present in the allowed set.
+
+ Why: The validator cross-checks the URI against the current LICENSES list.
+ Expectation: InvalidEvent is raised by .validate(submission).
+ """
+ s = _blank_submission()
+ e = SetLicense(creator=s.creator, license_uri="http://not-on-our-list")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+
+def test_abstract_rejects_when_not_capitalized():
+ """
+ Abstracts must start with a capital letter.
+ Expect InvalidEvent when the first character is lowercase.
+ """
+ s = _blank_submission()
+ e = SetAbstract(creator=s.creator, abstract="not capitalized first sentence.")
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+
+def test_abstract_rejects_when_too_long():
+ """
+ Abstracts longer than MAX_LENGTH should be rejected.
+ Your existing SetAbstract enforces MAX_LENGTH=1920.
+ """
+ s = _blank_submission()
+ # Start with a capital letter to isolate the length failure.
+ too_long = "A" + ("x" * 2000)
+ e = SetAbstract(creator=s.creator, abstract=too_long)
+ with pytest.raises(InvalidEvent):
+ e.validate(s)
+
+
+def test_remove_secondary_requires_existing_category_then_accepts():
+ """
+ RemoveSecondaryClassification requires the category to already be present.
+
+ Why: Validation checks that the category exists among secondary classifications.
+ Expectation:
+ - When missing: InvalidEvent
+ - After adding: validate() does not raise
+ """
+ s = _blank_submission()
+
+ # Missing category -> should raise
+ e_missing = RemoveSecondaryClassification(creator=s.creator, category="cond-mat.dis-nn")
+ with pytest.raises(InvalidEvent):
+ e_missing.validate(s)
+
+ # Add the category, then validate again -> should pass
+ s.secondary_classification.append(meta.Classification("cond-mat.dis-nn"))
+ e_present = RemoveSecondaryClassification(creator=s.creator, category="cond-mat.dis-nn")
+ e_present.validate(s)
+
+
+def test_finalize_submission_missing_required_fields():
+ """
+ FinalizeSubmission must see required fields populated on the Submission.
+
+ Why: FinalizeSubmission.validate checks multiple required properties.
+ Expectation: On a bare/minimal Submission, validate/apply should raise InvalidEvent.
+ """
+ s = _blank_submission()
+ e = FinalizeSubmission(creator=s.creator, created=_now())
+ with pytest.raises(InvalidEvent):
+ e.apply(s) # .apply() triggers .validate() internally
+
diff --git a/submit_ce/api/domain/submission.py b/submit_ce/api/domain/submission.py
index 1958759a..29c168ca 100644
--- a/submit_ce/api/domain/submission.py
+++ b/submit_ce/api/domain/submission.py
@@ -3,18 +3,16 @@
import hashlib
from enum import Enum
from datetime import datetime
-from dateutil.parser import parse as parse_date
from typing import Optional, Dict, List, Iterable, Set, Any
from dataclasses import dataclass, field
from .agent import Client, User, agent_factory
-from .annotation import Comment, Feature, Annotation, annotation_factory
-from .flag import Flag, flag_factory
+from .annotation import Comment, Feature, Annotation
+from .flag import Flag
from .meta import License, Classification
from .preview import Preview
from .process import ProcessStatus
-from .proposal import Proposal
from .util import get_tzaware_utc_now
diff --git a/submit_ce/api/domain/tests/test_domain_util.py b/submit_ce/api/domain/tests/test_domain_util.py
new file mode 100644
index 00000000..b6a36487
--- /dev/null
+++ b/submit_ce/api/domain/tests/test_domain_util.py
@@ -0,0 +1,34 @@
+"""Test util.py under api/domain"""
+import datetime as _dt
+
+from submit_ce.api.domain.util import (
+ get_tzaware_utc_now,
+ dict_coerce,
+ list_coerce,
+)
+
+def test_get_tzaware_utc_now_is_aware_and_utc():
+ now = get_tzaware_utc_now() # should be tz-aware in UTC
+ assert now.tzinfo is not None
+ assert now.utcoffset() == _dt.timedelta(0)
+
+def test_dict_coerce_mixed_items():
+ # factory that consumes dicts only
+ def factory(**kw):
+ return ("ok", kw["a"] + kw.get("b", 0))
+
+ data = {
+ "e1": {"a": 1, "b": 2}, # will be coerced via factory(**value)
+ "e2": 42, # left as-is (non-dict branch)
+ }
+ out = dict_coerce(factory, data)
+ assert out["e1"] == ("ok", 3)
+ assert out["e2"] == 42 # not coerced
+
+def test_list_coerce_filters_and_coerces_only_dicts():
+ def factory(**kw):
+ return kw["x"] * 10
+
+ data = [{"x": 3}, "skip-me", {"x": 7}, 99]
+ out = list_coerce(factory, data) # includes only dict items
+ assert out == [30, 70]
\ No newline at end of file
diff --git a/submit_ce/api/domain/tests/test_uploads_roundtrip.py b/submit_ce/api/domain/tests/test_uploads_roundtrip.py
new file mode 100644
index 00000000..5d52a6c6
--- /dev/null
+++ b/submit_ce/api/domain/tests/test_uploads_roundtrip.py
@@ -0,0 +1,174 @@
+"""
+Coverage-focused tests for submit_ce.api.domain.uploads
+
+What these tests cover
+----------------------
+- FileError: to_dict()/from_dict() roundtrip
+- FileStatus: to_dict()/from_dict() roundtrip, including:
+ * 'modified' as ISO string → parsed into datetime
+ * nested 'errors' list as dicts → FileError objects
+- Upload:
+ * file_count property
+ * to_dict()/from_dict() roundtrip
+ * all four timestamp fields handled as strings on input
+ * 'source_format' string converted to SubmissionContent.Format enum
+
+The 'uploads' module primarily provides data structures and conversion helpers.
+Exercising both serialization and deserialization paths touches the majority
+of uncovered lines in this module without requiring any external services.
+"""
+
+# -----------------------------
+# Imports
+# -----------------------------
+
+from datetime import datetime, timezone
+
+from submit_ce.api.domain.uploads import (
+ FileErrorLevels,
+ FileError,
+ FileStatus,
+ UploadStatus,
+ UploadLifecycleStates,
+ Upload,
+)
+from submit_ce.api.domain.submission import SubmissionContent
+
+
+# -----------------------------
+# Small helper (tz-aware 'now')
+# -----------------------------
+
+def _now():
+ # Use native tz-aware datetimes (UTC) to align with module expectations.
+ return datetime.now(timezone.utc)
+
+
+# ------------------------------------------------------------
+# FileError: verify dict round-trip and field value integrity
+# ------------------------------------------------------------
+
+def test_file_error_roundtrip_dict():
+ # build an error
+ err = FileError(
+ error_type=FileErrorLevels.ERROR,
+ message="bad file",
+ more_info="explanation",
+ )
+
+ # Convert to dict, then back to object using the module's helpers.
+ as_dict = err.to_dict()
+ restored = FileError.from_dict(as_dict)
+
+ # Equality semantics: NamedTuple compares field-by-field.
+ assert restored == err
+ # And .to_dict preserves the enum instance (module uses enum, not .value)
+ assert as_dict["error_type"] == FileErrorLevels.ERROR
+
+
+# -------------------------------------------------------------------------
+# FileStatus: verify dict round-trip with string 'modified' and nested errors
+# -------------------------------------------------------------------------
+
+def test_file_status_roundtrip_with_string_modified_and_errors():
+ # Prepare input dict in the *shape that from_dict expects*:
+ # - 'modified' provided as ISO string → should be parsed to datetime
+ # - 'errors' provided as list of dicts → should become FileError objects
+ input_dict = {
+ "path": "/workspace/paper",
+ "name": "paper.tex",
+ "file_type": "text/x-tex",
+ "size": 1234,
+ "modified": _now().isoformat(),
+ "ancillary": False,
+ "errors": [
+ {
+ "error_type": FileErrorLevels.WARNING,
+ "message": "suspicious macro",
+ "more_info": "line 42",
+ }
+ ],
+ }
+
+ # from_dict should parse the string datetime and map dicts → FileError objects.
+ status = FileStatus.from_dict(input_dict)
+
+ # Now go the other direction; to_dict should:
+ # - emit modified as ISO string
+ # - convert FileError objects back to dicts
+ roundtrip_dict = status.to_dict()
+
+ # Check essential fields survived the roundtrip.
+ assert roundtrip_dict["path"] == input_dict["path"]
+ assert roundtrip_dict["name"] == input_dict["name"]
+ assert roundtrip_dict["file_type"] == input_dict["file_type"]
+ assert roundtrip_dict["size"] == input_dict["size"]
+ assert isinstance(status.modified, datetime)
+ assert isinstance(status.errors[0], FileError)
+ assert roundtrip_dict["errors"][0]["message"] == "suspicious macro"
+
+
+# ---------------------------------------------------------------------
+# Upload: verify file_count and full nested round-trip with conversions
+# ---------------------------------------------------------------------
+
+def test_upload_roundtrip_with_nested_status_and_errors_and_conversions():
+ # Build a nested FileStatus (already in object form).
+ nested_status = FileStatus(
+ path="/workspace/paper",
+ name="paper.tex",
+ file_type="text/x-tex",
+ size=2048,
+ modified=_now(),
+ ancillary=False,
+ errors=[
+ FileError(FileErrorLevels.WARNING, "minor", "ok to proceed")
+ ],
+ )
+
+ # Construct an Upload object with enums and datetimes.
+ up = Upload(
+ started=_now(),
+ completed=_now(),
+ created=_now(),
+ modified=_now(),
+ status=UploadStatus.READY,
+ lifecycle=UploadLifecycleStates.ACTIVE,
+ locked=False,
+ identifier="upload-123",
+ source_format=SubmissionContent.Format.PDF, # enum
+ checksum="abc123",
+ size=4096,
+ compressed_size=1024,
+ files=[nested_status],
+ errors=[
+ FileError(FileErrorLevels.ERROR, "fatal", "stop here")
+ ],
+ )
+
+ # Sanity: file_count reflects the files list length.
+ assert up.file_count == 1
+
+ # Convert to dict; enums become .value, timestamps become ISO strings, and
+ # nested objects are converted to dicts.
+ up_dict = up.to_dict()
+
+ # Now modify dict to resemble typical JSON inbound payload where:
+ # - timestamps are strings (already true)
+ # - 'source_format' is an enum value string (already true)
+ # - nested lists are dicts (already true)
+ #
+ # Reconstruct Upload from the dict. from_dict should:
+ # - parse all four timestamp strings → datetime
+ # - convert source_format string → SubmissionContent.Format enum
+ # - map nested file/error dicts back to objects
+ restored = Upload.from_dict(up_dict)
+
+ # Verify key properties and nested structures survived the round-trip.
+ assert restored.status == UploadStatus.READY.value
+ assert restored.lifecycle == UploadLifecycleStates.ACTIVE.value
+ assert restored.source_format == SubmissionContent.Format.PDF
+ assert isinstance(restored.started, datetime)
+ assert isinstance(restored.files[0], FileStatus)
+ assert isinstance(restored.errors[0], FileError)
+
diff --git a/submit_ce/fastapi/config.py b/submit_ce/fastapi/config.py
index c2c87aa4..10637da8 100644
--- a/submit_ce/fastapi/config.py
+++ b/submit_ce/fastapi/config.py
@@ -1,11 +1,8 @@
-import os
-import secrets
from pydantic_settings import BaseSettings
from pydantic import SecretStr, ImportString
-from submit_ce.implementations import NullImplementation
DEV_SQLITE_FILE = "legacy.db"
class Settings(BaseSettings):
diff --git a/submit_ce/fastapi/routes.py b/submit_ce/fastapi/routes.py
index 5e49920a..123b4eae 100644
--- a/submit_ce/fastapi/routes.py
+++ b/submit_ce/fastapi/routes.py
@@ -22,8 +22,7 @@
)
from submit_ce.fastapi.auth import get_user, get_client
-from submit_ce.api import SubmitApi
-from submit_ce.api.domain import Submission, Event, User, Event, License, User, Client, Upload
+from submit_ce.api.domain import Submission, Event, Upload
from submit_ce.api.domain.process import ProcessStatus
# if not isinstance(config.submission_api_implementation, ImplementationConfig):
diff --git a/submit_ce/fastapi/test_default_api.py b/submit_ce/fastapi/test_default_api.py
index 0729aefd..5f121a51 100644
--- a/submit_ce/fastapi/test_default_api.py
+++ b/submit_ce/fastapi/test_default_api.py
@@ -71,7 +71,7 @@ def test_submission_id_accept_policy_post(client: TestClient):
response = client.request(
"POST",
- f"/v1/submission/888888/acceptPolicy",
+ "/v1/submission/888888/acceptPolicy",
headers=headers,
json={"accepted_policy_id": 3})
assert response.status_code == 404
@@ -228,7 +228,7 @@ def test_basic_submission(client: TestClient):
assert response.status_code == 200 or response.text == ""
- response = client.request("GET", f"/v1/user_submissions",)
+ response = client.request("GET", "/v1/user_submissions",)
assert response.status_code == 200 or response.content == ""
json = response.json()
assert isinstance(json, list) and json
diff --git a/submit_ce/implementations/compile/compile_at_gcp.py b/submit_ce/implementations/compile/compile_at_gcp.py
index 91b34fef..df5182fe 100644
--- a/submit_ce/implementations/compile/compile_at_gcp.py
+++ b/submit_ce/implementations/compile/compile_at_gcp.py
@@ -17,7 +17,6 @@
import time
import logging
import stat
-import requests
import httpx
import urllib.parse
from typing import List, Optional
@@ -237,7 +236,7 @@ def process_metadata_and_log(submission_dir, json_log_run_data, output_files_dir
"metadata: %s", new_log_path)
with open(new_log_path, "w") as f:
- f.write(f"Compilation Summary")
+ f.write("Compilation Summary")
converters = json_data.get("converters", [])
num_conversions = len(converters)
num_failed = sum(1 for c in converters if isinstance(c, dict) and c.get("status") == "fail")
@@ -291,7 +290,7 @@ def process_metadata_and_log(submission_dir, json_log_run_data, output_files_dir
f.write(f"\nOur system has compiled the above LaTeX file "
f"into a single PDF document: {final_pdf_file}.\n\n")
else:
- f.write(f"\nOur system failed to generate a PDF.\n\n")
+ f.write("\nOur system failed to generate a PDF.\n\n")
# f.write(f"Selected Errors and Warnings\n")
@@ -482,7 +481,7 @@ def compile_submission(
query_params['preflight'] = args.preflight
if not tex2pdf_url:
- raise FileNotFoundError(f"The tex2pdf_url is required. ")
+ raise FileNotFoundError("The tex2pdf_url is required. ")
url = f'{tex2pdf_url}/convert/?{urllib.parse.urlencode(query_params)}'
logger.info("TeX2PDF request url '%s'", url)
diff --git a/submit_ce/implementations/compile/compile_at_gcp_service.py b/submit_ce/implementations/compile/compile_at_gcp_service.py
index e2bb5b2f..866a6570 100644
--- a/submit_ce/implementations/compile/compile_at_gcp_service.py
+++ b/submit_ce/implementations/compile/compile_at_gcp_service.py
@@ -1,7 +1,4 @@
-import logging
-import os.path
from datetime import timezone, datetime
-from enum import Enum
from typing import Optional
from zoneinfo import ZoneInfo
diff --git a/submit_ce/implementations/file_store/legacy_file_store.py b/submit_ce/implementations/file_store/legacy_file_store.py
index cd4ac30e..06b44ed6 100644
--- a/submit_ce/implementations/file_store/legacy_file_store.py
+++ b/submit_ce/implementations/file_store/legacy_file_store.py
@@ -1,9 +1,8 @@
import os
import shutil
from datetime import datetime, timezone
-from io import BytesIO
from pathlib import Path
-from typing import IO, Optional, List
+from typing import IO, List
from subprocess import Popen
from hashlib import md5
from base64 import urlsafe_b64encode
diff --git a/submit_ce/implementations/legacy_implementation/__init__.py b/submit_ce/implementations/legacy_implementation/__init__.py
index 1a2ce841..12eb7758 100644
--- a/submit_ce/implementations/legacy_implementation/__init__.py
+++ b/submit_ce/implementations/legacy_implementation/__init__.py
@@ -121,7 +121,7 @@ def _save(self, *events,
event.created = datetime.now(UTC)
if isinstance(event, EventWithSideEffect):
if event.executed:
- raise RuntimeError(f"Must not save and execute an already executed event. "
+ raise RuntimeError("Must not save and execute an already executed event. "
"{event.event_id} {event.NAME} executed {event.executed}")
logger.debug('Execute event %s: %s', event.event_id, event.NAME)
event.execute(self, submission)
diff --git a/submit_ce/implementations/legacy_implementation/db.py b/submit_ce/implementations/legacy_implementation/db.py
index 5d535bab..c5450be6 100644
--- a/submit_ce/implementations/legacy_implementation/db.py
+++ b/submit_ce/implementations/legacy_implementation/db.py
@@ -53,7 +53,7 @@
from .models import DBEvent
from .patch import patch_hold
from ...api import domain
-from ...api.domain import Event, Submission, User, User, WithdrawalRequest, CrossListClassificationRequest, Client
+from ...api.domain import Event, Submission, User, WithdrawalRequest, CrossListClassificationRequest
from ...api.domain import License
from ...api.domain.event import SetJournalReference, SetDOI, SetReportNumber, CreateSubmission, Rollback, \
RequestWithdrawal, RequestCrossList, CancelRequest
diff --git a/submit_ce/implementations/legacy_implementation/interpolate.py b/submit_ce/implementations/legacy_implementation/interpolate.py
index 9856afb6..6604224e 100644
--- a/submit_ce/implementations/legacy_implementation/interpolate.py
+++ b/submit_ce/implementations/legacy_implementation/interpolate.py
@@ -27,7 +27,7 @@
SetTitle, SetAbstract, SetComments, SetMSCClassification, \
SetACMClassification, SetAuthors, ConfirmSourceProcessed, Reclassify
-from submit_ce.api.domain.agent import System, User
+from submit_ce.api.domain.agent import System
logger = logging.getLogger(__name__)
logger.propagate = False
diff --git a/submit_ce/implementations/legacy_implementation/log.py b/submit_ce/implementations/legacy_implementation/log.py
index c984761b..065bbc87 100644
--- a/submit_ce/implementations/legacy_implementation/log.py
+++ b/submit_ce/implementations/legacy_implementation/log.py
@@ -1,6 +1,6 @@
"""Interface to the classic admin log."""
-from typing import Optional, Dict, Callable, List
+from typing import Optional, Callable
from sqlalchemy.orm import Session as SQLAlchemySession
@@ -10,7 +10,7 @@
AddClassifierResults
from submit_ce.api.domain.flag import ContentFlag
from submit_ce.api.domain.submission import Submission
-from . import models, util
+from . import models
def log_unfinalize(session: SQLAlchemySession, event: Event, before: Optional[Submission],
diff --git a/submit_ce/implementations/legacy_implementation/patch.py b/submit_ce/implementations/legacy_implementation/patch.py
index 44ab751a..0a1774c6 100644
--- a/submit_ce/implementations/legacy_implementation/patch.py
+++ b/submit_ce/implementations/legacy_implementation/patch.py
@@ -6,7 +6,6 @@
"""
import datetime
-from typing import Any
from arxiv.db import models
diff --git a/submit_ce/implementations/pubsub/tests/conftest.py b/submit_ce/implementations/pubsub/tests/conftest.py
index 16a20210..ced274ea 100644
--- a/submit_ce/implementations/pubsub/tests/conftest.py
+++ b/submit_ce/implementations/pubsub/tests/conftest.py
@@ -1,4 +1,3 @@
-import multiprocessing
import pytest
from xprocess import ProcessStarter
import socket
diff --git a/submit_ce/implementations/pubsub/tests/test_pubusb_impl.py b/submit_ce/implementations/pubsub/tests/test_pubusb_impl.py
index 65d153b7..ea8433c2 100644
--- a/submit_ce/implementations/pubsub/tests/test_pubusb_impl.py
+++ b/submit_ce/implementations/pubsub/tests/test_pubusb_impl.py
@@ -1,12 +1,9 @@
-import json
import time
from unittest.mock import MagicMock
import inspect
import pytest
from google.cloud import pubsub_v1
-from hypothesis import given, settings
-from hypothesis_jsonschema import from_schema
from polyfactory.factories.pydantic_factory import ModelFactory
from pydantic import TypeAdapter
diff --git a/submit_ce/implementations/schedule.py b/submit_ce/implementations/schedule.py
index d25aaa4c..489f89c1 100644
--- a/submit_ce/implementations/schedule.py
+++ b/submit_ce/implementations/schedule.py
@@ -21,8 +21,8 @@
from typing import Optional
from datetime import datetime, timedelta
-from enum import IntEnum, Enum
-from pytz import timezone, UTC
+from enum import IntEnum
+from pytz import timezone
ET = timezone('US/Eastern')
diff --git a/submit_ce/implementations/tests/conftest.py b/submit_ce/implementations/tests/conftest.py
index 6064c862..5bfefd29 100644
--- a/submit_ce/implementations/tests/conftest.py
+++ b/submit_ce/implementations/tests/conftest.py
@@ -1,4 +1,3 @@
-import pytest
"""
Are there tests in the graveyard?
diff --git a/submit_ce/ui/auth.py b/submit_ce/ui/auth.py
index 954abf76..8caac721 100644
--- a/submit_ce/ui/auth.py
+++ b/submit_ce/ui/auth.py
@@ -1,13 +1,13 @@
from datetime import datetime, timezone
import logging
-from typing import Callable, Tuple, Optional
+from typing import Tuple, Optional
import jwt
from arxiv.auth.legacy import util
from arxiv.db.models import Demographic, TapirNickname, TapirUser
from arxiv.db import Session as DB # renamed due to too many session
-from flask import has_app_context, has_request_context, request
+from flask import has_request_context, request
from pydantic_core import ValidationError
from werkzeug.datastructures import MultiDict
from werkzeug.exceptions import Unauthorized, NotFound
diff --git a/submit_ce/ui/backend.py b/submit_ce/ui/backend.py
index 17ca799e..90b47727 100644
--- a/submit_ce/ui/backend.py
+++ b/submit_ce/ui/backend.py
@@ -9,7 +9,6 @@
from submit_ce.api import SubmitApi, Submission, Event
from submit_ce.api.domain import User
from submit_ce.api.exceptions import NoSuchSubmission
-from submit_ce.implementations import NullImplementation
from submit_ce.implementations.compile.compile_at_gcp_service import GcpCompileAtLegacy
from submit_ce.implementations.file_store.gs_file_store import GsFileStore
from submit_ce.implementations.file_store.legacy_file_store import LegacyFileStore
@@ -62,7 +61,7 @@ def get_submission(submission_id: int) -> Tuple[Submission, List[Event]]:
g.events = history
return submission, history
- except NoSuchSubmission as nss:
+ except NoSuchSubmission:
raise NotFound()
diff --git a/submit_ce/ui/conftest.py b/submit_ce/ui/conftest.py
index d6f60cf6..fa00f8d6 100644
--- a/submit_ce/ui/conftest.py
+++ b/submit_ce/ui/conftest.py
@@ -44,7 +44,6 @@
# to ensure we can import this due to confusing errors if deps are missing.
#import submit_ce.api.implementations.legacy_implementation
from submit_ce.make_test_db import bootstrap_db, create_all_legacy_db
-from submit_ce.ui import backend
from submit_ce.ui.tests import ClientArxivAuth
diff --git a/submit_ce/ui/controllers/new/license.py b/submit_ce/ui/controllers/new/license.py
index 8591f2e3..2ba4e30b 100644
--- a/submit_ce/ui/controllers/new/license.py
+++ b/submit_ce/ui/controllers/new/license.py
@@ -14,9 +14,7 @@
from flask import current_app
from submit_ce.ui.auth import user_and_client_from_session
-from submit_ce.api.exceptions import SaveError
from werkzeug.datastructures import MultiDict
-from werkzeug.exceptions import InternalServerError
from wtforms.fields import RadioField
from wtforms.validators import InputRequired
diff --git a/submit_ce/ui/controllers/new/review.py b/submit_ce/ui/controllers/new/review.py
index cb095a99..7e6acb3d 100644
--- a/submit_ce/ui/controllers/new/review.py
+++ b/submit_ce/ui/controllers/new/review.py
@@ -8,7 +8,6 @@
"""
import logging
-import traceback
from collections import OrderedDict
from http import HTTPStatus as status
from locale import strxfrm
@@ -18,13 +17,10 @@
from flask import current_app
from arxiv.auth.domain import Session
from arxiv.base import alerts
-from arxiv.base.filters import tidy_filesize
from arxiv.forms import csrf
from markupsafe import Markup
-from werkzeug.datastructures import FileStorage
from werkzeug.datastructures import MultiDict
from werkzeug.exceptions import (
- InternalServerError,
MethodNotAllowed,
RequestEntityTooLarge
)
@@ -36,7 +32,6 @@
from submit_ce.api.domain.uploads import Upload, FileStatus, UploadStatus
from submit_ce.api.exceptions import SaveError
-from submit_ce.ui.auth import user_and_client_from_session
from submit_ce.ui.controllers.util import add_immediate_alert, validate_command
from submit_ce.ui.routes.flow_control import stay_on_this_stage
from submit_ce.ui.backend import get_submission
diff --git a/submit_ce/ui/controllers/new/tests/test_metadata.py b/submit_ce/ui/controllers/new/tests/test_metadata.py
index edbd793f..0af9ad09 100644
--- a/submit_ce/ui/controllers/new/tests/test_metadata.py
+++ b/submit_ce/ui/controllers/new/tests/test_metadata.py
@@ -3,7 +3,6 @@
from submit_ce.api.domain.submission import Submission
from submit_ce.ui.tests import gets
from submit_ce.ui.tests.csrf_util import parse_csrf_token
-import pytest
def test_no_sub(app, authorized_client):
url = "/93489292/classification"
diff --git a/submit_ce/ui/controllers/new/unsubmit.py b/submit_ce/ui/controllers/new/unsubmit.py
index b88bc9ef..d0d84290 100644
--- a/submit_ce/ui/controllers/new/unsubmit.py
+++ b/submit_ce/ui/controllers/new/unsubmit.py
@@ -5,7 +5,7 @@
from flask import url_for, current_app
from wtforms import BooleanField, validators
from werkzeug.datastructures import MultiDict
-from werkzeug.exceptions import BadRequest, InternalServerError
+from werkzeug.exceptions import BadRequest
from arxiv.base import alerts
from arxiv.forms import csrf
diff --git a/submit_ce/ui/controllers/new/upload.py b/submit_ce/ui/controllers/new/upload.py
index 8ce7a27f..6c316f19 100644
--- a/submit_ce/ui/controllers/new/upload.py
+++ b/submit_ce/ui/controllers/new/upload.py
@@ -10,7 +10,6 @@
"""
import logging
-import traceback
from collections import OrderedDict
from http import HTTPStatus as status
from locale import strxfrm
@@ -26,7 +25,6 @@
from werkzeug.datastructures import FileStorage
from werkzeug.datastructures import MultiDict
from werkzeug.exceptions import (
- InternalServerError,
MethodNotAllowed,
RequestEntityTooLarge
)
diff --git a/submit_ce/ui/controllers/new/upload_delete.py b/submit_ce/ui/controllers/new/upload_delete.py
index 359bb913..93d06bdc 100644
--- a/submit_ce/ui/controllers/new/upload_delete.py
+++ b/submit_ce/ui/controllers/new/upload_delete.py
@@ -18,13 +18,12 @@
#from arxiv.submission.services import Filemanager
from arxiv.auth.domain import Session
from werkzeug.datastructures import MultiDict
-from werkzeug.exceptions import BadRequest, MethodNotAllowed
+from werkzeug.exceptions import MethodNotAllowed
from wtforms import BooleanField, HiddenField
from wtforms.validators import DataRequired
from submit_ce.ui.backend import get_submission
-from submit_ce.ui.routes.flow_control import ready_for_next, \
- stay_on_this_stage, return_to_parent_stage
+from submit_ce.ui.routes.flow_control import stay_on_this_stage, return_to_parent_stage
from submit_ce.ui.controllers.util import add_immediate_alert, validate_command
from submit_ce.ui import SUPPORT
diff --git a/submit_ce/ui/controllers/new/verify_user.py b/submit_ce/ui/controllers/new/verify_user.py
index 975f3b7b..2aab91cd 100644
--- a/submit_ce/ui/controllers/new/verify_user.py
+++ b/submit_ce/ui/controllers/new/verify_user.py
@@ -4,18 +4,16 @@
Creates an event of type `core.events.event.ConfirmContactInformation`
"""
from http import HTTPStatus as status
-from typing import Tuple, Dict, Any, Optional
+from typing import Tuple, Dict, Any
-from flask import url_for, current_app
+from flask import current_app
from werkzeug.datastructures import MultiDict
-from werkzeug.exceptions import InternalServerError, NotFound, BadRequest
from wtforms import BooleanField
from wtforms.validators import InputRequired
import logging
from arxiv.forms import csrf
from arxiv.auth.domain import Session
-from submit_ce.api.exceptions import SaveError
from submit_ce.ui.auth import user_and_client_from_session
from submit_ce.api.domain.event import ConfirmContactInformation
diff --git a/submit_ce/ui/controllers/tests/test_jref.py b/submit_ce/ui/controllers/tests/test_jref.py
index 22fef7a4..02c72e1a 100644
--- a/submit_ce/ui/controllers/tests/test_jref.py
+++ b/submit_ce/ui/controllers/tests/test_jref.py
@@ -1,14 +1,12 @@
"""Tests for :mod:`submit_ce.controllers.jref`."""
import pytest
-from unittest import TestCase, mock
+from unittest import mock
from werkzeug.datastructures import MultiDict
from http import HTTPStatus as status
from pytz import timezone
from datetime import timedelta, datetime
-from arxiv.auth import auth, domain
from submit_ce.ui.tests import CtrlBase
-import submit_ce.api.domain
from submit_ce.ui.controllers import jref
diff --git a/submit_ce/ui/controllers/util.py b/submit_ce/ui/controllers/util.py
index 765ead96..e7976990 100644
--- a/submit_ce/ui/controllers/util.py
+++ b/submit_ce/ui/controllers/util.py
@@ -1,12 +1,12 @@
"""Helpers for controllers."""
-from typing import Any, Dict, Iterable, Tuple, Optional, List, Union
+from typing import Any, Dict, Iterable, Tuple, Optional, Union
from markupsafe import Markup
from wtforms.validators import StopValidation
from wtforms.widgets import Select, html_params
from wtforms import SelectField, \
- SelectMultipleField, Form
+ Form
from wtforms.fields.core import UnboundField
from submit_ce.api.domain import Event, Submission
diff --git a/submit_ce/ui/routes/ui.py b/submit_ce/ui/routes/ui.py
index 9e6b0b3e..be250ccb 100644
--- a/submit_ce/ui/routes/ui.py
+++ b/submit_ce/ui/routes/ui.py
@@ -6,7 +6,7 @@
from arxiv.auth.auth.decorators import scoped
from arxiv.base import logging, alerts
from flask import Blueprint, make_response, redirect, request, \
- render_template, url_for, send_file, g
+ render_template, url_for, send_file
from flask import Response as FResponse
from markupsafe import Markup
from werkzeug import Response as WResponse
diff --git a/submit_ce/ui/static/css/submit.css b/submit_ce/ui/static/css/submit.css
index 57676710..3303fbb0 100644
--- a/submit_ce/ui/static/css/submit.css
+++ b/submit_ce/ui/static/css/submit.css
@@ -499,7 +499,7 @@ h1.title.title-submit { color: black !important; } /* keep H1 black */
.info-container .buttons.submit-nav .button { margin: 0; padding: .35em !important; height: auto; line-height: 1em; background-color: #E6E6E6; }
.info-container .buttons.submit-nav .button.is-success { background-color: #1e8bc3; font-weight: 600; } /* primary blue */
-/* Progress bar deemphasis */
+/* Progress bar deemphasis */ /* merge with progressbar settings above */
.progressbar li { background: transparent; border: 1px solid #a5d6fe; }
.progressbar li a { color: #1e8bc3; }
.progressbar li.is-complete.is-active { background-color: #a5d6fe; }
@@ -513,4 +513,39 @@ h1.title.title-submit { color: black !important; } /* keep H1 black */
.field-label, .field-body { padding: .5em !important; }
/* Asterisks/errors */
-.is-danger { color: red; }
\ No newline at end of file
+.is-danger { color: red; }
+
+.content-container .policy-scroll {
+ font-family: 'Times New Roman', Times, serif; /* legalese serif */
+}
+
+.content-container .policy-scroll h3 {
+ font-family: 'Times New Roman', Times, serif;
+ text-align: center;
+}
+
+/* Disabled primary button state used on Agreement */
+.info-container .buttons.submit-nav .button.is-success.is-disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+/* Legal / scrollable agreement & license text areas */
+/*
+.content-container {
+ background-color: #f9f7f7; /* light paper tone behind legal copy */
+}
+*/
+/* Optional: license option groups look like cards */
+/*
+.action-container {
+ background-color: #f9f7f7; /* subtle card background */
+}
+*/
+
+/* Only legal pages */
+.legal-page .content-container,
+.legal-page .action-container { background-color: #f9f7f7; }
+
+
diff --git a/submit_ce/ui/templates/submit/verify_user.html b/submit_ce/ui/templates/submit/verify_user.html
index df091ac5..29336eff 100644
--- a/submit_ce/ui/templates/submit/verify_user.html
+++ b/submit_ce/ui/templates/submit/verify_user.html
@@ -59,13 +59,6 @@