Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/lynall_iam_medical.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27""" 

28 

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

30 

31from sqlalchemy.sql.schema import Column 

32from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

33 

34from camcops_server.cc_modules.cc_constants import CssClass 

35from camcops_server.cc_modules.cc_html import ( 

36 get_yes_no, 

37 get_yes_no_none, 

38 tr_qa, 

39) 

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_sqla_coltypes import ( 

42 BoolColumn, 

43 CamcopsColumn, 

44 PermittedValueChecker, 

45) 

46from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

47from camcops_server.cc_modules.cc_text import SS 

48 

49 

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

51# Lynall1MedicalHistory 

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

53 

54class LynallIamMedicalHistory(TaskHasPatientMixin, Task): 

55 """ 

56 Server implementation of the Lynall1IamMedicalHistory task. 

57 """ 

58 __tablename__ = "lynall_1_iam_medical" # historically fixed 

59 shortname = "Lynall_IAM_Medical" 

60 extrastring_taskname = "lynall_iam_medical" 

61 

62 Q2_N_OPTIONS = 6 

63 Q3_N_OPTIONS = 11 

64 Q4_N_OPTIONS = 5 

65 Q4_OPTION_PSYCH_BEFORE_PHYSICAL = 1 

66 Q4_OPTION_PSYCH_AFTER_PHYSICAL = 2 

67 Q8_N_OPTIONS = 2 

68 Q7B_MIN = 1 

69 Q7B_MAX = 10 

70 

71 q1_age_first_inflammatory_sx = Column( 

72 "q1_age_first_inflammatory_sx", Integer, 

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

74 ) 

75 q2_when_psych_sx_started = CamcopsColumn( 

76 "q2_when_psych_sx_started", Integer, 

77 permitted_value_checker=PermittedValueChecker( 

78 minimum=1, maximum=Q2_N_OPTIONS), 

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

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

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

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

83 ) 

84 q3_worst_symptom_last_month = CamcopsColumn( 

85 "q3_worst_symptom_last_month", Integer, 

86 permitted_value_checker=PermittedValueChecker( 

87 minimum=1, maximum=Q3_N_OPTIONS), 

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

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

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

91 "in past month)" 

92 ) 

93 q4a_symptom_timing = CamcopsColumn( 

94 "q4a_symptom_timing", Integer, 

95 permitted_value_checker=PermittedValueChecker( 

96 minimum=1, maximum=Q4_N_OPTIONS), 

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

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

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

100 ) 

101 q4b_days_psych_before_phys = Column( 

102 "q4b_days_psych_before_phys", Integer, 

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

104 "before physical Sx" 

105 ) 

106 q4c_days_psych_after_phys = Column( 

107 "q4c_days_psych_after_phys", Integer, 

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

109 "after physical Sx" 

110 ) 

111 q5_antibiotics = BoolColumn( 

112 "q5_antibiotics", 

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

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

115 ) 

116 q6a_inpatient_last_y = BoolColumn( 

117 "q6a_inpatient_last_y", 

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

119 ) 

120 q6b_inpatient_weeks = Column( 

121 "q6b_inpatient_weeks", Integer, 

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

123 "inpatient in the past year" 

124 ) 

125 q7a_sx_last_2y = BoolColumn( 

126 "q7a_sx_last_2y", 

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

128 ) 

129 q7b_variability = Column( 

130 "q7b_variability", Integer, 

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

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

133 "there all the time)" 

134 ) 

135 q8_smoking = Column( 

136 "q8_smoking", Integer, 

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

138 "2 = every day)" 

139 ) 

140 q9_pregnant = BoolColumn( 

141 "q9_pregnant", 

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

143 ) 

144 q10a_effective_rx_physical = Column( 

145 "q10a_effective_rx_physical", UnicodeText, 

146 comment="Most effective treatments for physical Sx" 

147 ) 

148 q10b_effective_rx_psych = Column( 

149 "q10b_effective_rx_psych", UnicodeText, 

150 comment="Most effective treatments for brain/psychiatric Sx" 

151 ) 

152 q11a_ph_depression = BoolColumn( 

153 "q11a_ph_depression", 

154 comment="Personal history of depression?" 

155 ) 

156 q11b_ph_bipolar = BoolColumn( 

157 "q11b_ph_bipolar", 

158 comment="Personal history of bipolar disorder?" 

159 ) 

160 q11c_ph_schizophrenia = BoolColumn( 

161 "q11c_ph_schizophrenia", 

162 comment="Personal history of schizophrenia?" 

163 ) 

164 q11d_ph_autistic_spectrum = BoolColumn( 

165 "q11d_ph_autistic_spectrum", 

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

167 ) 

168 q11e_ph_ptsd = BoolColumn( 

169 "q11e_ph_ptsd", 

170 comment="Personal history of PTSD?" 

171 ) 

172 q11f_ph_other_anxiety = BoolColumn( 

173 "q11f_ph_other_anxiety", 

174 comment="Personal history of other anxiety disorders?" 

175 ) 

176 q11g_ph_personality_disorder = BoolColumn( 

177 "q11g_ph_personality_disorder", 

178 comment="Personal history of personality disorder?" 

179 ) 

180 q11h_ph_other_psych = BoolColumn( 

181 "q11h_ph_other_psych", 

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

183 ) 

184 q11h_ph_other_detail = Column( 

185 "q11h_ph_other_detail", UnicodeText, 

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

187 "details field" 

188 ) 

189 q12a_fh_depression = BoolColumn( 

190 "q12a_fh_depression", 

191 comment="Family history of depression?" 

192 ) 

193 q12b_fh_bipolar = BoolColumn( 

194 "q12b_fh_bipolar", 

195 comment="Family history of bipolar disorder?" 

196 ) 

197 q12c_fh_schizophrenia = BoolColumn( 

198 "q12c_fh_schizophrenia", 

199 comment="Family history of schizophrenia?" 

200 ) 

201 q12d_fh_autistic_spectrum = BoolColumn( 

202 "q12d_fh_autistic_spectrum", 

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

204 ) 

205 q12e_fh_ptsd = BoolColumn( 

206 "q12e_fh_ptsd", 

207 comment="Family history of PTSD?" 

208 ) 

209 q12f_fh_other_anxiety = BoolColumn( 

210 "q12f_fh_other_anxiety", 

211 comment="Family history of other anxiety disorders?" 

212 ) 

213 q12g_fh_personality_disorder = BoolColumn( 

214 "q12g_fh_personality_disorder", 

215 comment="Family history of personality disorder?" 

216 ) 

217 q12h_fh_other_psych = BoolColumn( 

218 "q12h_fh_other_psych", 

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

220 ) 

221 q12h_fh_other_detail = Column( 

222 "q12h_fh_other_detail", UnicodeText, 

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

224 "details field" 

225 ) 

226 q13a_behcet = BoolColumn( 

227 "q13a_behcet", 

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

229 ) 

230 q13b_oral_ulcers = BoolColumn( 

231 "q13b_oral_ulcers", 

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

233 ) 

234 q13c_oral_age_first = Column( 

235 "q13c_oral_age_first", Integer, 

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

237 ) 

238 q13d_oral_scarring = BoolColumn( 

239 "q13d_oral_scarring", 

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

241 ) 

242 q13e_genital_ulcers = BoolColumn( 

243 "q13e_genital_ulcers", 

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

245 ) 

246 q13f_genital_age_first = Column( 

247 "q13f_genital_age_first", Integer, 

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

249 ) 

250 q13g_genital_scarring = BoolColumn( 

251 "q13g_genital_scarring", 

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

253 ) 

254 

255 @staticmethod 

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

257 _ = req.gettext 

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

259 

260 def is_complete(self) -> bool: 

261 if self.any_fields_none(["q1_age_first_inflammatory_sx", 

262 "q2_when_psych_sx_started", 

263 "q3_worst_symptom_last_month", 

264 "q4a_symptom_timing", 

265 "q5_antibiotics", 

266 "q6a_inpatient_last_y", 

267 "q7a_sx_last_2y", 

268 "q8_smoking", 

269 "q9_pregnant", 

270 "q10a_effective_rx_physical", 

271 "q10b_effective_rx_psych", 

272 "q13a_behcet"]): 

273 return False 

274 if self.any_fields_null_or_empty_str(["q10a_effective_rx_physical", 

275 "q10b_effective_rx_psych"]): 

276 return False 

277 q4a = self.q4a_symptom_timing 

278 if (q4a == self.Q4_OPTION_PSYCH_BEFORE_PHYSICAL and 

279 self.q4b_days_psych_before_phys is None): 

280 return False 

281 if (q4a == self.Q4_OPTION_PSYCH_AFTER_PHYSICAL and 

282 self.q4c_days_psych_after_phys is None): 

283 return False 

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

285 return False 

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

287 return False 

288 if self.q11h_ph_other_psych and not self.q11h_ph_other_detail: 

289 return False 

290 if self.q12h_fh_other_psych and not self.q12h_fh_other_detail: 

291 return False 

292 if self.q13a_behcet: 

293 if self.any_fields_none(["q13b_oral_ulcers", 

294 "q13e_genital_ulcers"]): 

295 return False 

296 if self.q13b_oral_ulcers: 

297 if self.any_fields_none(["q13c_oral_age_first", 

298 "q13d_oral_scarring"]): 

299 return False 

300 if self.q13e_genital_ulcers: 

301 if self.any_fields_none(["q13f_genital_age_first", 

302 "q13g_genital_scarring"]): 

303 return False 

304 return True 

305 

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

307 def plainrow(qname: str, xstring_name: str, value: Any, 

308 if_applicable: bool = False, qsuffix: str = "") -> str: 

309 ia_str = ( 

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

311 if if_applicable else "" 

312 ) 

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

314 return tr_qa(q, value) 

315 

316 def lookuprow(qname: str, xstring_name: str, key: Optional[int], 

317 lookup: Dict[int, str], if_applicable: bool = False, 

318 qsuffix: str = "") -> str: 

319 description = lookup.get(key, None) 

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

321 return plainrow(qname, xstring_name, value, 

322 if_applicable=if_applicable, qsuffix=qsuffix) 

323 

324 def boolrow(qname: str, xstring_name: str, value: Optional[bool], 

325 lookup: Dict[int, str], if_applicable: bool = False, 

326 qsuffix: str = "") -> str: 

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

328 return lookuprow(qname, xstring_name, v, lookup, 

329 if_applicable=if_applicable, qsuffix=qsuffix) 

330 

331 def ynrow(qname: str, xstring_name: str, 

332 value: Optional[Union[int, bool]]) -> str: 

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

334 

335 def ynnrow(qname: str, xstring_name: str, 

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

337 if_applicable: bool = False) -> str: 

338 return plainrow(qname, xstring_name, get_yes_no_none(req, value), 

339 if_applicable=if_applicable) 

340 

341 q2_options = self.make_options_from_xstrings( 

342 req, "q2_option", 1, self.Q2_N_OPTIONS) 

343 q3_options = self.make_options_from_xstrings( 

344 req, "q3_option", 1, self.Q3_N_OPTIONS) 

345 q4a_options = self.make_options_from_xstrings( 

346 req, "q4a_option", 1, self.Q4_N_OPTIONS) 

347 q7a_options = self.make_options_from_xstrings( 

348 req, "q7a_option", 0, 1) 

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

350 for _o in [1, 10]: 

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

352 _q7b_anchors.append(f'{_o}: {_wxstring}') 

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

354 q8_options = self.make_options_from_xstrings( 

355 req, "q8_option", 1, self.Q8_N_OPTIONS) 

356 q9_options = self.make_options_from_xstrings( 

357 req, "q9_option", 0, 1) 

358 

359 return f""" 

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

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

362 {self.get_is_complete_tr(req)} 

363 </table> 

364 </div> 

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

366 <tr> 

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

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

369 </tr> 

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

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

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

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

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

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

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

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

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

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

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

381 qsuffix=q7b_explanation)} 

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

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

384 <tr class="subheading"> 

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

386 <td></td> 

387 </tr> 

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

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

390 <tr class="subheading"> 

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

392 <td></td> 

393 </tr> 

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

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

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

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

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

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

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

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

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

403 <tr class="subheading"> 

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

405 <td></td> 

406 </tr> 

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

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

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

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

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

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

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

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

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

416 <tr class="subheading"> 

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

418 <td></td> 

419 </tr> 

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

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

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

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

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

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

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

427 </table> 

428 """ # noqa