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
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/lynall_iam_medical.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28"""
30from typing import Any, Dict, List, Optional, Union
32from sqlalchemy.sql.schema import Column
33from sqlalchemy.sql.sqltypes import Integer, UnicodeText
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
51# =============================================================================
52# Lynall1MedicalHistory
53# =============================================================================
56class LynallIamMedicalHistory(TaskHasPatientMixin, Task):
57 """
58 Server implementation of the Lynall1IamMedicalHistory task.
59 """
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
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
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 )
267 @staticmethod
268 def longname(req: "CamcopsRequest") -> str:
269 _ = req.gettext
270 return _("Lynall M-E — 1 — IAM — Medical history")
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
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)
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 )
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 )
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))
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 )
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)
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