Coverage for tasks/psychiatricclerking.py: 43%

183 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/psychiatricclerking.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26""" 

27 

28from typing import Dict, List, Optional 

29 

30import cardinal_pythonlib.rnc_web as ws 

31from sqlalchemy.orm import Mapped, mapped_column 

32from sqlalchemy.sql.sqltypes import UnicodeText 

33 

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) 

48 

49 

50# ============================================================================= 

51# PsychiatricClerking 

52# ============================================================================= 

53 

54 

55class PsychiatricClerking( # type: ignore[misc] 

56 TaskHasPatientMixin, TaskHasClinicianMixin, Task, Base 

57): 

58 """ 

59 Server implementation of the Clerking task. 

60 """ 

61 

62 __tablename__ = "psychiatricclerking" 

63 shortname = "Clerking" 

64 info_filename_stem = "clinical" 

65 

66 # FIELDSPEC_A = CLINICIAN_FIELDSPECS # replaced by has_clinician, then by TaskHasClinicianMixin # noqa 

67 

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) 

74 

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 ) 

91 

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) 

103 

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 ) 

119 

120 assessment_scales: Mapped[Optional[str]] = mapped_column(UnicodeText) 

121 investigations_results: Mapped[Optional[str]] = mapped_column(UnicodeText) 

122 

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 ) 

128 

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) 

134 

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 ] 

188 

189 @staticmethod 

190 def longname(req: "CamcopsRequest") -> str: 

191 _ = req.gettext 

192 return _("Psychiatric clerking") 

193 

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 ) 

200 

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 ) 

208 

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 ) 

216 

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 

247 

248 # noinspection PyMethodOverriding 

249 @staticmethod 

250 def is_complete() -> bool: 

251 return True 

252 

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 ) 

257 

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 ) 

262 

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 ) 

267 

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 ) 

272 

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 ) 

278 

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 

304 

305 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

306 refinement = {} # type: Dict[SnomedConcept, str] 

307 

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 

314 

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) 

321 

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 ) 

360 

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) 

378 

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 ) 

399 

400 add(SnomedLookup.PSYCLERK_ASSESSMENT_SCALES, self.assessment_scales) 

401 add( 

402 SnomedLookup.PSYCLERK_INVESTIGATIONS_RESULTS, 

403 self.investigations_results, 

404 ) 

405 

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 ) 

412 

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) 

421 

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