Coverage for tasks/lynall_iam_medical.py : 54%

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
3"""
4camcops_server/tasks/lynall_iam_medical.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27"""
29from typing import Any, Dict, List, Optional, Union
31from sqlalchemy.sql.schema import Column
32from sqlalchemy.sql.sqltypes import Integer, UnicodeText
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
50# =============================================================================
51# Lynall1MedicalHistory
52# =============================================================================
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"
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
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 )
255 @staticmethod
256 def longname(req: "CamcopsRequest") -> str:
257 _ = req.gettext
258 return _("Lynall M-E — 1 — IAM — Medical history")
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
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)
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)
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)
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))
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)
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)
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