Coverage for tasks/cbir.py : 51%

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/cbir.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, Tuple, Type
31from cardinal_pythonlib.stringfunc import strseq
32from sqlalchemy.ext.declarative import DeclarativeMeta
33from sqlalchemy.sql.schema import Column
34from sqlalchemy.sql.sqltypes import Float, Integer, UnicodeText
36from camcops_server.cc_modules.cc_constants import CssClass
37from camcops_server.cc_modules.cc_db import add_multiple_columns
38from camcops_server.cc_modules.cc_html import (
39 answer,
40 get_yes_no,
41 subheading_spanning_three_columns,
42 tr,
43)
44from camcops_server.cc_modules.cc_request import CamcopsRequest
45from camcops_server.cc_modules.cc_sqla_coltypes import (
46 BIT_CHECKER,
47 CamcopsColumn,
48)
49from camcops_server.cc_modules.cc_summaryelement import SummaryElement
50from camcops_server.cc_modules.cc_task import (
51 get_from_dict,
52 Task,
53 TaskHasPatientMixin,
54 TaskHasRespondentMixin,
55)
58# =============================================================================
59# CBI-R
60# =============================================================================
62QUESTION_SNIPPETS = [
63 "memory: poor day to day memory", # 1
64 "memory: asks same questions",
65 "memory: loses things",
66 "memory: forgets familiar names",
67 "memory: forgets names of objects", # 5
68 "memory: poor concentration",
69 "memory: forgets day",
70 "memory: confused in unusual surroundings",
71 "everyday: electrical appliances",
72 "everyday: writing", # 10
73 "everyday: using telephone",
74 "everyday: making hot drink",
75 "everyday: money",
76 "self-care: grooming",
77 "self-care: dressing", # 15
78 "self-care: feeding",
79 "self-care: bathing",
80 "behaviour: inappropriate humour",
81 "behaviour: temper outbursts",
82 "behaviour: uncooperative", # 20
83 "behaviour: socially embarrassing",
84 "behaviour: tactless/suggestive",
85 "behaviour: impulsive",
86 "mood: cries",
87 "mood: sad/depressed", # 25
88 "mood: restless/agitated",
89 "mood: irritable",
90 "beliefs: visual hallucinations",
91 "beliefs: auditory hallucinations",
92 "beliefs: delusions", # 30
93 "eating: sweet tooth",
94 "eating: repetitive",
95 "eating: increased appetite",
96 "eating: table manners",
97 "sleep: disturbed at night", # 35
98 "sleep: daytime sleep increased",
99 "stereotypy/motor: rigid/fixed opinions",
100 "stereotypy/motor: routines",
101 "stereotypy/motor: preoccupied with time",
102 "stereotypy/motor: expression/catchphrase", # 40
103 "motivation: less enthusiasm in usual interests",
104 "motivation: no interest in new things",
105 "motivation: fails to contact friends/family",
106 "motivation: indifferent to family/friend concerns",
107 "motivation: reduced affection", # 45
108]
111class CbiRMetaclass(DeclarativeMeta):
112 # noinspection PyInitNewSignature
113 def __init__(cls: Type['CbiR'],
114 name: str,
115 bases: Tuple[Type, ...],
116 classdict: Dict[str, Any]) -> None:
117 add_multiple_columns(
118 cls, "frequency", 1, cls.NQUESTIONS,
119 comment_fmt="Frequency Q{n}, {s} (0-4, higher worse)",
120 minimum=cls.MIN_SCORE, maximum=cls.MAX_SCORE,
121 comment_strings=QUESTION_SNIPPETS
122 )
123 add_multiple_columns(
124 cls, "distress", 1, cls.NQUESTIONS,
125 comment_fmt="Distress Q{n}, {s} (0-4, higher worse)",
126 minimum=cls.MIN_SCORE, maximum=cls.MAX_SCORE,
127 comment_strings=QUESTION_SNIPPETS
128 )
129 super().__init__(name, bases, classdict)
132class CbiR(TaskHasPatientMixin, TaskHasRespondentMixin, Task,
133 metaclass=CbiRMetaclass):
134 """
135 Server implementation of the CBI-R task.
136 """
137 __tablename__ = "cbir"
138 shortname = "CBI-R"
140 confirm_blanks = CamcopsColumn(
141 "confirm_blanks", Integer,
142 permitted_value_checker=BIT_CHECKER,
143 comment="Respondent confirmed that blanks are deliberate (N/A) "
144 "(0/NULL no, 1 yes)"
145 )
146 comments = Column(
147 "comments", UnicodeText,
148 comment="Additional comments"
149 )
151 MIN_SCORE = 0
152 MAX_SCORE = 4
153 QNUMS_MEMORY = (1, 8) # tuple: first, last
154 QNUMS_EVERYDAY = (9, 13)
155 QNUMS_SELF = (14, 17)
156 QNUMS_BEHAVIOUR = (18, 23)
157 QNUMS_MOOD = (24, 27)
158 QNUMS_BELIEFS = (28, 30)
159 QNUMS_EATING = (31, 34)
160 QNUMS_SLEEP = (35, 36)
161 QNUMS_STEREOTYPY = (37, 40)
162 QNUMS_MOTIVATION = (41, 45)
164 NQUESTIONS = 45
165 TASK_FIELDS = (strseq("frequency", 1, NQUESTIONS) +
166 strseq("distress", 1, NQUESTIONS))
168 @staticmethod
169 def longname(req: "CamcopsRequest") -> str:
170 _ = req.gettext
171 return _("Cambridge Behavioural Inventory, Revised")
173 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
174 return self.standard_task_summary_fields() + [
175 SummaryElement(
176 name="memory_frequency_pct",
177 coltype=Float(),
178 value=self.frequency_subscore(*self.QNUMS_MEMORY),
179 comment="Memory/orientation: frequency score (% of max)"),
180 SummaryElement(
181 name="memory_distress_pct",
182 coltype=Float(),
183 value=self.distress_subscore(*self.QNUMS_MEMORY),
184 comment="Memory/orientation: distress score (% of max)"),
185 SummaryElement(
186 name="everyday_frequency_pct",
187 coltype=Float(),
188 value=self.frequency_subscore(*self.QNUMS_EVERYDAY),
189 comment="Everyday skills: frequency score (% of max)"),
190 SummaryElement(
191 name="everyday_distress_pct",
192 coltype=Float(),
193 value=self.distress_subscore(*self.QNUMS_EVERYDAY),
194 comment="Everyday skills: distress score (% of max)"),
195 SummaryElement(
196 name="selfcare_frequency_pct",
197 coltype=Float(),
198 value=self.frequency_subscore(*self.QNUMS_SELF),
199 comment="Self-care: frequency score (% of max)"),
200 SummaryElement(
201 name="selfcare_distress_pct",
202 coltype=Float(),
203 value=self.distress_subscore(*self.QNUMS_SELF),
204 comment="Self-care: distress score (% of max)"),
205 SummaryElement(
206 name="behaviour_frequency_pct",
207 coltype=Float(),
208 value=self.frequency_subscore(*self.QNUMS_BEHAVIOUR),
209 comment="Abnormal behaviour: frequency score (% of max)"),
210 SummaryElement(
211 name="behaviour_distress_pct",
212 coltype=Float(),
213 value=self.distress_subscore(*self.QNUMS_BEHAVIOUR),
214 comment="Abnormal behaviour: distress score (% of max)"),
215 SummaryElement(
216 name="mood_frequency_pct",
217 coltype=Float(),
218 value=self.frequency_subscore(*self.QNUMS_MOOD),
219 comment="Mood: frequency score (% of max)"),
220 SummaryElement(
221 name="mood_distress_pct",
222 coltype=Float(),
223 value=self.distress_subscore(*self.QNUMS_MOOD),
224 comment="Mood: distress score (% of max)"),
225 SummaryElement(
226 name="beliefs_frequency_pct",
227 coltype=Float(),
228 value=self.frequency_subscore(*self.QNUMS_BELIEFS),
229 comment="Beliefs: frequency score (% of max)"),
230 SummaryElement(
231 name="beliefs_distress_pct",
232 coltype=Float(),
233 value=self.distress_subscore(*self.QNUMS_BELIEFS),
234 comment="Beliefs: distress score (% of max)"),
235 SummaryElement(
236 name="eating_frequency_pct",
237 coltype=Float(),
238 value=self.frequency_subscore(*self.QNUMS_EATING),
239 comment="Eating habits: frequency score (% of max)"),
240 SummaryElement(
241 name="eating_distress_pct",
242 coltype=Float(),
243 value=self.distress_subscore(*self.QNUMS_EATING),
244 comment="Eating habits: distress score (% of max)"),
245 SummaryElement(
246 name="sleep_frequency_pct",
247 coltype=Float(),
248 value=self.frequency_subscore(*self.QNUMS_SLEEP),
249 comment="Sleep: frequency score (% of max)"),
250 SummaryElement(
251 name="sleep_distress_pct",
252 coltype=Float(),
253 value=self.distress_subscore(*self.QNUMS_SLEEP),
254 comment="Sleep: distress score (% of max)"),
255 SummaryElement(
256 name="stereotypic_frequency_pct",
257 coltype=Float(),
258 value=self.frequency_subscore(*self.QNUMS_STEREOTYPY),
259 comment="Stereotypic and motor behaviours: frequency "
260 "score (% of max)"),
261 SummaryElement(
262 name="stereotypic_distress_pct",
263 coltype=Float(),
264 value=self.distress_subscore(*self.QNUMS_STEREOTYPY),
265 comment="Stereotypic and motor behaviours: distress "
266 "score (% of max)"),
267 SummaryElement(
268 name="motivation_frequency_pct",
269 coltype=Float(),
270 value=self.frequency_subscore(*self.QNUMS_MOTIVATION),
271 comment="Motivation: frequency score (% of max)"),
272 SummaryElement(
273 name="motivation_distress_pct",
274 coltype=Float(),
275 value=self.distress_subscore(*self.QNUMS_MOTIVATION),
276 comment="Motivation: distress score (% of max)"),
277 ]
279 def subscore(self, first: int, last: int, fieldprefix: str) \
280 -> Optional[float]:
281 score = 0
282 n = 0
283 for q in range(first, last + 1):
284 value = getattr(self, fieldprefix + str(q))
285 if value is not None:
286 score += value / self.MAX_SCORE
287 n += 1
288 return 100 * score / n if n > 0 else None
290 def frequency_subscore(self, first: int, last: int) -> Optional[float]:
291 return self.subscore(first, last, "frequency")
293 def distress_subscore(self, first: int, last: int) -> Optional[float]:
294 return self.subscore(first, last, "distress")
296 def is_complete(self) -> bool:
297 if (not self.field_contents_valid() or
298 not self.is_respondent_complete()):
299 return False
300 if self.confirm_blanks:
301 return True
302 return self.all_fields_not_none(self.TASK_FIELDS)
304 def get_task_html(self, req: CamcopsRequest) -> str:
305 freq_dict = {None: None}
306 distress_dict = {None: None}
307 for a in range(self.MIN_SCORE, self.MAX_SCORE + 1):
308 freq_dict[a] = self.wxstring(req, "f" + str(a))
309 distress_dict[a] = self.wxstring(req, "d" + str(a))
311 heading_memory = self.wxstring(req, "h_memory")
312 heading_everyday = self.wxstring(req, "h_everyday")
313 heading_selfcare = self.wxstring(req, "h_selfcare")
314 heading_behaviour = self.wxstring(req, "h_abnormalbehaviour")
315 heading_mood = self.wxstring(req, "h_mood")
316 heading_beliefs = self.wxstring(req, "h_beliefs")
317 heading_eating = self.wxstring(req, "h_eating")
318 heading_sleep = self.wxstring(req, "h_sleep")
319 heading_motor = self.wxstring(req, "h_stereotypy_motor")
320 heading_motivation = self.wxstring(req, "h_motivation")
322 def get_question_rows(first, last):
323 html = ""
324 for q in range(first, last + 1):
325 f = getattr(self, "frequency" + str(q))
326 d = getattr(self, "distress" + str(q))
327 fa = (f"{f}: {get_from_dict(freq_dict, f)}"
328 if f is not None else None)
329 da = (f"{d}: {get_from_dict(distress_dict, d)}"
330 if d is not None else None)
331 html += tr(
332 self.wxstring(req, "q" + str(q)),
333 answer(fa),
334 answer(da),
335 )
336 return html
338 h = f"""
339 <div class="{CssClass.SUMMARY}">
340 <table class="{CssClass.SUMMARY}">
341 {self.get_is_complete_tr(req)}
342 </table>
343 <table class="{CssClass.SUMMARY}">
344 <tr>
345 <th>Subscale</th>
346 <th>Frequency (% of max)</th>
347 <th>Distress (% of max)</th>
348 </tr>
349 <tr>
350 <td>{heading_memory}</td>
351 <td>{answer(self.frequency_subscore(*self.QNUMS_MEMORY))}</td>
352 <td>{answer(self.distress_subscore(*self.QNUMS_MEMORY))}</td>
353 </tr>
354 <tr>
355 <td>{heading_everyday}</td>
356 <td>{answer(self.frequency_subscore(*self.QNUMS_EVERYDAY))}</td>
357 <td>{answer(self.distress_subscore(*self.QNUMS_EVERYDAY))}</td>
358 </tr>
359 <tr>
360 <td>{heading_selfcare}</td>
361 <td>{answer(self.frequency_subscore(*self.QNUMS_SELF))}</td>
362 <td>{answer(self.distress_subscore(*self.QNUMS_SELF))}</td>
363 </tr>
364 <tr>
365 <td>{heading_behaviour}</td>
366 <td>{answer(self.frequency_subscore(*self.QNUMS_BEHAVIOUR))}</td>
367 <td>{answer(self.distress_subscore(*self.QNUMS_BEHAVIOUR))}</td>
368 </tr>
369 <tr>
370 <td>{heading_mood}</td>
371 <td>{answer(self.frequency_subscore(*self.QNUMS_MOOD))}</td>
372 <td>{answer(self.distress_subscore(*self.QNUMS_MOOD))}</td>
373 </tr>
374 <tr>
375 <td>{heading_beliefs}</td>
376 <td>{answer(self.frequency_subscore(*self.QNUMS_BELIEFS))}</td>
377 <td>{answer(self.distress_subscore(*self.QNUMS_BELIEFS))}</td>
378 </tr>
379 <tr>
380 <td>{heading_eating}</td>
381 <td>{answer(self.frequency_subscore(*self.QNUMS_EATING))}</td>
382 <td>{answer(self.distress_subscore(*self.QNUMS_EATING))}</td>
383 </tr>
384 <tr>
385 <td>{heading_sleep}</td>
386 <td>{answer(self.frequency_subscore(*self.QNUMS_SLEEP))}</td>
387 <td>{answer(self.distress_subscore(*self.QNUMS_SLEEP))}</td>
388 </tr>
389 <tr>
390 <td>{heading_motor}</td>
391 <td>{answer(self.frequency_subscore(*self.QNUMS_STEREOTYPY))}</td>
392 <td>{answer(self.distress_subscore(*self.QNUMS_STEREOTYPY))}</td>
393 </tr>
394 <tr>
395 <td>{heading_motivation}</td>
396 <td>{answer(self.frequency_subscore(*self.QNUMS_MOTIVATION))}</td>
397 <td>{answer(self.distress_subscore(*self.QNUMS_MOTIVATION))}</td>
398 </tr>
399 </table>
400 </div>
401 <table class="{CssClass.TASKDETAIL}">
402 {tr(
403 "Respondent confirmed that blanks are deliberate (N/A)",
404 answer(get_yes_no(req, self.confirm_blanks))
405 )}
406 {tr("Comments", answer(self.comments, default=""))}
407 </table>
408 <table class="{CssClass.TASKDETAIL}">
409 <tr>
410 <th width="50%">Question</th>
411 <th width="25%">Frequency (0–4)</th>
412 <th width="25%">Distress (0–4)</th>
413 </tr>
414 {subheading_spanning_three_columns(heading_memory)}
415 {get_question_rows(*self.QNUMS_MEMORY)}
416 {subheading_spanning_three_columns(heading_everyday)}
417 {get_question_rows(*self.QNUMS_EVERYDAY)}
418 {subheading_spanning_three_columns(heading_selfcare)}
419 {get_question_rows(*self.QNUMS_SELF)}
420 {subheading_spanning_three_columns(heading_behaviour)}
421 {get_question_rows(*self.QNUMS_BEHAVIOUR)}
422 {subheading_spanning_three_columns(heading_mood)}
423 {get_question_rows(*self.QNUMS_MOOD)}
424 {subheading_spanning_three_columns(heading_beliefs)}
425 {get_question_rows(*self.QNUMS_BELIEFS)}
426 {subheading_spanning_three_columns(heading_eating)}
427 {get_question_rows(*self.QNUMS_EATING)}
428 {subheading_spanning_three_columns(heading_sleep)}
429 {get_question_rows(*self.QNUMS_SLEEP)}
430 {subheading_spanning_three_columns(heading_motor)}
431 {get_question_rows(*self.QNUMS_STEREOTYPY)}
432 {subheading_spanning_three_columns(heading_motivation)}
433 {get_question_rows(*self.QNUMS_MOTIVATION)}
434 </table>
435 """
436 return h