Round trip¶
Who this is for: the developer who wants to see FHIR go into DHIS2 and come back out as FHIR.
Before you start: Receive captures and Read forms and people.
You will be able to:
- commit a capture into DHIS2, rather than validate it
- read what DHIS2 now holds as FHIR
- ask FHIRPath and CQL questions of what came back
The runnable version of this page is
step11_round_trip.py.
It is the one step in the path that writes. It registers one made-up person with one visit and deletes them afterwards.
DHIS2 keeps a hidden copy of a deleted record, so run it against a demo instance.
graph LR
sent["QuestionnaireResponse"]
dhis2["DHIS2: tracked entity, enrollment, event"]
person["Patient"]
visit["QuestionnaireResponse"]
ask["FHIRPath and CQL"]
sent --> dhis2
dhis2 --> person
dhis2 --> visit
person --> ask
visit --> ask
FHIR in¶
A registration and a visit each go to DHIS2 at once. A visit names the person it belongs to, so the second synthesize
adopts the person the first capture registered.
registration_form, visit_form = facade.forms()[:2]
seed = draw_seed()
registration = facade.synthesize(registration_form, seed=seed)
await facade.submit(registration.model_dump_json(exclude_none=True, by_alias=True).encode())
visit = facade.synthesize(visit_form, seed=seed)
await facade.submit(visit.model_dump_json(exclude_none=True, by_alias=True).encode())
A fresh seed gives each run its own identifiers, because DHIS2 never lets a deleted identifier be used again. In a real
service the responses arrive from whoever submits them, and synthesize is not involved.
FHIR out¶
entity = await fetch_tracked_entity(facade.client, tracked_entity_uid)
record = await fetch_tracked_entity_record(facade.client, tracked_entity_uid)
index = TrackedEntityIndex.from_store(facade.project, facade.store)
published = next(kind for kind in index.tracked_entity_types if kind.uid == entity.trackedEntityType)
person = registered_entity_for(entity, index, published.resource_type)
projection = RecordProjection(
naming=facade.naming,
store=facade.store,
indexes=facade.indexes,
timezone=facade.project.config.generate.timezone,
)
visits = projection.project(record, record.events).responses
| What you sent | What comes back | What it holds |
|---|---|---|
| A registration | The resource the guide maps that tracked entity type to, here a Patient |
The attributes DHIS2 declares unique as identifier, and every attribute value as an extension |
| A visit | A QuestionnaireResponse |
The answers, on the same form you submitted against |
A visit comes back in the shape it went in, because the form a capture is checked against is the form its event is read
back through. A person does not. DHIS2 holds a person as attribute values, so a Patient has no name or birthDate
unless a project nominates them under [ips.identity]: see
identity_nominations.py.
Ask questions of it¶
Both evaluators read FHIR JSON, and neither knows the data came from DHIS2. FHIRPath asks the document you sent and the document you got back the same question:
ANSWERED_ITEMS = "QuestionnaireResponse.repeat(item).where(answer.exists())"
items = FHIRPathEvaluator().evaluate(ANSWERED_ITEMS, document)
The step exits with an error if an answer comes back changed, if a capture is refused, or if there was nothing to compare. It compares one visit on one form, so it shows the path works rather than that every value type survives it.
CQL reads whatever the Bundle you give it holds, and here that is only what DHIS2 handed back:
LIBRARY = """
library RoundTrip version '1.0'
using FHIR version '4.0.1'
define "People": Count([Patient])
define "Visits": Count([QuestionnaireResponse])
define "Completed Visits": Count([QuestionnaireResponse] Visit where Visit.status = 'completed')
"""
evaluator = CQLEvaluator(data_source=BundleDataSource(bundle))
evaluator.compile(LIBRARY)
evaluator.evaluate_definition("Completed Visits")
$ uv run python examples/facade/step11_round_trip.py
into DHIS2: the person and their visit
read back: Patient/UiRBU1AL8R5 with 1 visit(s)
FHIRPath: 3 answers sent, 3 came back, 3 identical
FHIRPath: the person carries 2 attribute values
CQL: People = 1
CQL: Visits = 1
CQL: Completed Visits = 1
deleted UiRBU1AL8R5
FHIRPath, CQL, and quality measures cover what these can ask.
Whose rights apply¶
This page reads and writes as the connection's account. Behind mount_facade(app, read_as_caller=True) the same round
trip runs as each caller: a capture posts at once under their credentials, and what they read back is what DHIS2 shows them.
See Who can do what.
Next: Mount the routes.