File Upload
A FileUpload is the metadata record for a file living in S3-compatible object storage. You touch it whenever a Care 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 Care resource by file_type + associating_id |
FileUpload extends EMRBaseModel (shared Care 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