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