Coverage for tasks/lynall_iam_medical.py: 54%

123 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/lynall_iam_medical.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Optional, Union 

31 

32from sqlalchemy.sql.schema import Column 

33from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_html import ( 

37 get_yes_no, 

38 get_yes_no_none, 

39 tr_qa, 

40) 

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_sqla_coltypes import ( 

43 BoolColumn, 

44 CamcopsColumn, 

45 PermittedValueChecker, 

46) 

47from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

48from camcops_server.cc_modules.cc_text import SS 

49 

50 

51# ============================================================================= 

52# Lynall1MedicalHistory 

53# ============================================================================= 

54 

55 

56class LynallIamMedicalHistory(TaskHasPatientMixin, Task): 

57 """ 

58 Server implementation of the Lynall1IamMedicalHistory task. 

59 """ 

60 

61 __tablename__ = "lynall_1_iam_medical" # historically fixed 

62 shortname = "Lynall_IAM_Medical" 

63 extrastring_taskname = "lynall_iam_medical" 

64 info_filename_stem = extrastring_taskname 

65 

66 Q2_N_OPTIONS = 6 

67 Q3_N_OPTIONS = 11 

68 Q4_N_OPTIONS = 5 

69 Q4_OPTION_PSYCH_BEFORE_PHYSICAL = 1 

70 Q4_OPTION_PSYCH_AFTER_PHYSICAL = 2 

71 Q8_N_OPTIONS = 2 

72 Q7B_MIN = 1 

73 Q7B_MAX = 10 

74 

75 q1_age_first_inflammatory_sx = Column( 

76 "q1_age_first_inflammatory_sx", 

77 Integer, 

78 comment="Age (y) at onset of first symptoms of inflammatory disease", 

79 ) 

80 q2_when_psych_sx_started = CamcopsColumn( 

81 "q2_when_psych_sx_started", 

82 Integer, 

83 permitted_value_checker=PermittedValueChecker( 

84 minimum=1, maximum=Q2_N_OPTIONS 

85 ), 

86 comment="Timing of onset of psych symptoms (1 = NA, 2 = before " 

87 "physical symptoms [Sx], 3 = same time as physical Sx but " 

88 "before diagnosis [Dx], 4 = around time of Dx, 5 = weeks or " 

89 "months after Dx, 6 = years after Dx)", 

90 ) 

91 q3_worst_symptom_last_month = CamcopsColumn( 

92 "q3_worst_symptom_last_month", 

93 Integer, 

94 permitted_value_checker=PermittedValueChecker( 

95 minimum=1, maximum=Q3_N_OPTIONS 

96 ), 

97 comment="Worst symptom in last month (1 = fatigue, 2 = low mood, 3 = " 

98 "irritable, 4 = anxiety, 5 = brain fog/confused, 6 = pain, " 

99 "7 = bowel Sx, 8 = mobility, 9 = skin, 10 = other, 11 = no Sx " 

100 "in past month)", 

101 ) 

102 q4a_symptom_timing = CamcopsColumn( 

103 "q4a_symptom_timing", 

104 Integer, 

105 permitted_value_checker=PermittedValueChecker( 

106 minimum=1, maximum=Q4_N_OPTIONS 

107 ), 

108 comment="Timing of brain/psych Sx relative to physical Sx (1 = brain " 

109 "before physical, 2 = brain after physical, 3 = same time, " 

110 "4 = no relationship, 5 = none of the above)", 

111 ) 

112 q4b_days_psych_before_phys = Column( 

113 "q4b_days_psych_before_phys", 

114 Integer, 

115 comment="If Q4a == 1, number of days that brain Sx typically begin " 

116 "before physical Sx", 

117 ) 

118 q4c_days_psych_after_phys = Column( 

119 "q4c_days_psych_after_phys", 

120 Integer, 

121 comment="If Q4a == 2, number of days that brain Sx typically begin " 

122 "after physical Sx", 

123 ) 

124 q5_antibiotics = BoolColumn( 

125 "q5_antibiotics", 

126 comment="Medication for infection (e.g. antibiotics) in past 3 months?" 

127 " (0 = no, 1 = yes)", 

128 ) 

