References/File Upload: Difference between revisions
More languages
More actions
OHC identity seed |
Add Navbox platform (via update-page on MediaWiki MCP Server) |
||
| Line 313: | Line 313: | ||
* Archival (<code>POST .../{external_id}/archive/</code>, body <code>{ archive_reason }</code>) sets <code>is_archived</code>, <code>archive_reason</code>, <code>archived_datetime</code>, <code>archived_by</code> server-side and returns <code>FileUploadListSpec</code>. This soft state is separate from the <code>EMRBaseModel</code> soft-delete (<code>deleted</code>). |
* Archival (<code>POST .../{external_id}/archive/</code>, body <code>{ archive_reason }</code>) sets <code>is_archived</code>, <code>archive_reason</code>, <code>archived_datetime</code>, <code>archived_by</code> server-side and returns <code>FileUploadListSpec</code>. This soft state is separate from the <code>EMRBaseModel</code> soft-delete (<code>deleted</code>). |
||
* <code>mark_upload_completed</code> and <code>archive</code> both return <code>FileUploadListSpec</code> (no signed URL). |
* <code>mark_upload_completed</code> and <code>archive</code> both return <code>FileUploadListSpec</code> (no signed URL). |
||
{{Navbox platform}} |
|||
{{Related}} |
{{Related}} |
||
Latest revision as of 09:53, 6 July 2026
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: [https://github.com/ohcnetwork/care/blob/develop/care/emr/models/file_upload.py
care/emr/models/file_upload.py] - Specs: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/file_upload/spec.py
care/emr/resources/file_upload/spec.py] - Viewset: [https://github.com/ohcnetwork/care/blob/develop/care/emr/api/viewsets/file_upload.py
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
{{Field|model=FileUpload|section=File metadata|name=file_type|type=CharField(100)|notes=Domain of the owning resource. API-constrained to [[#filetypechoices-values}} {{Field|model=FileUpload|section=File metadata|name=file_category|type=CharField(100)|notes=API-constrained to [[#filecategorychoices-values}} |
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 [[References/Patient |
encounter
|
An [[References/Encounter |
consent
|
A [[References/Consent |
diagnostic_report
|
A [[References/Diagnostic Report |
service_request
|
A [[References/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 | 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
|
None | column |
archive_reason
|
None | column |
archived_datetime
|
None | column |
created_date
|
datetime
|
column |
extension
|
str
|
derived via obj.get_extension()
|
mime_type
|
str
|
obj.meta.get("mime_type")
|
uploaded_by
|
None | UserSpec from cache (obj.created_by_id)
|
archived_by
|
None | UserSpec from cache (obj.archived_by_id)
|
created_by / updated_by
|
None | serialize_audit_users
|
FileUploadRetrieveSpec extra fields
| Field | Type | When populated |
|---|---|---|
signed_url
|
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
|
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
- Reference: References/User & Skills