Report & Templates
A Template defines reusable report markup and render options; a ReportUpload is one generated file (PDF/HTML/etc.) produced from that template and linked to a subject resource. You author templates through the API; report uploads are created for you by the file-upload flow.
Two layers back this:
- Storage layer — the Django models (
care/emr/models/report/). Several fields are opaque (TextField/JSONField); their real shape lives in the spec layer, not the model. - API layer — the Pydantic resource specs (
care/emr/resources/report/), built onEMRResource. These define the enums, validation, the structure of the JSON fields, and the read/write schemas the API exposes.
Source:
- Models:
report_upload.py,template.py - Resource specs:
report/report_upload/spec.py,report/template/spec.py
Models
| Model | Purpose |
|---|---|
ReportUpload | An uploaded, generated report file (PDF/HTML/etc.) produced from a Template and linked to a subject resource via associating_id |
Template | A facility-scoped (or instance-wide) reusable report definition (markup + render options) used to generate reports |
ReportUpload extends EMRBaseModel, the shared Care EMR base providing external_id, meta/history JSON, audit FKs, and soft-delete semantics.
Template extends SlugBaseModel, the facility-scoped slug variant (FACILITY_SCOPED = True, which adds slug handling on top of the EMR base). Slugs carry a prefix in storage: f-<facility_external_id>-<slug> when facility-scoped, i-<slug> when instance-wide (see base.py).
ReportUpload fields
Core
| Field | Type | Notes |
|---|---|---|
template | FK → Template (PROTECT) | Template the report was generated from. The template cannot be deleted while reports reference it |
name | CharField(2000) | Display name of the report. Required in every spec |
internal_name | CharField(2000) | Randomized storage key, auto-generated on save so the file name never leaks PII (see save behaviour). Exposed only in RetrieveSpec |
associating_id | CharField(100) | Non-null. Free-form identifier linking the report to its subject resource (e.g. an encounter) |
upload_completed | BooleanField | Default False. Flips to True once the file upload to storage finishes |
report_type | CharField(50) | File/content type of the report; also exposed via the file_type property for S3FilesManager |
Archival
| Field | Type | Notes |
|---|---|---|
is_archived | BooleanField | Default False |
archive_reason | TextField | Blank allowed |
archived_datetime | DateTimeField | Nullable; set when the report is archived |
archived_by | FK → User (PROTECT) | Nullable. related_name="archived_reports" |
Inherited / metadata
| Field | Type | Notes |
|---|---|---|
meta | JSONField (default {}) | Inherited from EMRBaseModel. Holds mime_type (surfaced by specs as mime_type) and other free-form metadata |
history | JSONField (default {}) | Inherited audit history |
created_by / updated_by | FK → User (SET_NULL) | Inherited audit FKs; serialized as created_by/updated_by UserSpec |
Storage
ReportUpload declares a class-level files_manager = S3FilesManager(BucketType.REPORT). The file body lives in object storage (REPORT bucket), not the database — the model row holds only metadata and the internal_name storage key.
Template fields
The Django model stores most config as plain strings plus an opaque options JSONField. The constraints live in the spec layer: it pins status, default_format, and the slug, and validates the structure and mutual compatibility of template_type, context, and options.
| Field | Type (model) | Spec constraint | Notes |
|---|---|---|---|
facility | FK → Facility (PROTECT, nullable) | UUID4 | None (write) | Null → instance-wide template; set → facility-scoped. Excluded from base serialization (__exclude__); resolved server-side on write |
slug | CharField(255) | written via slug_value: SlugType; read as slug + parsed slug_config | Stored prefixed (f-…/i-…). slug_value must be 5–50 chars, URL-safe, and start and end alphanumeric |
name | CharField(255) | str, required | |
status | CharField(255) | TemplateStatusOptions enum | See status values |
template_data | TextField | str, required (write) | The report markup/body. Returned only by RetrieveSpec, not in list/read |
template_type | CharField(255) | str, validated against ReportTypeRegistry | Must be a registered report type, and compatible with context (matching associating_model) |
default_format | CharField(255) | TemplateFormatOptions enum | See format values. Selects which generator validates options |
context | CharField(100) (default "encounter_base") | str, validated against DataPointRegistry | Defaults to encounter_base, so reports are encounter-oriented unless overridden. Must be a registered context compatible with template_type |
description | TextField (blank, default "") | str = "" | |
options | JSONField (default {}) | dict = {}, validated against the format generator's options_model | Render/config options; accepted keys depend on default_format (PDF vs HTML generator) |
TemplateStatusOptions values
status is bound to this enum (care/emr/resources/report/template/spec.py):
| Value | Meaning |
|---|---|
draft | Template being authored, not yet usable |
active | Published, usable for generating reports |
retired | Withdrawn from use |
TemplateFormatOptions values
default_format is bound to this enum:
| Value | Meaning |
|---|---|
pdf | Render to PDF (uses the PDF generator's options_model) |
html | Render to HTML (uses the HTML generator's options_model) |
slug_config shape (read)
On read, TemplateReadSpec parses the stored prefixed slug back into a slug_config dict:
slug_config (facility-scoped) → { facility: <facility_external_id>, slug_value: <slug> }
slug_config (instance-wide) → { slug_value: <slug> } # parsed from "i-<slug>"
Resource specs (API schema)
All specs subclass EMRResource. Read runs serialize() → DB-field copy → perform_extra_serialization() hook. Write runs de_serialize() → DB-field copy → perform_extra_deserialization() hook.
ReportUpload specs (report/report_upload/spec.py)
| Spec class | Role | Fields / behaviour |
|---|---|---|
ReportUploadBaseSpec | shared | id: UUID4 | None, name: str |
ReportUploadListSpec | read · list | Adds template (nested TemplateReadSpec), report_type, associating_id, archived_by (UserSpec), archived_datetime, upload_completed, is_archived, archive_reason, created_date, extension, uploaded_by, mime_type |
ReportUploadRetrieveSpec | read · detail | Extends list spec; adds signed_url, read_signed_url, internal_name |
Server-maintained read behaviour (perform_extra_serialization):
id←obj.external_id.extension←obj.get_extension()(dotted extension parsed frominternal_name).mime_type←obj.meta["mime_type"].template← serialized viaTemplateReadSpec(full nested template object).created_by/updated_bypopulated from the user cache viaserialize_audit_users.- Signed URLs (retrieve only): a freshly created object (
_just_created) gets a write/uploadsigned_urlviafiles_manager.signed_url(obj); any other read gets aread_signed_urlviafiles_manager.read_signed_url(obj). Exactly one is populated per response.
ReportUploadhas noCreateSpec/UpdateSpecin this module. Uploads come from the file-upload flow (File Upload), and the report metadata andinternal_nameare platform-maintained. See save behaviour.
Template specs (report/template/spec.py)
| Spec class | Role | Fields / behaviour |
|---|---|---|
TemplateBaseSpec | shared | id, name, status (TemplateStatusOptions), default_format (TemplateFormatOptions), description = "", options = {}. __exclude__ = ["facility"] |
TemplateCreateSpec | write · create | Adds facility: UUID4 | None, slug_value: SlugType, template_data: str, template_type: str, context: str |
TemplateUpdateSpec | write · update | Identical to TemplateCreateSpec (pass subclass) |
TemplateReadSpec | read · list | Base fields + slug_config: dict, slug: str, template_type: str, context: str (no template_data) |
TemplateRetrieveSpec | read · detail | Extends read spec; adds nested facility (FacilityBareMinimumSpec) and template_data: str |
Write-side validation and side effects (TemplateCreateSpec):
slug_value—SlugType: 5–50 chars, pattern^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$.template_type(field_validator) — non-empty and resolvable viaReportTypeRegistry, elseInvalid report type.context(field_validator) — non-empty and resolvable viaDataPointRegistry, elseInvalid Context type.validate_report_type_and_context(model_validator, after):- both
template_typeandcontextmust resolve, elseInvalid report type or context; - their
associating_modelmust match (template_type.associating_model == context.__associating_model__), elseReport Type and Context are not compatible; optionsis validated againstGeneratorRegistry.get(default_format).options_model, so the allowedoptionskeys depend on whetherdefault_formatispdforhtml.
- both
perform_extra_deserialization(side effects): resolvesfacilityexternal ID to theFacilityrow (get_object_or_404) when provided, and setsobj.slug = self.slug_value(the raw value; the model prefixes it incalculate_slug).
Read-side behaviour:
TemplateReadSpec.perform_extra_serialization:id ← external_id;slug_config ← obj.parse_slug(obj.slug)(seeslug_configshape).TemplateRetrieveSpec: additionally serializesfacilityviaFacilityBareMinimumSpecwhen set.
Methods & save behaviour
ReportUpload.save()
On create (no id) or when internal_name is empty, save() generates a random internal_name from uuid4() plus the current epoch. This intermediate name decouples the stored object from the human-readable name, so a storage or data leak never exposes PII in file names.
- Pass
skip_internal_name=Truetosave()to bypass generation and keep an explicitly setinternal_name. - If
internal_namealready held a value, its file extension is parsed (viaparse_file_extension) and appended to the new randomized name.
ReportUpload.get_extension()
Returns the dotted extension(s) parsed from internal_name (e.g. .pdf), or "" if none. Surfaced as the extension field in read specs.
ReportUpload.file_type (property)
Alias for report_type, kept for compatibility with S3FilesManager.
Template slug methods (inherited from SlugBaseModel)
calculate_slug()— returns the prefixed slug:f-<facility.external_id>-<slug>when facility-scoped, elsei-<slug>.parse_slug(slug)— the inverse; returnsslug_config(used byTemplateReadSpec).
API integration notes
- Report files live in object storage (S3-compatible REPORT bucket) and are read through
S3FilesManager; the database row is metadata only. internal_nameis platform-maintained — never set it from a client. Reserveskip_internal_namefor controlled migrations. It is exposed only viaReportUploadRetrieveSpec.- A freshly created report returns an upload
signed_url; subsequent reads return aread_signed_urlinstead. - Treat a report as available only once
upload_completedisTrue. - Populate
associating_idwhen generating a report so the file can be retrieved in its subject resource's context. Template.optionsis validated server-side against the chosen format's generator schema, not an arbitrary bag; valid keys depend ondefault_format(pdf/html).template_typeandcontextmust form a compatible pair (sameassociating_model). The defaultcontextisencounter_base, so templates target encounters unless changed.Template.facilityis nullable: null means instance-wide (i-slug), set means facility-scoped (f-slug).
Related
- Reference: File Upload
- Reference: Encounter
- Reference: Facility
- Reference: User
- Reference: Base model
- Source: report_upload.py, template.py
- Source: report_upload/spec.py, template/spec.py