Coverage for tasks/psychiatricclerking.py: 43%
183 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/psychiatricclerking.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26"""
28from typing import Dict, List, Optional
30import cardinal_pythonlib.rnc_web as ws
31from sqlalchemy.orm import Mapped, mapped_column
32from sqlalchemy.sql.sqltypes import UnicodeText
34from camcops_server.cc_modules.cc_constants import CssClass
35from camcops_server.cc_modules.cc_ctvinfo import CtvInfo
36from camcops_server.cc_modules.cc_request import CamcopsRequest
37from camcops_server.cc_modules.cc_snomed import (
38 SnomedConcept,
39 SnomedExpression,
40 SnomedLookup,
41)
42from camcops_server.cc_modules.cc_sqlalchemy import Base
43from camcops_server.cc_modules.cc_task import (
44 Task,
45 TaskHasClinicianMixin,
46 TaskHasPatientMixin,
47)
50# =============================================================================
51# PsychiatricClerking
52# =============================================================================
55class PsychiatricClerking( # type: ignore[misc]
56 TaskHasPatientMixin, TaskHasClinicianMixin, Task, Base
57):
58 """
59 Server implementation of the Clerking task.
60 """
62 __tablename__ = "psychiatricclerking"
63 shortname = "Clerking"
64 info_filename_stem = "clinical"
66 # FIELDSPEC_A = CLINICIAN_FIELDSPECS # replaced by has_clinician, then by TaskHasClinicianMixin # noqa
68 location: Mapped[Optional[str]] = mapped_column("location", UnicodeText)
69 contact_type: Mapped[Optional[str]] = mapped_column(UnicodeText)
70 reason_for_contact: Mapped[Optional[str]] = mapped_column(UnicodeText)
71 presenting_issue: Mapped[Optional[str]] = mapped_column(UnicodeText)
72 systems_review: Mapped[Optional[str]] = mapped_column(UnicodeText)
73 collateral_history: Mapped[Optional[str]] = mapped_column(UnicodeText)
75 diagnoses_psychiatric: Mapped[Optional[str]] = mapped_column(UnicodeText)
76 diagnoses_medical: Mapped[Optional[str]] = mapped_column(UnicodeText)
77 operations_procedures: Mapped[Optional[str]] = mapped_column(UnicodeText)
78 allergies_adverse_reactions: Mapped[Optional[str]] = mapped_column(
79 UnicodeText
80 )
81 medications: Mapped[Optional[str]] = mapped_column(UnicodeText)
82 recreational_drug_use: Mapped[Optional[str]] = mapped_column(UnicodeText)
83 family_history: Mapped[Optional[str]] = mapped_column(UnicodeText)
84 developmental_history: Mapped[Optional[str]] = mapped_column(UnicodeText)
85 personal_history: Mapped[Optional[str]] = mapped_column(UnicodeText)
86 premorbid_personality: Mapped[Optional[str]] = mapped_column(UnicodeText)
87 forensic_history: Mapped[Optional[str]] = mapped_column(UnicodeText)
88 current_social_situation: Mapped[Optional[str]] = mapped_column(
89 UnicodeText
90 )
92 mse_appearance_behaviour: Mapped[Optional[str]] = mapped_column(
93 UnicodeText
94 )
95 mse_speech: Mapped[Optional[str]] = mapped_column(UnicodeText)
96 mse_mood_subjective: Mapped[Optional[str]] = mapped_column(UnicodeText)
97 mse_mood_objective: Mapped[Optional[str]] = mapped_column(UnicodeText)
98 mse_thought_form: Mapped[Optional[str]] = mapped_column(UnicodeText)
99 mse_thought_content: Mapped[Optional[str]] = mapped_column(UnicodeText)
100 mse_perception: Mapped[Optional[str]] = mapped_column(UnicodeText)
101 mse_cognition: Mapped[Optional[str]] = mapped_column(UnicodeText)
102 mse_insight: Mapped[Optional[str]] = mapped_column(UnicodeText)
104 physical_examination_general: Mapped[Optional[str]] = mapped_column(
105 UnicodeText
106 )
107 physical_examination_cardiovascular: Mapped[Optional[str]] = mapped_column(
108 UnicodeText
109 )
110 physical_examination_respiratory: Mapped[Optional[str]] = mapped_column(
111 UnicodeText
112 )
113 physical_examination_abdominal: Mapped[Optional[str]] = mapped_column(
114 UnicodeText
115 )
116 physical_examination_neurological: Mapped[Optional[str]] = mapped_column(
117 UnicodeText
118 )
120 assessment_scales: Mapped[Optional[str]] = mapped_column(UnicodeText)
121 investigations_results: Mapped[Optional[str]] = mapped_column(UnicodeText)
123 safety_alerts: Mapped[Optional[str]] = mapped_column(UnicodeText)
124 risk_assessment: Mapped[Optional[str]] = mapped_column(UnicodeText)
125 relevant_legal_information: Mapped[Optional[str]] = mapped_column(
126 UnicodeText
127 )
129 current_problems: Mapped[Optional[str]] = mapped_column(UnicodeText)
130 patient_carer_concerns: Mapped[Optional[str]] = mapped_column(UnicodeText)
131 impression: Mapped[Optional[str]] = mapped_column(UnicodeText)
132 management_plan: Mapped[Optional[str]] = mapped_column(UnicodeText)
133 information_given: Mapped[Optional[str]] = mapped_column(UnicodeText)
135 FIELDS_B = [
136 "location",
137 "contact_type",
138 "reason_for_contact",
139 "presenting_issue",
140 "systems_review",
141 "collateral_history",
142 ]
143 FIELDS_C = [
144 "diagnoses_psychiatric",
145 "diagnoses_medical",
146 "operations_procedures",
147 "allergies_adverse_reactions",
148 "medications",
149 "recreational_drug_use",
150 "family_history",
151 "developmental_history",
152 "personal_history",
153 "premorbid_personality",
154 "forensic_history",
155 "current_social_situation",
156 ]
157 FIELDS_MSE = [
158 "mse_appearance_behaviour",
159 "mse_speech",
160 "mse_mood_subjective",
161 "mse_mood_objective",
162 "mse_thought_form",
163 "mse_thought_content",
164 "mse_perception",
165 "mse_cognition",
166 "mse_insight",
167 ]
168 FIELDS_PE = [
169 "physical_examination_general",
170 "physical_examination_cardiovascular",
171 "physical_examination_respiratory",
172 "physical_examination_abdominal",
173 "physical_examination_neurological",
174 ]
175 FIELDS_D = ["assessment_scales", "investigations_results"]
176 FIELDS_E = [
177 "safety_alerts",
178 "risk_assessment",
179 "relevant_legal_information",
180 ]
181 FIELDS_F = [
182 "current_problems",
183 "patient_carer_concerns",
184 "impression",
185 "management_plan",
186 "information_given",
187 ]
189 @staticmethod
190 def longname(req: "CamcopsRequest") -> str:
191 _ = req.gettext
192 return _("Psychiatric clerking")
194 def get_ctv_heading(
195 self, req: CamcopsRequest, wstringname: str
196 ) -> CtvInfo:
197 return CtvInfo(
198 heading=self.wxstring(req, wstringname), skip_if_no_content=False
199 )
201 def get_ctv_subheading(
202 self, req: CamcopsRequest, wstringname: str
203 ) -> CtvInfo:
204 return CtvInfo(
205 subheading=self.wxstring(req, wstringname),
206 skip_if_no_content=False,
207 )
209 def get_ctv_description_content(
210 self, req: CamcopsRequest, x: str
211 ) -> CtvInfo:
212 return CtvInfo(
213 description=self.wxstring(req, x),
214 content=ws.webify(getattr(self, x)),
215 )
217 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
218 infolist = [self.get_ctv_heading(req, "heading_current_contact")]
219 for x in self.FIELDS_B:
220 infolist.append(self.get_ctv_description_content(req, x))
221 infolist.append(self.get_ctv_heading(req, "heading_background"))
222 for x in self.FIELDS_C:
223 infolist.append(self.get_ctv_description_content(req, x))
224 infolist.append(
225 self.get_ctv_heading(req, "heading_examination_investigations")
226 )
227 infolist.append(
228 self.get_ctv_subheading(req, "mental_state_examination")
229 )
230 for x in self.FIELDS_MSE:
231 infolist.append(self.get_ctv_description_content(req, x))
232 infolist.append(self.get_ctv_subheading(req, "physical_examination"))
233 for x in self.FIELDS_PE:
234 infolist.append(self.get_ctv_description_content(req, x))
235 infolist.append(
236 self.get_ctv_subheading(req, "assessments_and_investigations")
237 )
238 for x in self.FIELDS_D:
239 infolist.append(self.get_ctv_description_content(req, x))
240 infolist.append(self.get_ctv_heading(req, "heading_risk_legal"))
241 for x in self.FIELDS_E:
242 infolist.append(self.get_ctv_description_content(req, x))
243 infolist.append(self.get_ctv_heading(req, "heading_summary_plan"))
244 for x in self.FIELDS_F:
245 infolist.append(self.get_ctv_description_content(req, x))
246 return infolist
248 # noinspection PyMethodOverriding
249 @staticmethod
250 def is_complete() -> bool:
251 return True
253 def heading(self, req: CamcopsRequest, wstringname: str) -> str:
254 return '<div class="{CssClass.HEADING}">{s}</div>'.format(
255 CssClass=CssClass, s=self.wxstring(req, wstringname)
256 )
258 def subheading(self, req: CamcopsRequest, wstringname: str) -> str:
259 return '<div class="{CssClass.SUBHEADING}">{s}</div>'.format(
260 CssClass=CssClass, s=self.wxstring(req, wstringname)
261 )
263 def subsubheading(self, req: CamcopsRequest, wstringname: str) -> str:
264 return '<div class="{CssClass.SUBSUBHEADING}">{s}</div>'.format(
265 CssClass=CssClass, s=self.wxstring(req, wstringname)
266 )
268 def subhead_text(self, req: CamcopsRequest, fieldname: str) -> str:
269 return self.subheading(req, fieldname) + "<div><b>{}</b></div>".format(
270 ws.webify(getattr(self, fieldname))
271 )
273 def subsubhead_text(self, req: CamcopsRequest, fieldname: str) -> str:
274 return (
275 self.subsubheading(req, fieldname)
276 + f"<div><b>{ws.webify(getattr(self, fieldname))}</b></div>"
277 )
279 def get_task_html(self, req: CamcopsRequest) -> str:
280 # Avoid tables - PDF generator crashes if text is too long.
281 html = ""
282 html += self.heading(req, "heading_current_contact")
283 for x in self.FIELDS_B:
284 html += self.subhead_text(req, x)
285 html += self.heading(req, "heading_background")
286 for x in self.FIELDS_C:
287 html += self.subhead_text(req, x)
288 html += self.heading(req, "heading_examination_investigations")
289 html += self.subheading(req, "mental_state_examination")
290 for x in self.FIELDS_MSE:
291 html += self.subsubhead_text(req, x)
292 html += self.subheading(req, "physical_examination")
293 for x in self.FIELDS_PE:
294 html += self.subsubhead_text(req, x)
295 for x in self.FIELDS_D:
296 html += self.subhead_text(req, x)
297 html += self.heading(req, "heading_risk_legal")
298 for x in self.FIELDS_E:
299 html += self.subhead_text(req, x)
300 html += self.heading(req, "heading_summary_plan")
301 for x in self.FIELDS_F:
302 html += self.subhead_text(req, x)
303 return html
305 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
306 refinement = {} # type: Dict[SnomedConcept, str]
308 def add(snomed_lookup: str, contents: Optional[str]) -> None:
309 if not contents:
310 return
311 nonlocal refinement
312 concept = req.snomed(snomed_lookup)
313 refinement[concept] = contents
315 # not location
316 # not contact type
317 add(SnomedLookup.PSYCLERK_REASON_FOR_REFERRAL, self.reason_for_contact)
318 add(SnomedLookup.PSYCLERK_PRESENTING_ISSUE, self.presenting_issue)
319 add(SnomedLookup.PSYCLERK_SYSTEMS_REVIEW, self.systems_review)
320 add(SnomedLookup.PSYCLERK_COLLATERAL_HISTORY, self.collateral_history)
322 add(
323 SnomedLookup.PSYCLERK_PAST_MEDICAL_SURGICAL_MENTAL_HEALTH_HISTORY,
324 self.diagnoses_medical,
325 )
326 add(
327 SnomedLookup.PSYCLERK_PAST_MEDICAL_SURGICAL_MENTAL_HEALTH_HISTORY,
328 self.diagnoses_psychiatric,
329 )
330 add(SnomedLookup.PSYCLERK_PROCEDURES, self.operations_procedures)
331 add(
332 SnomedLookup.PSYCLERK_ALLERGIES_ADVERSE_REACTIONS,
333 self.allergies_adverse_reactions,
334 )
335 add(
336 SnomedLookup.PSYCLERK_MEDICATIONS_MEDICAL_DEVICES, self.medications
337 )
338 add(
339 SnomedLookup.PSYCLERK_DRUG_SUBSTANCE_USE,
340 self.recreational_drug_use,
341 )
342 add(SnomedLookup.PSYCLERK_FAMILY_HISTORY, self.family_history)
343 add(
344 SnomedLookup.PSYCLERK_DEVELOPMENTAL_HISTORY,
345 self.developmental_history,
346 )
347 add(
348 SnomedLookup.PSYCLERK_SOCIAL_PERSONAL_HISTORY,
349 self.personal_history,
350 )
351 add(SnomedLookup.PSYCLERK_PERSONALITY, self.premorbid_personality)
352 add(
353 SnomedLookup.PSYCLERK_PRISON_RECORD_CRIMINAL_ACTIVITY,
354 self.forensic_history,
355 )
356 add(
357 SnomedLookup.PSYCLERK_SOCIAL_HISTORY_BASELINE,
358 self.current_social_situation,
359 )
361 add(
362 SnomedLookup.PSYCLERK_MSE_APPEARANCE, self.mse_appearance_behaviour
363 ) # duplication
364 add(
365 SnomedLookup.PSYCLERK_MSE_BEHAVIOUR, self.mse_appearance_behaviour
366 ) # duplication
367 add(SnomedLookup.PSYCLERK_MSE_MOOD, self.mse_mood_subjective) # close
368 add(SnomedLookup.PSYCLERK_MSE_AFFECT, self.mse_mood_objective)
369 # ... Logic here: "objective mood" is certainly affect (emotional
370 # weather). "Subjective mood" is both mood (emotional climate) and
371 # affect. Not perfect, but reasonable.
372 add(SnomedLookup.PSYCLERK_MSE_THOUGHT, self.mse_thought_form)
373 add(SnomedLookup.PSYCLERK_MSE_THOUGHT, self.mse_thought_content)
374 # ... No way of disambiguating the two in SNOMED-CT.
375 add(SnomedLookup.PSYCLERK_MSE_PERCEPTION, self.mse_perception)
376 add(SnomedLookup.PSYCLERK_MSE_COGNITION, self.mse_cognition)
377 add(SnomedLookup.PSYCLERK_MSE_INSIGHT, self.mse_insight)
379 add(
380 SnomedLookup.PSYCLERK_PHYSEXAM_GENERAL,
381 self.physical_examination_general,
382 )
383 add(
384 SnomedLookup.PSYCLERK_PHYSEXAM_CARDIOVASCULAR,
385 self.physical_examination_cardiovascular,
386 )
387 add(
388 SnomedLookup.PSYCLERK_PHYSEXAM_RESPIRATORY,
389 self.physical_examination_respiratory,
390 )
391 add(
392 SnomedLookup.PSYCLERK_PHYSEXAM_ABDOMINAL,
393 self.physical_examination_abdominal,
394 )
395 add(
396 SnomedLookup.PSYCLERK_PHYSEXAM_NEUROLOGICAL,
397 self.physical_examination_neurological,
398 )
400 add(SnomedLookup.PSYCLERK_ASSESSMENT_SCALES, self.assessment_scales)
401 add(
402 SnomedLookup.PSYCLERK_INVESTIGATIONS_RESULTS,
403 self.investigations_results,
404 )
406 add(SnomedLookup.PSYCLERK_SAFETY_ALERTS, self.safety_alerts)
407 add(SnomedLookup.PSYCLERK_RISK_ASSESSMENT, self.risk_assessment)
408 add(
409 SnomedLookup.PSYCLERK_RELEVANT_LEGAL_INFORMATION,
410 self.relevant_legal_information,
411 )
413 add(SnomedLookup.PSYCLERK_CURRENT_PROBLEMS, self.current_problems)
414 add(
415 SnomedLookup.PSYCLERK_PATIENT_CARER_CONCERNS,
416 self.patient_carer_concerns,
417 )
418 add(SnomedLookup.PSYCLERK_CLINICAL_NARRATIVE, self.impression)
419 add(SnomedLookup.PSYCLERK_MANAGEMENT_PLAN, self.management_plan)
420 add(SnomedLookup.PSYCLERK_INFORMATION_GIVEN, self.information_given)
422 codes = [
423 SnomedExpression(
424 req.snomed(
425 SnomedLookup.DIAGNOSTIC_PSYCHIATRIC_INTERVIEW_PROCEDURE
426 ),
427 refinement=refinement or None, # type: ignore[arg-type]
428 )
429 ]
430 return codes