129 q6a_inpatient_last_y = BoolColumn( 

130 "q6a_inpatient_last_y", 

131 comment="Inpatient in the last year? (0 = no, 1 = yes)", 

132 ) 

133 q6b_inpatient_weeks = Column( 

134 "q6b_inpatient_weeks", 

135 Integer, 

136 comment="If Q6a is true, approximate number of weeks spent as an " 

137 "inpatient in the past year", 

138 ) 

139 q7a_sx_last_2y = BoolColumn( 

140 "q7a_sx_last_2y", 

141 comment="Symptoms within the last 2 years? (0 = no, 1 = yes)", 

142 ) 

143 q7b_variability = Column( 

144 "q7b_variability", 

145 Integer, 

146 comment="If Q7a is true, degree of variability of symptoms (1-10 " 

147 "where 1 = highly variable [from none to severe], 10 = " 

148 "there all the time)", 

149 ) 

150 q8_smoking = Column( 

151 "q8_smoking", 

152 Integer, 

153 comment="Current smoking status (0 = no, 1 = yes but not every day, " 

154 "2 = every day)", 

155 ) 

156 q9_pregnant = BoolColumn( 

157 "q9_pregnant", comment="Currently pregnant (0 = no or N/A, 1 = yes)" 

158 ) 

159 q10a_effective_rx_physical = Column( 

160 "q10a_effective_rx_physical", 

161 UnicodeText, 

162 comment="Most effective treatments for physical Sx", 

163 ) 

164 q10b_effective_rx_psych = Column( 

165 "q10b_effective_rx_psych", 

166 UnicodeText, 

167 comment="Most effective treatments for brain/psychiatric Sx", 

168 ) 

169 q11a_ph_depression = BoolColumn( 

170 "q11a_ph_depression", comment="Personal history of depression?" 

171 ) 

172 q11b_ph_bipolar = BoolColumn( 

173 "q11b_ph_bipolar", comment="Personal history of bipolar disorder?" 

174 ) 

175 q11c_ph_schizophrenia = BoolColumn( 

176 "q11c_ph_schizophrenia", comment="Personal history of schizophrenia?" 

177 ) 

178 q11d_ph_autistic_spectrum = BoolColumn( 

179 "q11d_ph_autistic_spectrum", 

180 comment="Personal history of autism/Asperger's?", 

181 ) 

182 q11e_ph_ptsd = BoolColumn( 

183 "q11e_ph_ptsd", comment="Personal history of PTSD?" 

184 ) 

185 q11f_ph_other_anxiety = BoolColumn( 

186 "q11f_ph_other_anxiety", 

187 comment="Personal history of other anxiety disorders?", 

188 ) 

189 q11g_ph_personality_disorder = BoolColumn( 

190 "q11g_ph_personality_disorder", 

191 comment="Personal history of personality disorder?", 

192 ) 

193 q11h_ph_other_psych = BoolColumn( 

194 "q11h_ph_other_psych", 

195 comment="Personal history of other psychiatric disorder(s)?", 

196 ) 

197 q11h_ph_other_detail = Column( 

198 "q11h_ph_other_detail", 

199 UnicodeText, 

200 comment="If q11h_ph_other_psych is true, this is the free-text " 

201 "details field", 

202 ) 

203 q12a_fh_depression = BoolColumn( 

204 "q12a_fh_depression", comment="Family history of depression?" 

205 ) 

206 q12b_fh_bipolar = BoolColumn( 

207 "q12b_fh_bipolar", comment="Family history of bipolar disorder?" 

208 ) 

209 q12c_fh_schizophrenia = BoolColumn( 

210 "q12c_fh_schizophrenia", comment="Family history of schizophrenia?" 

211 ) 

212 q12d_fh_autistic_spectrum = BoolColumn( 

213 "q12d_fh_autistic_spectrum", 

214 comment="Family history of autism/Asperger's?", 

215 ) 

216 q12e_fh_ptsd = BoolColumn( 

217 "q12e_fh_ptsd", comment="Family history of PTSD?" 

218 ) 

219 q12f_fh_other_anxiety = BoolColumn( 

220 "q12f_fh_other_anxiety", 

221 comment="Family history of other anxiety disorders?", 

222 ) 

