Clinical care
File Upload
A FileUpload is the metadata record for a file living in S3-compatible object storage. You touch it whenever a MANSOQ resource needs an attachment — a patient document, an X-ray on an encounter, a signed consent form. Two layers split the work.…
3.1 · versioned documentation · 7 min read
A FileUpload is the metadata record for a file living in S3-compatible object storage. You touch it whenever a MANSOQ resource needs an attachment — a patient document, an X-ray on an encounter, a signed consent form.
Two layers split the work. The Django model is storage: it persists the row and an opaque meta JSON column, nothing more. The Pydantic resource specs in care/emr/resources/file_upload/spec.py are the API — they define the file_type / file_category enums, validate the upload, decide what goes into meta (mime_type), and shape every read/write payload, including extension, signed_url, and read_signed_url, which are computed on the fly and have no backing columns.
Source:
- Model:
care/emr/models/file_upload.py - Specs:
care/emr/resources/file_upload/spec.py - Viewset:
care/emr/api/viewsets/file_upload.py
Models
| Model | Purpose |
|---|---|
FileUpload |
Metadata record for a file stored in object storage (S3), associated with another MANSOQ resource by file_type + associating_id |
FileUpload extends EMRBaseModel (shared MANSOQ EMR base with external_id, meta JSON, audit fields, and soft-delete semantics).
FileUpload fields
File metadata
| Field | Type | Notes |
|---|---|---|
name |
CharField(2000) |
User-facing file name. Required on the API (FileUploadBaseSpec.name) |
internal_name |
CharField(2000) |
Storage object key. Set server-side from original_name on create, then a random key is generated on save(). Never set by clients |
associating_id |
CharField(100) |
The external_id of the owning resource. Required (blank=False, null=False). There's no DB ForeignKey — integrity is enforced in the app layer |
file_type |
CharField(100) |
Domain of the owning resource. API-constrained to FileTypeChoices |
file_category |
CharField(100) |
API-constrained to FileCategoryChoices |
upload_completed |
BooleanField |
Default False. Flips to True once the client finishes the direct-to-S3 upload — or immediately, on the inline upload-file path |
meta JSON contents
meta is the opaque JSONField inherited from EMRBaseModel. The specs put exactly one structured key in it:
| Key | Type | Shape / Notes |
|---|---|---|
mime_type |
str |
MIME type of the file. Written in FileUploadCreateSpec.perform_extra_deserialization (obj.meta["mime_type"] = self.mime_type); read back during serialization via obj.meta.get("mime_type"). Validated against settings.ALLOWED_MIME_TYPES |
Archival
| Field | Type | Notes |
|---|---|---|
is_archived |
BooleanField |
Default False |
archive_reason |
TextField |
Blank-able; set when archiving (required in the archive action body) |
archived_datetime |
DateTimeField |
Nullable; set to timezone.now() when archived |
archived_by |
FK → User (PROTECT) |
Nullable; related_name="archived_files" |
Storage manager
FileUpload declares a class-level files_manager = S3FilesManager(BucketType.PATIENT). It's not a database field — it's the object-storage helper that mints signed write/read URLs and runs put_object, all against the PATIENT bucket.
Enums
FileTypeChoices values
The owning-resource domain (spec.py). String enum.
| Value | Links a file to |
|---|---|
patient |
A patient |
encounter |
An encounter |
consent |
A consent |
diagnostic_report |
A diagnostic report |
service_request |
A service request |
FileCategoryChoices values
File category (spec.py). String enum.
| Value |
|---|
audio |
xray |
identity_proof |
unspecified |
discharge_summary |
consent_attachment |
Resource specs (API schema)
Every spec extends EMRResource (care/emr/resources/base.py), which supplies serialize (DB → Pydantic) and de_serialize (Pydantic → DB) plus the perform_extra_serialization / perform_extra_deserialization hooks. FileUpload deliberately omits __store_metadata__, so its hooks move mime_type in and out of meta by hand instead of leaning on the generic meta machinery.
| Spec | Role | Exposes / behaviour |
|---|---|---|
FileUploadBaseSpec |
shared base | id: UUID4 | None, name: str (required) |
FileUploadCreateSpec |
write · create | Adds original_name, file_type, file_category, associating_id, mime_type. Validates mime_type and original_name; on deserialize sets _just_created=True, internal_name = original_name, and meta["mime_type"] |
FileUploadUpdateSpec |
write · update | Same fields as base (id, name) — the display name is the only thing you can edit |
FileUploadListSpec |
read · list | Read view (fields below); resolves extension, mime_type, audit users, uploaded_by, archived_by |
FileUploadRetrieveSpec |
read · detail | Extends FileUploadListSpec with signed_url, read_signed_url, internal_name; emits a write URL for just-created rows, otherwise a read URL |
ConsentFileUploadCreateSpec |
write · create (consent) | original_name + associating_id: UUID4; forces file_type=consent, file_category=consent_attachment server-side |
Nothing here binds to a value set or uses CodeableConcept / Period / other common types — file_type and file_category are plain string enums.
FileUploadCreateSpec validation
| Field | Rule |
|---|---|
name |
Required (inherited from base) |
original_name |
Non-empty; run through file_name_validator (care/utils/models/validators.py): ≤ 255 chars, must not start with ., must have an extension, extension must be in ALLOWED_FILE_EXTENSIONS and not in BLOCKED_FILE_EXTENSIONS |
mime_type |
Must be in settings.ALLOWED_MIME_TYPES, else "Invalid mime type" |
file_type |
Must be a FileTypeChoices member |
file_category |
Must be a FileCategoryChoices member |
associating_id |
Required str |
FileUploadListSpec read fields
| Field | Type | Source |
|---|---|---|
id |
UUID4 |
obj.external_id (set in perform_extra_serialization) |
name |
str |
column |
file_type |
FileTypeChoices |
column |
file_category |
FileCategoryChoices |
column |
associating_id |
str |
column |
upload_completed |
bool |
column |
is_archived |
bool | None |
column |
archive_reason |
str | None |
column |
archived_datetime |
datetime | None |
column |
created_date |
datetime |
column |
extension |
str |
derived via obj.get_extension() |
mime_type |
str |
obj.meta.get("mime_type") |
uploaded_by |
dict | None |
UserSpec from cache (obj.created_by_id) |
archived_by |
UserSpec | None |
UserSpec from cache (obj.archived_by_id) |
created_by / updated_by |
dict | None |
serialize_audit_users |
FileUploadRetrieveSpec extra fields
| Field | Type | When populated |
|---|---|---|
signed_url |
str | None |
When obj._just_created is truthy — a write (PUT) URL from files_manager.signed_url(obj). The upload target handed back at create time |
read_signed_url |
str | None |
Otherwise — a read (GET) URL from files_manager.read_signed_url(obj) |
internal_name |
str |
The storage object key (flagged in source as possibly not needing to be returned) |
Methods & save behaviour
get_extension()
Derives the file extension from internal_name via parse_file_extension, formatted as .ext (or a multi-part form like .tar.gz), or "" when there's none. This is what surfaces to clients as the extension read field.
save() side effects
On save, a random internal_name is generated when internal_name is empty or the row is new (not self.id) and the caller has not passed skip_internal_name=True:
- A random
internal_nameis generated asuuid4() + int(time.time()). - If an extension can be derived, it is appended to that random name.
internal_nameis set before the row is persisted.
Keeping the user-supplied name out of the storage key limits PII leakage if the bucket is compromised. The inline upload-file path calls save(skip_internal_name=True) after uploading, so the generated key survives.
API integration notes
Two upload flows
- Signed-URL (direct-to-S3), default:
POSTaFileUploadCreateSpec. The server validates, deserializes (_just_created=True), and returnsFileUploadRetrieveSpeccarrying a writesigned_url. The clientPUTs the bytes straight to S3, then callsPOST .../{external_id}/mark_upload_completed/to flipupload_completed=True. - Inline base64 (
POST .../upload-file/): the client sendsoriginal_name+ base64file_data. The server decodes it, enforcessettings.MAX_FILE_UPLOAD_SIZE(MB), sniffs the real MIME type withpython-magic, re-checksALLOWED_MIME_TYPES, builds aFileUploadCreateSpec, sets_just_created=False, uploads viafiles_manager.put_object, setsupload_completed=True, and returnsFileUploadRetrieveSpec— so the response carriesread_signed_url, not a write URL.
Server-maintained behaviour
internal_nameis platform-maintained — never set it from a client; the model generates a random PII-free key.mime_typelives inmeta, not a column.FileUploadCreateSpecwrites it and serialization reads it back. On the inline path it is re-derived from file content (magic.from_buffer) rather than trusted from the client.associating_id+file_typetogether link a file to its owning resource.listrequires both as query params and returns onlyupload_completed=Truerows.- Every action runs
file_authorizer(user, file_type, associating_id, "read"|"write"). - Archival (
POST .../{external_id}/archive/, body{ archive_reason }) setsis_archived,archive_reason,archived_datetime,archived_byserver-side and returnsFileUploadListSpec. This soft state is separate from theEMRBaseModelsoft-delete (deleted). mark_upload_completedandarchiveboth returnFileUploadListSpec(no signed URL).
Related
- Base model: EMRBaseModel
- Owning resources: patient, encounter, consent, diagnostic-report, service-request
- User (audit /
uploaded_by/archived_by): user - Source: file_upload.py, spec.py
Source evidence
References are pinned to the private MANSOQ source repository. Repository access is required to open these links.