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