You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add a token-authenticated REST endpoint to create and poll batch transfer jobs #422
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
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.
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?
Should the calls count toward APIUsage (adit/dicom_web/views.py:71-80) or get their own record?
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.
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
api/dicom-web/(adit/urls.py:34);adit/dicom_web/urls.pycontains QIDO/WADO/STOW/NIfTI routes only.adit-clientwraps DICOMweb and "cannot create or manage transfer jobs" (CLAUDE.md:238).BatchTransferJobCreateViewatjobs/new/(adit/batch_transfer/urls.py:36-40), permissionbatch_transfer.add_batchtransferjob(adit/batch_transfer/views.py:57), behindBatchTransferLockedMixin(adit/batch_transfer/mixins.py:7-9)..xlsxupload, but the validation core is already file-agnostic:BatchTransferTaskSerializerwith thecan_transfer_unpseudonymizedrule and list-level cross-row checks (adit/batch_transfer/serializers.py:8-36,39-75), modelclean()viaBatchTaskSerializer.validate(adit/core/serializers.py:43-52), and per-study grouping inBatchTransferFileParser.parse(adit/batch_transfer/parsers.py:40-74).urgentneedscan_process_urgently,ethics_application_idis required whenETHICS_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=Falsedefault atcore/models.py:37-39).forms.py:152-160), PENDING +queue_pending_tasks()when staff orSTART_BATCH_TRANSFER_UNVERIFIED(adit/core/views.py:109-121, defaultTrueatadit/settings/base.py:412).IsAuthenticatedand a JSON exception handler to any new view (adit/settings/base.py:225-233).BatchTransferTask.linesis a non-nullableArrayField(adit/batch_transfer/models.py:34).Proposal
POST /api/batch-transfer/jobs/(201) andGET /api/batch-transfer/jobs/<pk>/,Authorization: Token ....Request:
source,destination(nodename, unique for servers and folders,adit/core/models.py:80; optionally alsoae_title,core/models.py:136),project_name,project_description,ethics_application_id, optionaltrial_protocol_id,trial_protocol_name,urgent,convert_to_nifti,send_finished_mail, andtasks: [{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 ofparsers.py:43-72into a function shared with the parser,linesfilled with the JSON row index (no migration), the same size/urgent/ethics rules, node access viais_accessible_by_user, and thelockedflag thatLockedMixinenforces 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 withSTART_BATCH_TRANSFER_UNVERIFIED=Falseit is the only way a caller learns the job is awaiting verification.Add
create_batch_transfer_job/get_batch_transfer_jobtoadit-client(needs a plain HTTP client next todicomweb-client,adit-client/pyproject.toml:9-13) and document the endpoint indocs/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
adit/dicom_web/views.py:54, tested atadit/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.adit/mass_transfer/models.py:39-40,adit/core/utils/pseudonymizer.py:13-24,adit/mass_transfer/processors.py:404-433). Apseudonymize: trueflag would let callers withoutcan_transfer_unpseudonymizedtransfer without inventing pseudonyms client-side. Include here or follow up?APIUsage(adit/dicom_web/views.py:71-80) or get their own record?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:
.xlsxas multipart and reuseBatchTransferJobForm. Least code, but keeps Excel as the wire format and Excel-line error messages. Not recommended.APIViewis enough;adrfasync views (as indicom_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.SelectiveTransferJobinstead (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 mirroringadit/batch_transfer/tests/test_views.py:49.Related