Skip to content

Add a token-authenticated REST endpoint to create and poll batch transfer jobs #422

Description

@samuelvkwong

Motivation

The September 2026 ADIT/RADIS brainstorming asks to "directly start a transfer from RADIS". Today the only programmatic surface of ADIT is DICOMweb, so a RADIS user has to export a spreadsheet, reshape it to the batch columns and upload it in ADIT by hand. A small JSON endpoint that creates a batch transfer job from rows in the same shape as the Excel columns removes that round trip and is equally useful for scripts and for adit-client. The RADIS side is a separate issue (openradx/radis#312) and depends on this one.

ADIT had a read-only GET /api/transfer-jobs/ listing once (adit/api, removed in 92f7bb6 "Delete legacy api app", 2024-05-27). This proposal is different: a create + status pair with a concrete consumer, deliberately limited to batch transfer.

Current behaviour

  • The only API mount is api/dicom-web/ (adit/urls.py:34); adit/dicom_web/urls.py contains QIDO/WADO/STOW/NIfTI routes only. adit-client wraps DICOMweb and "cannot create or manage transfer jobs" (CLAUDE.md:238).
  • Batch transfer jobs are created only by BatchTransferJobCreateView at jobs/new/ (adit/batch_transfer/urls.py:36-40), permission batch_transfer.add_batchtransferjob (adit/batch_transfer/views.py:57), behind BatchTransferLockedMixin (adit/batch_transfer/mixins.py:7-9).
  • The form is welded to an .xlsx upload, but the validation core is already file-agnostic: BatchTransferTaskSerializer with the can_transfer_unpseudonymized rule and list-level cross-row checks (adit/batch_transfer/serializers.py:8-36,39-75), model clean() via BatchTaskSerializer.validate (adit/core/serializers.py:43-52), and per-study grouping in BatchTransferFileParser.parse (adit/batch_transfer/parsers.py:40-74).
  • Form policy: urgent needs can_process_urgently, ethics_application_id is required when ETHICS_COMMITTEE_APPROVAL_REQUIRED, MAX_BATCH_TRANSFER_SIZE (500) applies to non-staff (adit/batch_transfer/forms.py:84-90); node access is checked against the active group (forms.py:112-122, adit/core/models.py:107-110, all_groups=False default at core/models.py:37-39).
  • Job creation: owner set, tasks bulk-created with source/destination (forms.py:152-160), PENDING + queue_pending_tasks() when staff or START_BATCH_TRANSFER_UNVERIFIED (adit/core/views.py:109-121, default True at adit/settings/base.py:412).
  • DRF defaults already give token auth, IsAuthenticated and a JSON exception handler to any new view (adit/settings/base.py:225-233).
  • BatchTransferTask.lines is a non-nullable ArrayField (adit/batch_transfer/models.py:34).

Proposal

POST /api/batch-transfer/jobs/ (201) and GET /api/batch-transfer/jobs/<pk>/, Authorization: Token ....

Request: source, destination (node name, unique for servers and folders, adit/core/models.py:80; optionally also ae_title, core/models.py:136), project_name, project_description, ethics_application_id, optional trial_protocol_id, trial_protocol_name, urgent, convert_to_nifti, send_finished_mail, and tasks: [{patient_id, study_uid, series_uids?, pseudonym?}]. archive_password (core/models.py:378) is left out of v1 so no secret travels in a request body.

Validation reuses the form path: BatchTransferTaskSerializer(many=True, can_transfer_unpseudonymized=...), the grouping factored out of parsers.py:43-72 into a function shared with the parser, lines filled with the JSON row index (no migration), the same size/urgent/ethics rules, node access via is_accessible_by_user, and the locked flag that LockedMixin enforces for HTML views. Errors: 400 with per-row errors keyed by index.

Response: {id, status, url, task_count}. GET returns job status/message/timestamps plus per-status task counts and tasks (patient_id, study_uid, pseudonym, status, message), owner-scoped with staff seeing all (adit/core/views.py:138-141). The GET is not optional: on deployments with START_BATCH_TRANSFER_UNVERIFIED=False it is the only way a caller learns the job is awaiting verification.

Add create_batch_transfer_job / get_batch_transfer_job to adit-client (needs a plain HTTP client next to dicomweb-client, adit-client/pyproject.toml:9-13) and document the endpoint in docs/user-docs/technical-overview.md (currently DICOMweb only, line 47).

Out of scope: resolving accession numbers to StudyInstanceUID (a mini batch query) belongs to #152. A node listing and server-side pseudonymization are open questions below.

Open questions

  1. Group semantics (blocks the design): active group like the form, or all groups like DICOMweb (adit/dicom_web/views.py:54, tested at adit/dicom_web/tests/test_authorization.py:331)? Active-group semantics make an API call depend on a UI toggle the caller cannot see. Associate an API token with a group adit-radis-shared#42 (token bound to a group) resolves this; the endpoint could adopt whatever Feature/dicom web support #42 yields and state that in the docs.
  2. Per-user cap: Limit queries and transfers per user #123 records the rule that a user should not create additional batch queries or transfer jobs while one is already pending or in progress. A programmatic endpoint makes flooding trivial, so that check should probably land with it, plus the rate-limit mechanism of Setup rate limits and/or concurrency limits for DICOMweb API #230.
  3. Server-side pseudonymization: mass transfer already derives per-patient pseudonyms (adit/mass_transfer/models.py:39-40, adit/core/utils/pseudonymizer.py:13-24, adit/mass_transfer/processors.py:404-433). A pseudonymize: true flag would let callers without can_transfer_unpseudonymized transfer without inventing pseudonyms client-side. Include here or follow up?
  4. Should the calls count toward APIUsage (adit/dicom_web/views.py:71-80) or get their own record?
  5. GET /api/dicom-nodes/ (accessible nodes for the token owner) so clients can offer a destination picker: same issue or follow-up?

Implementation notes

Options, cheapest first:

  • A1: accept the existing .xlsx as multipart and reuse BatchTransferJobForm. Least code, but keeps Excel as the wire format and Excel-line error messages. Not recommended.
  • A2 (recommended): JSON endpoint as above. Sync DRF APIView is enough; adrf async views (as in dicom_web) if consistency is preferred. upload_api_view (adit/upload/views.py:96-116) is an existing async, session-authenticated, permission-checked, node-addressing precedent.
  • A3: generic job API (all job types, node listing). Can grow out of A2 later.
  • A4: create a SelectiveTransferJob instead (adit/selective_transfer/consumers.py:375-406). Wrong fit: 10-study cap for non-staff, no project/ethics metadata.

The "build a batch transfer job from validated rows" function that A2 factors out of the form is the same one the sibling issue "#419" needs; whichever lands first should extract it.

Tests: 401/403/404 in the style of adit/dicom_web/tests/test_authorization.py, and a create-and-enqueue case mirroring adit/batch_transfer/tests/test_views.py:49.

Related

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions