Coverage for tasks/khandaker_mojo_medical.py: 69%

129 statements  

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

1""" 

2camcops_server/tasks/khandaker_mojo_medical.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 Any, Type 

29 

30from sqlalchemy.sql.sqltypes import Date, Float, Integer, UnicodeText 

31 

32from camcops_server.cc_modules.cc_constants import CssClass 

33from camcops_server.cc_modules.cc_html import tr_qa 

34from camcops_server.cc_modules.cc_request import CamcopsRequest 

35from camcops_server.cc_modules.cc_sqla_coltypes import ( 

36 bool_column, 

37 camcops_column, 

38 ZERO_TO_TWO_CHECKER, 

39) 

40from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

41 

42 

43class KhandakerMojoMedical( # type: ignore[misc] 

44 TaskHasPatientMixin, 

45 Task, 

46): 

47 """ 

48 Server implementation of the KhandakerMojoMedical task 

49 """ 

50 

51 __tablename__ = "khandaker_mojo_medical" 

52 shortname = "Khandaker_MOJO_Medical" 

53 info_filename_stem = "khandaker_mojo" 

54 provides_trackers = False 

55 

56 # Section 1: General Information 

57 FN_DIAGNOSIS = "diagnosis" 

58 FN_DIAGNOSIS_DATE = "diagnosis_date" 

59 FN_DIAGNOSIS_DATE_APPROXIMATE = "diagnosis_date_approximate" 

60 FN_HAS_FIBROMYALGIA = "has_fibromyalgia" 

61 FN_IS_PREGNANT = "is_pregnant" 

62 FN_HAS_INFECTION_PAST_MONTH = "has_infection_past_month" 

63 

64 @classmethod 

65 def extend_columns( 

66 cls: Type["KhandakerMojoMedical"], **kwargs: Any 

67 ) -> None: 

68 setattr( 

69 cls, 

70 cls.FN_DIAGNOSIS, 

71 camcops_column( 

72 cls.FN_DIAGNOSIS, 

73 Integer, 

74 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

75 comment=( 

76 "Diagnosis (0 Rheumatoid Arthritis, " 

77 "1 Ankylosing Spondylitis, 2 Sjögren’s Syndrome)" 

78 ), 

79 ), 

80 ) 

81 setattr( 

82 cls, 

83 cls.FN_DIAGNOSIS_DATE, 

84 camcops_column( 

85 cls.FN_DIAGNOSIS_DATE, 

86 Date, 

87 comment=( 

88 "Date of first diagnosis (may be approx from " 

89 "'duration of illness (years))'" 

90 ), 

91 ), 

92 ) 

93 setattr( 

94 cls, 

95 cls.FN_DIAGNOSIS_DATE_APPROXIMATE, 

96 bool_column( 

97 cls.FN_DIAGNOSIS_DATE_APPROXIMATE, 

98 comment="True if diagnosis date was derived from duration", 

99 ), 

100 ) 

101 setattr( 

102 cls, 

103 cls.FN_HAS_FIBROMYALGIA, 

104 bool_column( 

105 cls.FN_HAS_FIBROMYALGIA, 

106 comment="Do you have a diagnosis of fibromyalgia?", 

107 ), 

108 ) 

109 setattr( 

110 cls, 

111 cls.FN_IS_PREGNANT, 

112 bool_column( 

113 cls.FN_IS_PREGNANT, 

114 comment=( 

115 "Are you, or is there any possibility that you might " 

116 "be pregnant?" 

117 ), 

118 ), 

119 ) 

120 setattr( 

121 cls, 

122 cls.FN_HAS_INFECTION_PAST_MONTH, 

123 bool_column( 

124 cls.FN_HAS_INFECTION_PAST_MONTH, 

125 comment=( 

126 "Do you currently have an infection, or had " 

127 "treatment for an infection (e.g antibiotics) " 

128 "in the past month?" 

129 ), 

130 ), 

131 ) 

132 setattr( 

133 cls, 

134 cls.FN_HAD_INFECTION_TWO_MONTHS_PRECEDING, 

135 bool_column( 

136 cls.FN_HAD_INFECTION_TWO_MONTHS_PRECEDING, 

137 comment=( 

138 "Have you had an infection, or had treatment for " 

139 "an infection (e.g antibiotics) in the 2 months " 

140 "preceding last month?" 

141 ), 

142 constraint_name="ck_kh2mm_had_infection", 

143 ), 

144 ) 

145 setattr( 

146 cls, 

147 cls.FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE, 

148 bool_column( 

149 cls.FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE, 

150 comment=( 

151 "Do you have a current diagnosis of alcohol or " 

152 "substance dependence?" 

153 ), 

154 constraint_name="ck_kh2mm_has_alcohol", 

155 ), 

156 ) 

157 setattr( 

158 cls, 

159 cls.FN_SMOKING_STATUS, 

160 camcops_column( 

161 cls.FN_SMOKING_STATUS, 

162 Integer, 

163 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

164 comment=( 

165 "What is your smoking status? (0 Never smoked, " 

166 "1 Ex-smoker, 2 Current smoker)" 

167 ), 

168 ), 

169 ) 

170 setattr( 

171 cls, 

172 cls.FN_ALCOHOL_UNITS_PER_WEEK, 

173 camcops_column( 

174 cls.FN_ALCOHOL_UNITS_PER_WEEK, 

175 Float, 

176 comment=( 

177 "How much alcohol do you drink per week? (medium " 

178 "glass of wine = 2 units, pint of beer at 4.5% = " 

179 "2.5 units, 25ml of spirits at 40% = 1 unit)" 

180 ), 

181 ), 

182 ) 

183 setattr( 

184 cls, 

185 cls.FN_DEPRESSION, 

186 bool_column( 

187 cls.FN_DEPRESSION, 

188 comment=( 

189 "Have you had any of the following conditions " 

190 "diagnosed by a doctor?" 

191 ), 

192 ), 

193 ) 

194 setattr( 

195 cls, 

196 cls.FN_BIPOLAR_DISORDER, 

197 bool_column( 

198 cls.FN_BIPOLAR_DISORDER, 

199 comment=( 

200 "Have you had any of the following conditions " 

201 "diagnosed by a doctor?" 

202 ), 

203 ), 

204 ) 

205 setattr( 

206 cls, 

207 cls.FN_SCHIZOPHRENIA, 

208 bool_column( 

209 cls.FN_SCHIZOPHRENIA, 

210 comment=( 

211 "Have you had any of the following conditions " 

212 "diagnosed by a doctor?" 

213 ), 

214 ), 

215 ) 

216 setattr( 

217 cls, 

218 cls.FN_AUTISM, 

219 bool_column( 

220 cls.FN_AUTISM, 

221 comment=( 

222 "Have you had any of the following conditions " 

223 "diagnosed by a doctor?" 

224 ), 

225 ), 

226 ) 

227 setattr( 

228 cls, 

229 cls.FN_PTSD, 

230 bool_column( 

231 cls.FN_PTSD, 

232 comment=( 

233 "Have you had any of the following conditions " 

234 "diagnosed by a doctor?" 

235 ), 

236 ), 

237 ) 

238 setattr( 

239 cls, 

240 cls.FN_ANXIETY, 

241 bool_column( 

242 cls.FN_ANXIETY, 

243 comment=( 

244 "Have you had any of the following conditions " 

245 "diagnosed by a doctor?" 

246 ), 

247 ), 

248 ) 

249 setattr( 

250 cls, 

251 cls.FN_PERSONALITY_DISORDER, 

252 bool_column( 

253 cls.FN_PERSONALITY_DISORDER, 

254 comment=( 

255 "Have you had any of the following conditions " 

256 "diagnosed by a doctor?" 

257 ), 

258 ), 

259 ) 

260 setattr( 

261 cls, 

262 cls.FN_INTELLECTUAL_DISABILITY, 

263 bool_column( 

264 cls.FN_INTELLECTUAL_DISABILITY, 

265 comment=( 

266 "Have you had any of the following conditions " 

267 "diagnosed by a doctor?" 

268 ), 

269 ), 

270 ) 

271 setattr( 

272 cls, 

273 cls.FN_OTHER_MENTAL_ILLNESS, 

274 bool_column( 

275 cls.FN_OTHER_MENTAL_ILLNESS, 

276 comment=( 

277 "Have you had any of the following conditions " 

278 "diagnosed by a doctor?" 

279 ), 

280 ), 

281 ) 

282 setattr( 

283 cls, 

284 cls.FN_OTHER_MENTAL_ILLNESS_DETAILS, 

285 camcops_column( 

286 cls.FN_OTHER_MENTAL_ILLNESS_DETAILS, 

287 UnicodeText, 

288 comment="If other, please list here", 

289 ), 

290 ) 

291 setattr( 

292 cls, 

293 cls.FN_HOSPITALISED_IN_LAST_YEAR, 

294 bool_column( 

295 cls.FN_HOSPITALISED_IN_LAST_YEAR, 

296 comment=( 

297 "Have you had a physical or mental illness " 

298 "requiring hospitalisation in the previous 12 " 

299 "months?" 

300 ), 

301 ), 

302 ) 

303 setattr( 

304 cls, 

305 cls.FN_HOSPITALISATION_DETAILS, 

306 camcops_column( 

307 cls.FN_HOSPITALISATION_DETAILS, 

308 UnicodeText, 

309 comment=( 

310 "If yes, please list here (name of illness, number " 

311 "of hospitilisations and duration):" 

312 ), 

313 ), 

314 ) 

315 setattr( 

316 cls, 

317 cls.FN_FAMILY_DEPRESSION, 

318 bool_column( 

319 cls.FN_FAMILY_DEPRESSION, 

320 comment=( 

321 "Has anyone in your immediate family " 

322 "(parents, siblings or children) had any of the " 

323 "following conditions diagnosed by a doctor?" 

324 ), 

325 ), 

326 ) 

327 setattr( 

328 cls, 

329 cls.FN_FAMILY_BIPOLAR_DISORDER, 

330 bool_column( 

331 cls.FN_FAMILY_BIPOLAR_DISORDER, 

332 comment=( 

333 "Has anyone in your immediate family " 

334 "(parents, siblings or children) had any of the " 

335 "following conditions diagnosed by a doctor?" 

336 ), 

337 ), 

338 ) 

339 setattr( 

340 cls, 

341 cls.FN_FAMILY_SCHIZOPHRENIA, 

342 bool_column( 

343 cls.FN_FAMILY_SCHIZOPHRENIA, 

344 comment=( 

345 "Has anyone in your immediate family " 

346 "(parents, siblings or children) had any of the " 

347 "following conditions diagnosed by a doctor?" 

348 ), 

349 ), 

350 ) 

351 setattr( 

352 cls, 

353 cls.FN_FAMILY_AUTISM, 

354 bool_column( 

355 cls.FN_FAMILY_AUTISM, 

356 comment=( 

357 "Has anyone in your immediate family " 

358 "(parents, siblings or children) had any of the " 

359 "following conditions diagnosed by a doctor?" 

360 ), 

361 ), 

362 ) 

363 setattr( 

364 cls, 

365 cls.FN_FAMILY_PTSD, 

366 bool_column( 

367 cls.FN_FAMILY_PTSD, 

368 comment=( 

369 "Has anyone in your immediate family " 

370 "(parents, siblings or children) had any of the " 

371 "following conditions diagnosed by a doctor?" 

372 ), 

373 ), 

374 ) 

375 setattr( 

376 cls, 

377 cls.FN_FAMILY_ANXIETY, 

378 bool_column( 

379 cls.FN_FAMILY_ANXIETY, 

380 comment=( 

381 "Has anyone in your immediate family " 

382 "(parents, siblings or children) had any of the " 

383 "following conditions diagnosed by a doctor?" 

384 ), 

385 ), 

386 ) 

387 setattr( 

388 cls, 

389 cls.FN_FAMILY_PERSONALITY_DISORDER, 

390 bool_column( 

391 cls.FN_FAMILY_PERSONALITY_DISORDER, 

392 comment=( 

393 "Has anyone in your immediate family " 

394 "(parents, siblings or children) had any of the " 

395 "following conditions diagnosed by a doctor?" 

396 ), 

397 ), 

398 ) 

399 setattr( 

400 cls, 

401 cls.FN_FAMILY_INTELLECTUAL_DISABILITY, 

402 bool_column( 

403 cls.FN_FAMILY_INTELLECTUAL_DISABILITY, 

404 comment=( 

405 "Has anyone in your immediate family " 

406 "(parents, siblings or children) had any of the " 

407 "following conditions diagnosed by a doctor?" 

408 ), 

409 constraint_name="ck_kh2mm_fam_int_dis", 

410 ), 

411 ) 

412 setattr( 

413 cls, 

414 cls.FN_FAMILY_OTHER_MENTAL_ILLNESS, 

415 bool_column( 

416 cls.FN_FAMILY_OTHER_MENTAL_ILLNESS, 

417 comment=( 

418 "Has anyone in your immediate family " 

419 "(parents, siblings or children) had any of the " 

420 "following conditions diagnosed by a doctor?" 

421 ), 

422 ), 

423 ) 

424 setattr( 

425 cls, 

426 cls.FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS, 

427 camcops_column( 

428 cls.FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS, 

429 UnicodeText, 

430 comment="If other, please list here", 

431 ), 

432 ) 

433 

434 FN_HAD_INFECTION_TWO_MONTHS_PRECEDING = ( 

435 "had_infection_two_months_preceding" 

436 ) 

437 FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE = "has_alcohol_substance_dependence" 

438 FN_SMOKING_STATUS = "smoking_status" 

439 FN_ALCOHOL_UNITS_PER_WEEK = "alcohol_units_per_week" 

440 

441 # Section 2: Medical History 

442 FN_DEPRESSION = "depression" 

443 FN_BIPOLAR_DISORDER = "bipolar_disorder" 

444 FN_SCHIZOPHRENIA = "schizophrenia" 

445 FN_AUTISM = "autism" 

446 FN_PTSD = "ptsd" 

447 FN_ANXIETY = "anxiety" 

448 FN_PERSONALITY_DISORDER = "personality_disorder" 

449 FN_INTELLECTUAL_DISABILITY = "intellectual_disability" 

450 FN_OTHER_MENTAL_ILLNESS = "other_mental_illness" 

451 FN_OTHER_MENTAL_ILLNESS_DETAILS = "other_mental_illness_details" 

452 FN_HOSPITALISED_IN_LAST_YEAR = "hospitalised_in_last_year" 

453 FN_HOSPITALISATION_DETAILS = "hospitalisation_details" 

454 

455 # Section 3: Family history 

456 FN_FAMILY_DEPRESSION = "family_depression" 

457 FN_FAMILY_BIPOLAR_DISORDER = "family_bipolar_disorder" 

458 FN_FAMILY_SCHIZOPHRENIA = "family_schizophrenia" 

459 FN_FAMILY_AUTISM = "family_autism" 

460 FN_FAMILY_PTSD = "family_ptsd" 

461 FN_FAMILY_ANXIETY = "family_anxiety" 

462 FN_FAMILY_PERSONALITY_DISORDER = "family_personality_disorder" 

463 FN_FAMILY_INTELLECTUAL_DISABILITY = "family_intellectual_disability" 

464 FN_FAMILY_OTHER_MENTAL_ILLNESS = "family_other_mental_illness" 

465 FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS = ( 

466 "family_other_mental_illness_details" 

467 ) 

468 

469 MANDATORY_FIELD_NAMES_1 = [ 

470 FN_DIAGNOSIS, 

471 FN_DIAGNOSIS_DATE, 

472 FN_HAS_FIBROMYALGIA, 

473 FN_IS_PREGNANT, 

474 FN_HAS_INFECTION_PAST_MONTH, 

475 FN_HAD_INFECTION_TWO_MONTHS_PRECEDING, 

476 FN_HAS_ALCOHOL_SUBSTANCE_DEPENDENCE, 

477 FN_SMOKING_STATUS, 

478 FN_ALCOHOL_UNITS_PER_WEEK, 

479 ] 

480 

481 MANDATORY_FIELD_NAMES_2 = [ 

482 FN_DEPRESSION, 

483 FN_BIPOLAR_DISORDER, 

484 FN_SCHIZOPHRENIA, 

485 FN_AUTISM, 

486 FN_PTSD, 

487 FN_ANXIETY, 

488 FN_PERSONALITY_DISORDER, 

489 FN_INTELLECTUAL_DISABILITY, 

490 FN_OTHER_MENTAL_ILLNESS, 

491 FN_HOSPITALISED_IN_LAST_YEAR, 

492 ] 

493 

494 MANDATORY_FIELD_NAMES_3 = [ 

495 FN_FAMILY_DEPRESSION, 

496 FN_FAMILY_BIPOLAR_DISORDER, 

497 FN_FAMILY_SCHIZOPHRENIA, 

498 FN_FAMILY_AUTISM, 

499 FN_FAMILY_PTSD, 

500 FN_FAMILY_ANXIETY, 

501 FN_FAMILY_PERSONALITY_DISORDER, 

502 FN_FAMILY_INTELLECTUAL_DISABILITY, 

503 FN_FAMILY_OTHER_MENTAL_ILLNESS, 

504 ] 

505 

506 MANDATORY_FIELD_NAMES = ( 

507 MANDATORY_FIELD_NAMES_1 

508 + MANDATORY_FIELD_NAMES_2 

509 + MANDATORY_FIELD_NAMES_3 

510 ) 

511 

512 # If the answer is yes to any of these, we need to have the details 

513 DETAILS_FIELDS = { 

514 FN_OTHER_MENTAL_ILLNESS: FN_OTHER_MENTAL_ILLNESS_DETAILS, 

515 FN_HOSPITALISED_IN_LAST_YEAR: FN_HOSPITALISATION_DETAILS, 

516 FN_FAMILY_OTHER_MENTAL_ILLNESS: FN_FAMILY_OTHER_MENTAL_ILLNESS_DETAILS, 

517 } 

518 

519 MULTI_CHOICE_FIELD_NAMES = [FN_DIAGNOSIS, FN_SMOKING_STATUS] 

520 

521 @staticmethod 

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

523 _ = req.gettext 

524 return _("Khandaker GM — MOJO — Medical questionnaire") 

525 

526 def is_complete(self) -> bool: 

527 if self.any_fields_none(self.MANDATORY_FIELD_NAMES): 

528 return False 

529 

530 if not self.field_contents_valid(): 

531 return False 

532 

533 for field_name, details_field_name in self.DETAILS_FIELDS.items(): 

534 if getattr(self, field_name): 

535 if getattr(self, details_field_name) is None: 

536 return False 

537 

538 return True 

539 

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

541 heading_1 = self.xstring(req, "general_information_title") 

542 

543 rows_1 = "" 

544 for field_name in self.MANDATORY_FIELD_NAMES_1: 

545 rows_1 += self.get_rows(req, field_name) 

546 

547 heading_2 = self.xstring(req, "medical_history_title") 

548 

549 rows_2 = "" 

550 for field_name in self.MANDATORY_FIELD_NAMES_2: 

551 rows_2 += self.get_rows(req, field_name) 

552 

553 heading_3 = self.xstring(req, "family_history_title") 

554 

555 rows_3 = "" 

556 for field_name in self.MANDATORY_FIELD_NAMES_3: 

557 rows_3 += self.get_rows(req, field_name) 

558 

559 html = f""" 

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

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

