Submissions¶
A Submission is the entry point into the system. It represents:
- The file being validated (JSON, XML, EnergyPlus IDF, etc.).
- The workflow version to run.
- The organization, project, and user context.
- Metadata such as content type, size, and SHA-256 checksum.
Submissions can have multiple Validation Runs over time, but typically point to the latest run.
User Context¶
Submission.user captures the human (or service) that supplied the payload. We
store it even though each ValidationRun also has a user field because a
single submission can be re-run many times by different operators:
- A data engineer uploads a file, but an admin later replays the same submission to verify fixes.
- An API integration pushes content using an org-level API token where no Django
Userinstance exists. - Background processes can enqueue submissions on behalf of a workflow (for example, nightly batch imports) without an authenticated user object.
Those flows mean Submission.user is nullable. When it is NULL we rely on the
organization/project ForeignKeys and metadata provided in the payload (API key,
signed request, etc.) to decide who owns the submission.
Relationship to Validation Runs¶
A ValidationRun references the submission that triggered it, but it records the
user who executed the run. Keeping both fields lets the audit trail answer two
questions:
- Who provided the content? →
submission.user - Who triggered this execution? →
validation_run.user
When you launch a run via the UI, both values usually match. When executions are
scheduled, retried by Celery, or invoked via an API key, the run user may be
NULL or different from the submission user. Treat the submission record as the
ownership anchor for the payload itself, and the run record as the executor
context for a single processing attempt.
Data Retention¶
Submissions support configurable retention policies that control how long the actual content (file or inline text) is stored. This supports compliance requirements and reduces storage costs for workflows that don't need to retain user data.
Retention Policies¶
| Policy | Behavior |
|---|---|
DO_NOT_STORE |
Content deleted after validation completes |
STORE_1_DAY |
Content retained for 1 day |
STORE_7_DAYS |
Content retained for 7 days |
STORE_30_DAYS |
Content retained for 30 days |
STORE_PERMANENTLY |
Content retained indefinitely |
Key Fields¶
retention_policy: Snapshot of the workflow's retention setting at submission timeexpires_at: Provisional receipt-time deadline, reset after the last related run to give the full author-selected post-processing window (null forDO_NOT_STORE, permanent retention, or already purged)content_purged_at: Timestamp when content was purged (audit trail)
Content Purge vs Record Deletion¶
When a submission's retention expires, we purge the content but preserve a minimal record. This means:
- The
Submissionrow remains in the database contentis cleared to empty stringinput_fileis deleted from storage- submitter-supplied names, original filenames, and arbitrary metadata are cleared
checksum_sha256,size_bytes,file_type, and timestamps remain for audit- auxiliary
SubmissionInputFilebytes and submitter context are removed - copied run inputs and input envelopes are deleted, while independently retained outputs remain until their own policy expires
No purge runs while a related validation is active. Shared inputs are eligible only after every related run is terminal.
Defensive FK: ValidationRun.submission¶
ValidationRun.submission uses SET_NULL instead of CASCADE. This means:
- If a Submission record is accidentally deleted, the ValidationRun survives
- Code accessing
run.submissionmust handleNone - API responses show
"submission": nullwhen unavailable
Management Commands¶
Two commands handle retention:
# Purge finite-retention submissions past expires_at (run hourly)
python manage.py purge_expired_submissions --batch-size 100
# Process no-retention and failed purge work (run every 5 minutes)
python manage.py process_purge_retries --batch-size 50
PurgeRetry Model¶
One PurgeRetry row exists per submission. Failed deletion uses capped
exponential backoff (1 minute, 5 minutes, 1 hour, 6 hours, then 24 hours).
Crossing five attempts raises an operator warning but never abandons
privacy-critical deletion. A repair scan recreates missing no-retention work
after missed hooks.