Skip to content

Commit 6eec946

Browse files
authored
Merge pull request #27 from helpwave/patch/patient-location-split
split up patient location into clinic, position and teams
2 parents 61fb701 + 1b37174 commit 6eec946

19 files changed

Lines changed: 1108 additions & 115 deletions

File tree

backend/api/inputs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class CreatePatientInput:
6969
sex: Sex
7070
assigned_location_id: strawberry.ID | None = None
7171
assigned_location_ids: list[strawberry.ID] | None = None
72+
clinic_id: strawberry.ID # Required: location node from kind CLINIC
73+
position_id: strawberry.ID | None = None # Optional: location node from type hospital, practice, clinic, ward, bed or room
74+
team_ids: list[strawberry.ID] | None = None # Array: location nodes from type clinic, team, practice, hospital
7275
properties: list[PropertyValueInput] | None = None
7376
state: PatientState | None = None
7477

@@ -81,6 +84,9 @@ class UpdatePatientInput:
8184
sex: Sex | None = None
8285
assigned_location_id: strawberry.ID | None = None
8386
assigned_location_ids: list[strawberry.ID] | None = None
87+
clinic_id: strawberry.ID | None = None # Location node from kind CLINIC
88+
position_id: strawberry.ID | None = None # Optional: location node from type hospital, practice, clinic, ward, bed or room
89+
team_ids: list[strawberry.ID] | None = None # Array: location nodes from type clinic, team, practice, hospital
8490
properties: list[PropertyValueInput] | None = None
8591

8692

backend/api/resolvers/patient.py

Lines changed: 146 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,35 @@
1212
from .utils import process_properties
1313

1414

