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 @@
{{ user.email }} *
- diff --git a/submit_ce/ui/tests/test_submit_web.py b/submit_ce/ui/tests/test_submit_web.py index 14fcc96f..f3ee8cee 100644 --- a/submit_ce/ui/tests/test_submit_web.py +++ b/submit_ce/ui/tests/test_submit_web.py @@ -2,7 +2,6 @@ from http import HTTPStatus as status from urllib.parse import urlparse -from submit_ce.ui import backend from submit_ce.ui.tests.csrf_util import parse_csrf_token diff --git a/submit_ce/ui/workflow/tests/test_endorsement_workflow.py b/submit_ce/ui/workflow/tests/test_endorsement_workflow.py index ab02ede8..3bf9e8c6 100644 --- a/submit_ce/ui/workflow/tests/test_endorsement_workflow.py +++ b/submit_ce/ui/workflow/tests/test_endorsement_workflow.py @@ -1,22 +1,8 @@ """Tests for the submission application as a whole.""" -import os -import tempfile -from http import HTTPStatus as status -from unittest import TestCase, mock -from urllib.parse import urlparse - -from arxiv.auth.auth import scopes -from arxiv.auth.helpers import generate_token - -from submit_ce.api.domain import Author, SubmissionContent -from submit_ce.api.domain import User -from submit_ce.api.domain.event import SetPrimaryClassification, CreateSubmission, ConfirmContactInformation, \ - ConfirmAuthorship, SetLicense, ConfirmPolicy, SetUploadPackage, SetTitle, SetAbstract, SetComments, SetReportNumber, \ - SetAuthors, FinalizeSubmission - -from submit_ce.ui.tests import CtrlBase -from submit_ce.ui.tests.csrf_util import parse_csrf_token + + + # SKIP: endorsement doesn't currently work correct due to # TODO fix submit_ci/ui/auth.py for auth, auth use to be on JWT but will not be in the future diff --git a/submit_ce/ui/workflow/tests/test_jref_workflow.py b/submit_ce/ui/workflow/tests/test_jref_workflow.py index 46845235..6a200992 100644 --- a/submit_ce/ui/workflow/tests/test_jref_workflow.py +++ b/submit_ce/ui/workflow/tests/test_jref_workflow.py @@ -1,24 +1,12 @@ """Tests for the submission application as a whole.""" -import pytest -import os from http import HTTPStatus as status -from arxiv.auth.auth import scopes -from arxiv.auth.helpers import generate_token from arxiv.db import models as classic -from submit_ce.api.domain import Author, SubmissionContent -from submit_ce.api.domain import User, Client -from submit_ce.api.domain.agent import PublicUser -from submit_ce.api.domain.event import SetPrimaryClassification, CreateSubmission, ConfirmContactInformation, \ - ConfirmAuthorship, SetLicense, ConfirmPolicy, SetUploadPackage, SetTitle, SetAbstract, SetComments, SetReportNumber, \ - SetAuthors, FinalizeSubmission -from submit_ce.ui.tests import CtrlBase, ClientArxivAuth from submit_ce.ui.tests.csrf_util import parse_csrf_token from arxiv.db import Session -from arxiv.db import models # @pytest.fixture diff --git a/submit_ce/ui/workflow/tests/test_unsubmit_workflow.py b/submit_ce/ui/workflow/tests/test_unsubmit_workflow.py index cb774257..93b7054e 100644 --- a/submit_ce/ui/workflow/tests/test_unsubmit_workflow.py +++ b/submit_ce/ui/workflow/tests/test_unsubmit_workflow.py @@ -2,7 +2,6 @@ from http import HTTPStatus as status -from flask import current_app from submit_ce.ui.tests import gets from submit_ce.ui.tests.csrf_util import parse_csrf_token diff --git a/submit_ce/ui/workflow/tests/test_withdraw_workflow.py b/submit_ce/ui/workflow/tests/test_withdraw_workflow.py index 51f97a1a..0d948527 100644 --- a/submit_ce/ui/workflow/tests/test_withdraw_workflow.py +++ b/submit_ce/ui/workflow/tests/test_withdraw_workflow.py @@ -1,22 +1,11 @@ """Tests for the submission application as a whole.""" -import os -import tempfile from http import HTTPStatus as status from arxiv.db import Session import arxiv.db.models as classic -from arxiv.auth.auth import scopes -from arxiv.auth.helpers import generate_token -from submit_ce.api.domain import Author, SubmissionContent -from submit_ce.api.domain import User -from submit_ce.api.domain.agent import InternalClient -from submit_ce.api.domain.event import SetPrimaryClassification, CreateSubmission, ConfirmContactInformation, \ - ConfirmAuthorship, SetLicense, ConfirmPolicy, SetUploadPackage, SetTitle, SetAbstract, SetComments, SetReportNumber, \ - SetAuthors, FinalizeSubmission -from submit_ce.ui import backend from submit_ce.ui.tests.csrf_util import parse_csrf_token