223 q12g_fh_personality_disorder = BoolColumn( 

224 "q12g_fh_personality_disorder", 

225 comment="Family history of personality disorder?", 

226 ) 

227 q12h_fh_other_psych = BoolColumn( 

228 "q12h_fh_other_psych", 

229 comment="Family history of other psychiatric disorder(s)?", 

230 ) 

231 q12h_fh_other_detail = Column( 

232 "q12h_fh_other_detail", 

233 UnicodeText, 

234 comment="If q12h_fh_other_psych is true, this is the free-text " 

235 "details field", 

236 ) 

237 q13a_behcet = BoolColumn( 

238 "q13a_behcet", comment="Behçet’s syndrome? (0 = no, 1 = yes)" 

239 ) 

240 q13b_oral_ulcers = BoolColumn( 

241 "q13b_oral_ulcers", 

242 comment="(If Behçet’s) Oral ulcers? (0 = no, 1 = yes)", 

243 ) 

244 q13c_oral_age_first = Column( 

245 "q13c_oral_age_first", 

246 Integer, 

247 comment="(If Behçet’s + oral) Age (y) at first oral ulcers", 

248 ) 

249 q13d_oral_scarring = BoolColumn( 

250 "q13d_oral_scarring", 

251 comment="(If Behçet’s + oral) Oral scarring? (0 = no, 1 = yes)", 

252 ) 

253 q13e_genital_ulcers = BoolColumn( 

254 "q13e_genital_ulcers", 

255 comment="(If Behçet’s) Genital ulcers? (0 = no, 1 = yes)", 

256 ) 

257 q13f_genital_age_first = Column( 

258 "q13f_genital_age_first", 

259 Integer, 

260 comment="(If Behçet’s + genital) Age (y) at first genital ulcers", 

261 ) 

262 q13g_genital_scarring = BoolColumn( 

263 "q13g_genital_scarring", 

264 comment="(If Behçet’s + genital) Genital scarring? (0 = no, 1 = yes)", 

265 ) 

266 

267 @staticmethod 

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

269 _ = req.gettext 

270 return _("Lynall M-E — 1 — IAM — Medical history") 

271 

272 def is_complete(self) -> bool: 

273 if self.any_fields_none( 

274 [ 

275 "q1_age_first_inflammatory_sx", 

276 "q2_when_psych_sx_started", 

277 "q3_worst_symptom_last_month", 

278 "q4a_symptom_timing", 

279 "q5_antibiotics", 

280 "q6a_inpatient_last_y", 

281 "q7a_sx_last_2y", 

282 "q8_smoking", 

283 "q9_pregnant", 

284 "q10a_effective_rx_physical", 

285 "q10b_effective_rx_psych", 

286 "q13a_behcet", 

287 ] 

288 ): 

289 return False 

290 if self.any_fields_null_or_empty_str( 

291 ["q10a_effective_rx_physical", "q10b_effective_rx_psych"] 

292 ): 

293 return False 

294 q4a = self.q4a_symptom_timing 

295 if ( 

296 q4a == self.Q4_OPTION_PSYCH_BEFORE_PHYSICAL 

297 and self.q4b_days_psych_before_phys is None 

298 ): 

299 return False 

300 if ( 

301 q4a == self.Q4_OPTION_PSYCH_AFTER_PHYSICAL 

302 and self.q4c_days_psych_after_phys is None 

303 ): 

304 return False 

305 if self.q6a_inpatient_last_y and self.q6b_inpatient_weeks is None: 

306 return False 

307 if self.q7a_sx_last_2y and self.q7b_variability is None: 

308 return False 

309 if self.q11h_ph_other_psych and not self.q11h_ph_other_detail: 

310 return False 

311 if self.q12h_fh_other_psych and not self.q12h_fh_other_detail: 

312 return False 

313 if self.q13a_behcet: 

314 if self.any_fields_none( 

315 ["q13b_oral_ulcers", "q13e_genital_ulcers"] 

316 ): 

317 return False 

318 if self.q13b_oral_ulcers: 

319 if self.any_fields_none( 

320 ["q13c_oral_age_first", "q13d_oral_scarring"] 

321 ): 

322 return False 

323 if self.q13e_genital_ulcers: 

