Coverage for tasks/lynall_iam_life.py: 56%
97 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_life.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**Lynall M-E — IAM study — life events.**
30"""
32from typing import Any, Dict, List, Tuple, Type
34from sqlalchemy.ext.declarative import DeclarativeMeta
35from sqlalchemy.sql.sqltypes import Integer
37from camcops_server.cc_modules.cc_constants import CssClass
38from camcops_server.cc_modules.cc_html import answer, get_yes_no_none
39from camcops_server.cc_modules.cc_request import CamcopsRequest
40from camcops_server.cc_modules.cc_sqla_coltypes import (
41 BoolColumn,
42 CamcopsColumn,
43 MIN_ZERO_CHECKER,
44 ONE_TO_THREE_CHECKER,
45 ZERO_TO_100_CHECKER,
46)
47from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
50# =============================================================================
51# LynallIamLifeEvents
52# =============================================================================
54N_QUESTIONS = 14
56SPECIAL_SEVERITY_QUESTIONS = [14]
57SPECIAL_FREQUENCY_QUESTIONS = [1, 2, 3, 8]
58FREQUENCY_AS_PERCENT_QUESTIONS = [1, 2, 8]
60QPREFIX = "q"
61QSUFFIX_MAIN = "_main"
62QSUFFIX_SEVERITY = "_severity"
63QSUFFIX_FREQUENCY = "_frequency"
65SEVERITY_MIN = 1
66SEVERITY_MAX = 3
69def qfieldname_main(qnum: int) -> str:
70 return f"{QPREFIX}{qnum}{QSUFFIX_MAIN}"
73def qfieldname_severity(qnum: int) -> str:
74 return f"{QPREFIX}{qnum}{QSUFFIX_SEVERITY}"
77def qfieldname_frequency(qnum: int) -> str:
78 return f"{QPREFIX}{qnum}{QSUFFIX_FREQUENCY}"
81class LynallIamLifeEventsMetaclass(DeclarativeMeta):
82 # noinspection PyInitNewSignature
83 def __init__(
84 cls: Type["LynallIamLifeEvents"],
85 name: str,
86 bases: Tuple[Type, ...],
87 classdict: Dict[str, Any],
88 ) -> None:
89 comment_strings = [
90 "illness/injury/assault (self)", # 1
91 "illness/injury/assault (relative)",
92 "parent/child/spouse/sibling died",
93 "close family friend/other relative died",
94 "marital separation or broke off relationship", # 5
95 "ended long-lasting friendship with close friend/relative",
96 "problems with close friend/neighbour/relative",
97 "unsuccessful job-seeking for >1 month", # 8
98 "sacked/made redundant", # 9
99 "major financial crisis", # 10
100 "problem with police involving court appearance",
101 "something valued lost/stolen",
102 "self/partner gave birth",
103 "other significant negative events", # 14
104 ]
105 for q in range(1, N_QUESTIONS + 1):
106 i = q - 1
108 fn_main = qfieldname_main(q)
109 cmt_main = (
110 f"Q{q}: in last 6 months: {comment_strings[i]} (0 no, 1 yes)"
111 )
112 setattr(cls, fn_main, BoolColumn(fn_main, comment=cmt_main))
114 fn_severity = qfieldname_severity(q)
115 cmt_severity = (
116 f"Q{q}: (if yes) how bad was that "
117 f"(1 not too bad, 2 moderately bad, 3 very bad)"
118 )
119 setattr(
120 cls,
121 fn_severity,
122 CamcopsColumn(
123 fn_severity,
124 Integer,
125 comment=cmt_severity,
126 permitted_value_checker=ONE_TO_THREE_CHECKER,
127 ),
128 )
130 fn_frequency = qfieldname_frequency(q)
131 if q in FREQUENCY_AS_PERCENT_QUESTIONS:
132 cmt_frequency = (
133 f"Q{q}: For what percentage of your life since aged 18 "
134 f"has [this event: {comment_strings[i]}] been happening? "
135 f"(0-100)"
136 )
137 pv_frequency = ZERO_TO_100_CHECKER
138 else:
139 cmt_frequency = (
140 f"Q{q}: Since age 18, how many times has this happened to "
141 f"you in total?"
142 )
143 pv_frequency = MIN_ZERO_CHECKER
144 setattr(
145 cls,
146 fn_frequency,
147 CamcopsColumn(
148 fn_frequency,
149 Integer,
150 comment=cmt_frequency,
151 permitted_value_checker=pv_frequency,
152 ),
153 )
155 super().__init__(name, bases, classdict)
158class LynallIamLifeEvents(
159 TaskHasPatientMixin, Task, metaclass=LynallIamLifeEventsMetaclass
160):
161 """
162 Server implementation of the LynallIamLifeEvents task.
163 """
165 __tablename__ = "lynall_iam_life"
166 shortname = "Lynall_IAM_Life"
168 prohibits_commercial = True
170 @staticmethod
171 def longname(req: "CamcopsRequest") -> str:
172 _ = req.gettext
173 return _("Lynall M-E — IAM — Life events")
175 def is_complete(self) -> bool:
176 for q in range(1, N_QUESTIONS + 1):
177 value_main = getattr(self, qfieldname_main(q))
178 if value_main is None:
179 return False
180 if not value_main:
181 continue
182 if (
183 getattr(self, qfieldname_severity(q)) is None
184 or getattr(self, qfieldname_frequency(q)) is None
185 ):
186 return False
187 return True
189 def n_endorsed(self) -> int:
190 """
191 The number of main items endorsed.
192 """
193 fieldnames = [qfieldname_main(q) for q in range(1, N_QUESTIONS + 1)]
194 return self.count_booleans(fieldnames)
196 def severity_score(self) -> int:
197 """
198 The sum of severity scores.
200 These are intrinsically coded 1 = not too bad, 2 = moderately bad, 3 =
201 very bad. In addition, we score 0 for "not experienced".
202 """
203 total = 0
204 for q in range(1, N_QUESTIONS + 1):
205 v_main = getattr(self, qfieldname_main(q))
206 if v_main: # if endorsed
207 v_severity = getattr(self, qfieldname_severity(q))
208 if v_severity is not None:
209 total += v_severity
210 return total
212 def get_task_html(self, req: CamcopsRequest) -> str:
213 options_severity = {
214 3: self.wxstring(req, "severity_a3"),
215 2: self.wxstring(req, "severity_a2"),
216 1: self.wxstring(req, "severity_a1"),
217 }
218 q_a = [] # type: List[str]
219 for q in range(1, N_QUESTIONS + 1):
220 fieldname_main = qfieldname_main(q)
221 q_main = self.wxstring(req, fieldname_main)
222 v_main = getattr(self, fieldname_main)
223 a_main = answer(get_yes_no_none(req, v_main))
224 if v_main:
225 v_severity = getattr(self, qfieldname_severity(q))
226 a_severity = answer(
227 f"{v_severity}: {options_severity.get(v_severity)}"
228 if v_severity is not None
229 else None
230 )
231 v_frequency = getattr(self, qfieldname_frequency(q))
232 text_frequency = v_frequency
233 if q in FREQUENCY_AS_PERCENT_QUESTIONS:
234 note_frequency = "a"
235 if v_frequency is not None:
236 text_frequency = f"{v_frequency}%"
237 else:
238 note_frequency = "b"
239 a_frequency = (
240 f"{answer(text_frequency)} <sup>[{note_frequency}]</sup>"
241 if text_frequency is not None
242 else answer(None)
243 )
244 else:
245 a_severity = ""
246 a_frequency = ""
247 q_a.append(
248 f"""
249 <tr>
250 <td>{q_main}</td>
251 <td>{a_main}</td>
252 <td>{a_severity}</td>
253 <td>{a_frequency}</td>
254 </tr>
255 """
256 )
257 return f"""
258 <div class="{CssClass.SUMMARY}">
259 <table class="{CssClass.SUMMARY}">
260 {self.get_is_complete_tr(req)}
261 <tr>
262 <td>Number of categories endorsed</td>
263 <td>{answer(self.n_endorsed())} / {N_QUESTIONS}</td>
264 </tr>
265 <tr>
266 <td>Severity score <sup>[c]</sup></td>
267 <td>{answer(self.severity_score())} /
268 {N_QUESTIONS * 3}</td>
269 </tr>
270 </table>
271 </div>
272 <table class="{CssClass.TASKDETAIL}">
273 <tr>
274 <th width="40%">Question</th>
275 <th width="20%">Experienced</th>
276 <th width="20%">Severity</th>
277 <th width="20%">Frequency</th>
278 </tr>
279 {"".join(q_a)}
280 </table>
281 <div class="{CssClass.FOOTNOTES}">
282 [a] Percentage of life, since age 18, spent experiencing this.
283 [b] Number of times this has happened, since age 18.
284 [c] The severity score is the sum of “severity” ratings
285 (0 = not experienced, 1 = not too bad, 1 = moderately bad,
286 3 = very bad).
287 </div>
288 """