Coverage for tasks/psychiatricclerking.py: 43%

1"""

2 camcops_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

28 from typing import Dict, List, Optional

29

30 import cardinal_pythonlib.rnc_web as ws

31 from sqlalchemy.orm import Mapped, mapped_column

32 from sqlalchemy.sql.sqltypes import UnicodeText

33

34 from camcops_server.cc_modules.cc_constants import CssClass

35 from camcops_server.cc_modules.cc_ctvinfo import CtvInfo

36 from camcops_server.cc_modules.cc_request import CamcopsRequest

37 from camcops_server.cc_modules.cc_snomed import (

38 SnomedConcept,

39 SnomedExpression,

40 SnomedLookup,

41)

42 from camcops_server.cc_modules.cc_sqlalchemy import Base

43 from camcops_server.cc_modules.cc_task import (

44 Task,

45 TaskHasClinicianMixin,

46 TaskHasPatientMixin,

47)

48

49

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

51# PsychiatricClerking

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

53

54

55 class 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