15+
def validate_location_kind(location: models.LocationNode, expected_kind: str, field_name: str) -> None:
16+
"""Validate that a location has the expected kind."""
17+
if location.kind.upper() != expected_kind.upper():
18+
raise Exception(
19+
f"{field_name} must be a location of kind {expected_kind}, "
20+
f"but got {location.kind}"
21+
)
22+
23+
24+
def validate_position_kind(location: models.LocationNode, field_name: str) -> None:
25+
"""Validate that a location is a valid position type."""
26+
allowed_kinds = {"HOSPITAL", "PRACTICE", "CLINIC", "WARD", "BED", "ROOM"}
27+
if location.kind.upper() not in allowed_kinds:
28+
raise Exception(
29+
f"{field_name} must be a location of kind HOSPITAL, PRACTICE, CLINIC, "
30+
f"WARD, BED, or ROOM, but got {location.kind}"
31+
)
32+
33+
34+
def validate_team_kind(location: models.LocationNode, field_name: str) -> None:
35+
"""Validate that a location is a valid team type."""
36+
allowed_kinds = {"CLINIC", "TEAM", "PRACTICE", "HOSPITAL"}
37+
if location.kind.upper() not in allowed_kinds:
38+
raise Exception(
39+
f"{field_name} must be a location of kind CLINIC, TEAM, PRACTICE, "
40+
f"or HOSPITAL, but got {location.kind}"
41+
)
42+
43+
1544
@strawberry.type
1645
class PatientQuery:
1746
@strawberry.field
@@ -26,6 +55,7 @@ async def patient(
2655
.options(
2756
selectinload(models.Patient.assigned_locations),
2857
selectinload(models.Patient.tasks),
58+
selectinload(models.Patient.teams),
2959
),
3060
)
3161
return result.scalars().first()
@@ -40,6 +70,7 @@ async def patients(
4070
query = select(models.Patient).options(
4171
selectinload(models.Patient.assigned_locations),
4272
selectinload(models.Patient.tasks),
73+
selectinload(models.Patient.teams),
4374
)
4475

4576
if states:
@@ -88,6 +119,7 @@ async def recent_patients(
88119
.options(
89120
selectinload(models.Patient.assigned_locations),
90121
selectinload(models.Patient.tasks),
122+
selectinload(models.Patient.teams),
91123
)
92124
.limit(limit)
93125
)
@@ -103,26 +135,70 @@ async def create_patient(
103135
info: Info,
104136
data: CreatePatientInput,
105137
) -> PatientType:
138+
db = info.context.db
106139
initial_state = data.state.value if data.state else PatientState.WAIT.value
140+
141+
clinic_result = await db.execute(
142+
select(models.LocationNode).where(
143+
models.LocationNode.id == data.clinic_id,
144+
),
145+
)
146+
clinic = clinic_result.scalars().first()
147+
if not clinic:
148+
raise Exception(f"Clinic location with id {data.clinic_id} not found")
149+
validate_location_kind(clinic, "CLINIC", "clinic_id")
150+
151+
position = None
152+
if data.position_id:
153+
position_result = await db.execute(
154+
select(models.LocationNode).where(
155+
models.LocationNode.id == data.position_id,
156+
),
157+
)
158+
position = position_result.scalars().first()
159+
if not position:
160+
raise Exception(f"Position location with id {data.position_id} not found")
161+
validate_position_kind(position, "position_id")
162+
163+
teams = []
164+
if data.team_ids:
165+
teams_result = await db.execute(
166+
select(models.LocationNode).where(
167+
models.LocationNode.id.in_(data.team_ids),
168+
),
169+
)
170+
teams = list(teams_result.scalars().all())
171+
if len(teams) != len(data.team_ids):
172+
found_ids = {t.id for t in teams}
173+
missing_ids = set(data.team_ids) - found_ids
174+
raise Exception(f"Team locations with ids {missing_ids} not found")
175+
for team in teams:
176+
validate_team_kind(team, "team_ids")
177+
107178
new_patient = models.Patient(
108179
firstname=data.firstname,
109180
lastname=data.lastname,
110181
birthdate=data.birthdate,
111182
sex=data.sex.value,
112183
state=initial_state,
113184
assigned_location_id=data.assigned_location_id,
185+
clinic_id=data.clinic_id,
186+
position_id=data.position_id,
114187
)
115188

189+
if teams:
190+
new_patient.teams = teams
191+
116192
if data.assigned_location_ids:
117-
result = await info.context.db.execute(
193+
result = await db.execute(
118194
select(models.LocationNode).where(
119195
models.LocationNode.id.in_(data.assigned_location_ids),
120196
),
121197
)
122198
locations = result.scalars().all()
123199
new_patient.assigned_locations = list(locations)
124200
elif data.assigned_location_id:
125-
result = await info.context.db.execute(
201+
result = await db.execute(
126202
select(models.LocationNode).where(
127203
models.LocationNode.id == data.assigned_location_id,
128204
),
@@ -133,16 +209,16 @@ async def create_patient(
133209

134210
if data.properties:
135211
await process_properties(
136-
info.context.db,
212+
db,
137213
new_patient,
138214
data.properties,
139215
"patient",
140216
)
141217

142-
info.context.db.add(new_patient)
143-
await info.context.db.commit()
218+
db.add(new_patient)
219+
await db.commit()
144220

145-
await info.context.db.refresh(new_patient, ["assigned_locations"])
221+
await db.refresh(new_patient, ["assigned_locations", "teams"])
146222
await redis_client.publish("patient_created", new_patient.id)
147223
return new_patient
148224

@@ -157,7 +233,10 @@ async def update_patient(
157233
result = await db.execute(
158234
select(models.Patient)
159235
.where(models.Patient.id == id)
160-
.options(selectinload(models.Patient.assigned_locations)),
236+
.options(
237+
selectinload(models.Patient.assigned_locations),
238+
selectinload(models.Patient.teams),
239+
),
161240
)
162241
patient = result.scalars().first()
163242
if not patient:
@@ -172,6 +251,49 @@ async def update_patient(
172251
if data.sex is not None:
173252
patient.sex = data.sex.value
174253

254+
# Update clinic (if provided)
255+
if data.clinic_id is not None:
256+
clinic_result = await db.execute(
257+
select(models.LocationNode).where(
258+
models.LocationNode.id == data.clinic_id,
259+
),
260+
)
261+
clinic = clinic_result.scalars().first()
262+
if not clinic:
263+
raise Exception(f"Clinic location with id {data.clinic_id} not found")
264+
validate_location_kind(clinic, "CLINIC", "clinic_id")
265+
patient.clinic_id = data.clinic_id
266+
267+
if data.position_id is not None:
268+
position_result = await db.execute(
269+
select(models.LocationNode).where(
270+
models.LocationNode.id == data.position_id,
271+
),
272+
)
273+
position = position_result.scalars().first()
274+
if not position:
275+
raise Exception(f"Position location with id {data.position_id} not found")
276+
validate_position_kind(position, "position_id")
277+
patient.position_id = data.position_id
278+
279+
if data.team_ids is not None:
280+
if len(data.team_ids) == 0:
281+
patient.teams = []
282+
else:
283+
teams_result = await db.execute(
284+
select(models.LocationNode).where(
285+
models.LocationNode.id.in_(data.team_ids),
286+
),
287+
)
288+
teams = list(teams_result.scalars().all())
289+
if len(teams) != len(data.team_ids):
290+
found_ids = {t.id for t in teams}
291+
missing_ids = set(data.team_ids) - found_ids
292+
raise Exception(f"Team locations with ids {missing_ids} not found")
293+
for team in teams:
294+
validate_team_kind(team, "team_ids")
295+
patient.teams = teams
296+
175297
if data.assigned_location_ids is not None:
176298
result = await db.execute(
177299
select(models.LocationNode).where(
@@ -196,7 +318,7 @@ async def update_patient(
196318
await process_properties(db, patient, data.properties, "patient")
197319

198320
await db.commit()
199-
await db.refresh(patient, ["assigned_locations"])
321+
await db.refresh(patient, ["assigned_locations", "teams"])
200322
return patient
201323

202324
@strawberry.mutation
@@ -218,7 +340,10 @@ async def admit_patient(self, info: Info, id: strawberry.ID) -> PatientType:
218340
result = await db.execute(
219341
select(models.Patient)
220342
.where(models.Patient.id == id)
221-
.options(selectinload(models.Patient.assigned_locations)),
343+
.options(
344+
selectinload(models.Patient.assigned_locations),
345+
selectinload(models.Patient.teams),
346+
),
222347
)
223348
patient = result.scalars().first()
224349
if not patient:
@@ -234,7 +359,10 @@ async def discharge_patient(self, info: Info, id: strawberry.ID) -> PatientType:
234359
result = await db.execute(
235360
select(models.Patient)
236361
.where(models.Patient.id == id)
237-
.options(selectinload(models.Patient.assigned_locations)),
362+
.options(
363+
selectinload(models.Patient.assigned_locations),
364+
selectinload(models.Patient.teams),
365+
),
238366
)
239367
patient = result.scalars().first()
240368
if not patient:
@@ -250,7 +378,10 @@ async def mark_patient_dead(self, info: Info, id: strawberry.ID) -> PatientType:
250378
result = await db.execute(
251379
select(models.Patient)
252380
.where(models.Patient.id == id)
253-
.options(selectinload(models.Patient.assigned_locations)),
381+
.options(
382+
selectinload(models.Patient.assigned_locations),
383+
selectinload(models.Patient.teams),
384+
),
254385
)
255386
patient = result.scalars().first()
256387
if not patient:
@@ -266,7 +397,10 @@ async def wait_patient(self, info: Info, id: strawberry.ID) -> PatientType:
266397
result = await db.execute(
267398
select(models.Patient)
268399
.where(models.Patient.id == id)
269-
.options(selectinload(models.Patient.assigned_locations)),
400+
.options(
401+
selectinload(models.Patient.assigned_locations),
402+
selectinload(models.Patient.teams),
403+
),
270404
)
271405
patient = result.scalars().first()
272406
if not patient:

backend/api/types/patient.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class PatientType:
2323
sex: Sex
2424
state: PatientState
2525
assigned_location_id: strawberry.ID | None
26+
clinic_id: strawberry.ID
27+
position_id: strawberry.ID | None
2628

2729
@strawberry.field
2830
def name(self) -> str:
@@ -81,6 +83,57 @@ async def assigned_locations(
8183
await info.context.db.refresh(self, ["assigned_locations"])
8284
return self.assigned_locations or []
8385

86+
@strawberry.field
87+
async def clinic(
88+
self,
89+
info: Info,
90+
) -> Annotated[
91+
"LocationNodeType",
92+
strawberry.lazy("api.types.location"),
93+
]:
94+
result = await info.context.db.execute(
95+
select(models.LocationNode).where(
96+
models.LocationNode.id == self.clinic_id,
97+
),
98+
)
99+
clinic = result.scalars().first()
100+
if not clinic:
101+
raise Exception(f"Clinic location not found for patient {self.id}")
102+
return clinic
103+
104+
@strawberry.field
105+
async def position(
106+
self,
107+
info: Info,
108+
) -> (
109+
Annotated[
110+
"LocationNodeType",
111+
strawberry.lazy("api.types.location"),
112+
]
113+
| None
114+
):
115+
if not self.position_id:
116+
return None
117+
result = await info.context.db.execute(
118+
select(models.LocationNode).where(
119+
models.LocationNode.id == self.position_id,
120+
),
121+
)
122+
return result.scalars().first()
123+
124+
@strawberry.field
125+
async def teams(
126+
self,
127+
info: Info,
128+
) -> list[
129+
Annotated[
130+
"LocationNodeType",
131+
strawberry.lazy("api.types.location"),
132+
]
133+
]:
134+
await info.context.db.refresh(self, ["teams"])
135+
return self.teams or []
136+
84137
@strawberry.field
85138
async def tasks(
86139
self,

0 commit comments

Comments
 (0)