324 if self.any_fields_none( 

325 ["q13f_genital_age_first", "q13g_genital_scarring"] 

326 ): 

327 return False 

328 return True 

329 

330 def get_task_html(self, req: CamcopsRequest) -> str: 

331 def plainrow( 

332 qname: str, 

333 xstring_name: str, 

334 value: Any, 

335 if_applicable: bool = False, 

336 qsuffix: str = "", 

337 ) -> str: 

338 ia_str = ( 

339 f"<i>[{req.wsstring(SS.IF_APPLICABLE)}]</i> " 

340 if if_applicable 

341 else "" 

342 ) 

343 q = f"{ia_str}{qname}. {self.wxstring(req, xstring_name)}{qsuffix}" 

344 return tr_qa(q, value) 

345 

346 def lookuprow( 

347 qname: str, 

348 xstring_name: str, 

349 key: Optional[int], 

350 lookup: Dict[int, str], 

351 if_applicable: bool = False, 

352 qsuffix: str = "", 

353 ) -> str: 

354 description = lookup.get(key, None) 

355 value = None if description is None else f"{key}: {description}" 

356 return plainrow( 

357 qname, 

358 xstring_name, 

359 value, 

360 if_applicable=if_applicable, 

361 qsuffix=qsuffix, 

362 ) 

363 

364 def boolrow( 

365 qname: str, 

366 xstring_name: str, 

367 value: Optional[bool], 

368 lookup: Dict[int, str], 

369 if_applicable: bool = False, 

370 qsuffix: str = "", 

371 ) -> str: 

372 v = int(value) if value is not None else None 

373 return lookuprow( 

374 qname, 

375 xstring_name, 

376 v, 

377 lookup, 

378 if_applicable=if_applicable, 

379 qsuffix=qsuffix, 

380 ) 

381 

382 def ynrow( 

383 qname: str, xstring_name: str, value: Optional[Union[int, bool]] 

384 ) -> str: 

385 return plainrow(qname, xstring_name, get_yes_no(req, value)) 

386 

387 def ynnrow( 

388 qname: str, 

389 xstring_name: str, 

390 value: Optional[Union[int, bool]], 

391 if_applicable: bool = False, 

392 ) -> str: 

393 return plainrow( 

394 qname, 

395 xstring_name, 

396 get_yes_no_none(req, value), 

397 if_applicable=if_applicable, 

398 ) 

399 

400 q2_options = self.make_options_from_xstrings( 

401 req, "q2_option", 1, self.Q2_N_OPTIONS 

402 ) 

403 q3_options = self.make_options_from_xstrings( 

404 req, "q3_option", 1, self.Q3_N_OPTIONS 

405 ) 

406 q4a_options = self.make_options_from_xstrings( 

407 req, "q4a_option", 1, self.Q4_N_OPTIONS 

408 ) 

409 q7a_options = self.make_options_from_xstrings(req, "q7a_option", 0, 1) 

410 _q7b_anchors = [] # type: List[str] 

411 for _o in (1, 10): 

412 _wxstring = self.wxstring(req, f"q7b_anchor_{_o}") 

413 _q7b_anchors.append(f"{_o}: {_wxstring}") 

414 q7b_explanation = f" <i>(Anchors: {' // '.join(_q7b_anchors)})</i>" 

415 q8_options = self.make_options_from_xstrings( 

416 req, "q8_option", 1, self.Q8_N_OPTIONS 

417 ) 

418 q9_options = self.make_options_from_xstrings(req, "q9_option", 0, 1) 

419 