562 {self.get_is_complete_tr(req)} 

563 </table> 

564 </div> 

565 <h3>{heading_1}</h3> 

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

567 <tr> 

568 <th width="60%">Question</th> 

569 <th width="40%">Answer</th> 

570 </tr> 

571 {rows_1} 

572 </table> 

573 <h3>{heading_2}</h3> 

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

575 <tr> 

576 <th width="60%">Question</th> 

577 <th width="40%">Answer</th> 

578 </tr> 

579 {rows_2} 

580 </table> 

581 <h3>{heading_3}</h3> 

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

583 <tr> 

584 <th width="60%">Question</th> 

585 <th width="40%">Answer</th> 

586 </tr> 

587 {rows_3} 

588 </table> 

589 """ 

590 

591 return html 

592 

593 def get_rows(self, req: CamcopsRequest, field_name: str) -> str: 

594 rows = "" 

595 

596 question_text = self.xstring(req, f"q_{field_name}") 

597 answer = getattr(self, field_name) 

598 

599 answer_text = answer 

600 

601 if answer is not None and ( 

602 field_name in self.MULTI_CHOICE_FIELD_NAMES 

603 ): 

604 answer_text = self.xstring(req, f"{field_name}_{answer}") 

605 

606 rows += tr_qa(question_text, answer_text) 

607 

608 if answer and field_name in self.DETAILS_FIELDS: 

609 details_field_name = self.DETAILS_FIELDS[field_name] 

610 details_question_text = self.xstring( 

611 req, f"q_{details_field_name}" 

612 ) 

613 details_answer = getattr(self, details_field_name) 

614 

615 rows += tr_qa(details_question_text, details_answer) 

616 

617 if field_name == self.FN_DIAGNOSIS_DATE: 

618 rows += tr_qa( 

619 self.xstring(req, f"q_{self.FN_DIAGNOSIS_DATE_APPROXIMATE}"), 

620 getattr(self, self.FN_DIAGNOSIS_DATE_APPROXIMATE), 

621 ) 

622 

623 return rows