Coverage for tasks/lynall_iam_life.py : 56%

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