420 return f""" 

421 <div class="{CssClass.SUMMARY}"> 

422 <table class="{CssClass.SUMMARY}"> 

423 {self.get_is_complete_tr(req)} 

424 </table> 

425 </div> 

426 <table class="{CssClass.TASKDETAIL}"> 

427 <tr> 

428 <th width="60%">{req.sstring(SS.QUESTION)}</th> 

429 <th width="40%">{req.sstring(SS.ANSWER)}</th> 

430 </tr> 

431 {plainrow("1", "q1_question", self.q1_age_first_inflammatory_sx)} 

432 {lookuprow("2", "q2_question", self.q2_when_psych_sx_started, q2_options)} 

433 {lookuprow("3", "q3_question", self.q3_worst_symptom_last_month, q3_options)} 

434 {lookuprow("4a", "q4a_question", self.q4a_symptom_timing, q4a_options)} 

435 {plainrow("4b", "q4b_question", self.q4b_days_psych_before_phys, True)} 

436 {plainrow("4c", "q4c_question", self.q4c_days_psych_after_phys, True)} 

437 {ynnrow("5", "q5_question", self.q5_antibiotics)} 

438 {ynnrow("6a", "q6a_question", self.q6a_inpatient_last_y)} 

439 {plainrow("6b", "q6b_question", self.q6b_inpatient_weeks, True)} 

440 {boolrow("7a", "q7a_question", self.q7a_sx_last_2y, q7a_options)} 

441 {plainrow("7b", "q7b_question", self.q7b_variability, True, 

442 qsuffix=q7b_explanation)} 

443 {lookuprow("8", "q8_question", self.q8_smoking, q8_options)} 

444 {boolrow("9", "q9_question", self.q9_pregnant, q9_options)} 

445 <tr class="subheading"> 

446 <td><i>{self.wxstring(req, "q10_stem")}</i></td> 

447 <td></td> 

448 </tr> 

449 {plainrow("10a", "q10a_question", self.q10a_effective_rx_physical)} 

450 {plainrow("10b", "q10b_question", self.q10b_effective_rx_psych)} 

451 <tr class="subheading"> 

452 <td><i>{self.wxstring(req, "q11_title")}</i></td> 

453 <td></td> 

454 </tr> 

455 {ynrow("11a", "depression", self.q11a_ph_depression)} 

456 {ynrow("11b", "bipolar", self.q11b_ph_bipolar)} 

457 {ynrow("11c", "schizophrenia", self.q11c_ph_schizophrenia)} 

458 {ynrow("11d", "autistic_spectrum", self.q11d_ph_autistic_spectrum)} 

459 {ynrow("11e", "ptsd", self.q11e_ph_ptsd)} 

460 {ynrow("11f", "other_anxiety", self.q11f_ph_other_anxiety)} 

461 {ynrow("11g", "personality_disorder", self.q11g_ph_personality_disorder)} 

462 {ynrow("11h", "other_psych", self.q11h_ph_other_psych)} 

463 {plainrow("11h", "other_psych", self.q11h_ph_other_detail, True)} 

464 <tr class="subheading"> 

465 <td><i>{self.wxstring(req, "q12_title")}</i></td> 

466 <td></td> 

467 </tr> 

468 {ynrow("12a", "depression", self.q12a_fh_depression)} 

469 {ynrow("12b", "bipolar", self.q12b_fh_bipolar)} 

470 {ynrow("12c", "schizophrenia", self.q12c_fh_schizophrenia)} 

471 {ynrow("12d", "autistic_spectrum", self.q12d_fh_autistic_spectrum)} 

472 {ynrow("12e", "ptsd", self.q12e_fh_ptsd)} 

473 {ynrow("12f", "other_anxiety", self.q12f_fh_other_anxiety)} 

474 {ynrow("12g", "personality_disorder", self.q12g_fh_personality_disorder)} 

475 {ynrow("12h", "other_psych", self.q12h_fh_other_psych)} 

476 {plainrow("12h", "other_psych", self.q12h_fh_other_detail, True)} 

477 <tr class="subheading"> 

478 <td><i>{self.wxstring(req, "q13_title")}</i></td> 

479 <td></td> 

480 </tr> 

481 {ynnrow("13a", "q13a_question", self.q13a_behcet)} 

482 {ynnrow("13b", "q13b_question", self.q13b_oral_ulcers, True)} 

483 {plainrow("13c", "q13c_question", self.q13c_oral_age_first, True)} 

484 {ynnrow("13d", "q13d_question", self.q13d_oral_scarring, True)} 

485 {ynnrow("13e", "q13e_question", self.q13e_genital_ulcers, True)} 

486 {plainrow("13f", "q13f_question", self.q13f_genital_age_first, True)} 

487 {ynnrow("13g", "q13g_question", self.q13g_genital_scarring, True)} 

488 </table> 

489 """ # noqa