Coverage for tasks/cia.py: 50%
64 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/cia.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**The Clinical Impairment Assessment questionnaire (CIA) task.**
28"""
30from typing import Any, List, Optional, Type
32from cardinal_pythonlib.stringfunc import strnumlist, strseq
33from sqlalchemy.sql.sqltypes import Integer
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_db import add_multiple_columns
37from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
38from camcops_server.cc_modules.cc_request import CamcopsRequest
39from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
40from camcops_server.cc_modules.cc_text import SS
41from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
44class Cia( # type: ignore[misc]
45 TaskHasPatientMixin,
46 Task,
47):
48 __tablename__ = "cia"
49 shortname = "CIA"
50 provides_trackers = True
52 Q_PREFIX = "q"
53 FIRST_Q = 1
54 LAST_Q = 16
55 MAX_SCORE = 48
57 @classmethod
58 def extend_columns(cls: Type["Cia"], **kwargs: Any) -> None:
60 add_multiple_columns(
61 cls,
62 cls.Q_PREFIX,
63 cls.FIRST_Q,
64 cls.LAST_Q,
65 coltype=Integer,
66 minimum=0,
67 maximum=3,
68 comment_fmt=cls.Q_PREFIX + "{n} - {s}",
69 comment_strings=[
70 "difficult to concentrate",
71 "critical of self",
72 "going out",
73 "affected work performance",
74 "forgetful",
75 "everyday decisions",
76 "meals with family",
77 "upset",
78 "ashamed",
79 "difficult to eat out",
80 "guilty",
81 "things used to enjoy",
82 "absent-minded",
83 "failure",
84 "relationships",
85 "worry",
86 ],
87 )
89 ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q)
90 MANDATORY_QUESTIONS = [1, 2, 5, 6, 8, 9, 11, 12, 13, 14, 15, 16]
91 MANDATORY_FIELD_NAMES = strnumlist(Q_PREFIX, MANDATORY_QUESTIONS)
93 @staticmethod
94 def longname(req: CamcopsRequest) -> str:
95 _ = req.gettext
96 return _("Clinical Impairment Assessment questionnaire")
98 def is_complete(self) -> bool:
99 if self.any_fields_none(self.MANDATORY_FIELD_NAMES):
100 return False
102 return True
104 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
105 return [
106 TrackerInfo(
107 value=self.global_score(),
108 plot_label="CIA global impairment score",
109 axis_label=f"Global score (out of {self.MAX_SCORE})",
110 axis_min=-0.5,
111 axis_max=self.MAX_SCORE + 0.5,
112 ),
113 ]
115 def global_score(self) -> Optional[float]:
116 """
117 The original paper states:
119 "To obtain the global CIA impairment score the ratings on all items are
120 added together with prorating of missing ratings, so long as at least
121 12 of the 16 items have been rated."
123 In our implementation all questions are mandatory except for 3, 4, 7
124 and 10. So there won't be fewer than 12 items rated for a complete
125 questionnaire.
126 """
127 if not self.is_complete():
128 return None
130 num_answered = self.n_fields_not_none(self.ALL_FIELD_NAMES)
131 scale_factor = self.LAST_Q / num_answered
133 return scale_factor * self.sum_fields(self.ALL_FIELD_NAMES)
135 def get_task_html(self, req: CamcopsRequest) -> str:
136 question = self.xstring(req, "grid_title")
138 rows = ""
139 for q_num in range(self.FIRST_Q, self.LAST_Q + 1):
140 field = self.Q_PREFIX + str(q_num)
141 question_cell = "{}. {}".format(q_num, self.wxstring(req, field))
143 rows += tr_qa(question_cell, self.get_answer_cell(req, q_num))
145 global_score = self.global_score()
146 if global_score is None:
147 global_score_display = "?"
148 else:
149 global_score_display = "{:.2f} / {}".format(
150 global_score, self.MAX_SCORE
151 )
153 html = """
154 <div class="{CssClass.SUMMARY}">
155 <table class="{CssClass.SUMMARY}">
156 {tr_is_complete}
157 {global_score}
158 </table>
159 </div>
160 <table class="{CssClass.TASKDETAIL}">
161 <tr>
162 <th width="60%">{question}</th>
163 <th width="40%">Response</th>
164 </tr>
165 {rows}
166 </table>
167 <div class="{CssClass.FOOTNOTES}">
168 [1] Sum for all questions with prorating of missing ratings,
169 so long as at least 12 of the 16 items have been rated.
170 </div>
171 """.format(
172 CssClass=CssClass,
173 tr_is_complete=self.get_is_complete_tr(req),
174 global_score=tr(
175 req.sstring(SS.TOTAL_SCORE) + "<sup>[1]</sup>",
176 answer(global_score_display),
177 ),
178 question=question,
179 rows=rows,
180 )
181 return html
183 def get_answer_cell(self, req: CamcopsRequest, q_num: int) -> str:
184 q_field = self.Q_PREFIX + str(q_num)
186 score = getattr(self, q_field)
187 if score is None:
188 if q_num in self.MANDATORY_QUESTIONS:
189 return "?"
191 return req.sstring(SS.NA)
193 meaning = self.get_score_meaning(req, score)
194 return f"{score} [{meaning}]"
196 def get_score_meaning(self, req: CamcopsRequest, score: int) -> str:
197 return self.wxstring(req, f"option_{score}")