Coverage for tasks/cgi_task.py: 58%
76 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/cgi_task.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"""
28from typing import cast, Dict, List, Optional
30from sqlalchemy.orm import Mapped
31from sqlalchemy.sql.sqltypes import Integer
33from camcops_server.cc_modules.cc_constants import CssClass
34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
35from camcops_server.cc_modules.cc_html import answer, italic, tr, tr_qa
36from camcops_server.cc_modules.cc_request import CamcopsRequest
37from camcops_server.cc_modules.cc_sqla_coltypes import (
38 mapped_camcops_column,
39 PermittedValueChecker,
40)
41from camcops_server.cc_modules.cc_summaryelement import SummaryElement
42from camcops_server.cc_modules.cc_task import (
43 get_from_dict,
44 Task,
45 TaskHasClinicianMixin,
46 TaskHasPatientMixin,
47)
48from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
51# =============================================================================
52# CGI
53# =============================================================================
56class Cgi(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501
57 """
58 Server implementation of the CGI task.
59 """
61 __tablename__ = "cgi"
62 shortname = "CGI"
63 provides_trackers = True
65 q1: Mapped[Optional[int]] = mapped_camcops_column(
66 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
67 comment="Q1. Severity (1-7, higher worse, 0 not assessed)",
68 )
69 q2: Mapped[Optional[int]] = mapped_camcops_column(
70 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
71 comment="Q2. Global improvement (1-7, higher worse, 0 not assessed)",
72 )
73 q3t: Mapped[Optional[int]] = mapped_camcops_column(
74 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4),
75 comment="Q3T. Therapeutic effects (1-4, higher worse, 0 not assessed)",
76 )
77 q3s: Mapped[Optional[int]] = mapped_camcops_column(
78 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4),
79 comment="Q3S. Side effects (1-4, higher worse, 0 not assessed)",
80 )
81 q3: Mapped[Optional[int]] = mapped_camcops_column(
82 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=16),
83 comment="Q3 (calculated). Efficacy index [(Q3T - 1) * 4 + Q3S].",
84 )
86 TASK_FIELDS = ["q1", "q2", "q3t", "q3s", "q3"]
87 MAX_SCORE = 30
89 @staticmethod
90 def longname(req: "CamcopsRequest") -> str:
91 _ = req.gettext
92 return _("Clinical Global Impressions")
94 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
95 return [
96 TrackerInfo(
97 value=self.total_score(),
98 plot_label="CGI total score",
99 axis_label=f"Total score (out of {self.MAX_SCORE})",
100 axis_min=-0.5,
101 axis_max=self.MAX_SCORE + 0.5,
102 )
103 ]
105 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
106 if not self.is_complete():
107 return CTV_INCOMPLETE
108 return [
109 CtvInfo(
110 content=(
111 f"CGI total score {self.total_score()}/{self.MAX_SCORE}"
112 )
113 )
114 ]
116 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
117 return self.standard_task_summary_fields() + [
118 SummaryElement(
119 name="total", coltype=Integer(), value=self.total_score()
120 )
121 ]
123 def is_complete(self) -> bool:
124 if not (
125 self.all_fields_not_none(self.TASK_FIELDS)
126 and self.field_contents_valid()
127 ):
128 return False
129 # Requirement for everything to be non-zero removed in v2.0.0
130 # if self.q1 == 0 or self.q2 == 0 or self.q3t == 0 or self.q3s == 0:
131 # return False
132 return True
134 def total_score(self) -> int:
135 return cast(int, self.sum_fields(["q1", "q2", "q3"]))
137 def get_task_html(self, req: CamcopsRequest) -> str:
138 q1_dict = {
139 None: None,
140 0: self.wxstring(req, "q1_option0"),
141 1: self.wxstring(req, "q1_option1"),
142 2: self.wxstring(req, "q1_option2"),
143 3: self.wxstring(req, "q1_option3"),
144 4: self.wxstring(req, "q1_option4"),
145 5: self.wxstring(req, "q1_option5"),
146 6: self.wxstring(req, "q1_option6"),
147 7: self.wxstring(req, "q1_option7"),
148 }
149 q2_dict = {
150 None: None,
151 0: self.wxstring(req, "q2_option0"),
152 1: self.wxstring(req, "q2_option1"),
153 2: self.wxstring(req, "q2_option2"),
154 3: self.wxstring(req, "q2_option3"),
155 4: self.wxstring(req, "q2_option4"),
156 5: self.wxstring(req, "q2_option5"),
157 6: self.wxstring(req, "q2_option6"),
158 7: self.wxstring(req, "q2_option7"),
159 }
160 q3t_dict = {
161 None: None,
162 0: self.wxstring(req, "q3t_option0"),
163 1: self.wxstring(req, "q3t_option1"),
164 2: self.wxstring(req, "q3t_option2"),
165 3: self.wxstring(req, "q3t_option3"),
166 4: self.wxstring(req, "q3t_option4"),
167 }
168 q3s_dict = {
169 None: None,
170 0: self.wxstring(req, "q3s_option0"),
171 1: self.wxstring(req, "q3s_option1"),
172 2: self.wxstring(req, "q3s_option2"),
173 3: self.wxstring(req, "q3s_option3"),
174 4: self.wxstring(req, "q3s_option4"),
175 }
177 tr_total_score = tr(
178 "Total score <sup>[1]</sup>", answer(self.total_score())
179 )
180 tr_q1 = tr_qa(
181 self.wxstring(req, "q1_s") + " <sup>[2]</sup>",
182 get_from_dict(q1_dict, self.q1),
183 )
184 tr_q2 = tr_qa(
185 self.wxstring(req, "q2_s") + " <sup>[2]</sup>",
186 get_from_dict(q2_dict, self.q2),
187 )
188 tr_q3t = tr_qa(
189 self.wxstring(req, "q3t_s") + " <sup>[3]</sup>",
190 get_from_dict(q3t_dict, self.q3t),
191 )
192 tr_q3s = tr_qa(
193 self.wxstring(req, "q3s_s") + " <sup>[3]</sup>",
194 get_from_dict(q3s_dict, self.q3s),
195 )
196 tr_q3 = tr(
197 f"""
198 {self.wxstring(req, "q3_s")} <sup>[4]</sup>
199 <div class="{CssClass.SMALLPRINT}">
200 [(Q3T – 1) × 4 + Q3S]
201 </div>
202 """,
203 answer(self.q3, formatter_answer=italic),
204 )
205 return f"""
206 <div class="{CssClass.SUMMARY}">
207 <table class="{CssClass.SUMMARY}">
208 {self.get_is_complete_tr(req)}
209 {tr_total_score}
210 </table>
211 </div>
212 <table class="{CssClass.TASKDETAIL}">
213 <tr>
214 <th width="30%">Question</th>
215 <th width="70%">Answer</th>
216 </tr>
217 {tr_q1}
218 {tr_q2}
219 {tr_q3t}
220 {tr_q3s}
221 {tr_q3}
222 </table>
223 <div class="{CssClass.FOOTNOTES}">
224 [1] Total score: Q1 + Q2 + Q3. Range 3–30 when complete.
225 [2] Questions 1 and 2 are scored 1–7 (0 for not assessed).
226 [3] Questions 3T and 3S are scored 1–4 (0 for not assessed).
227 [4] Q3 is scored 1–16 if Q3T/Q3S complete.
228 </div>
229 """
232# =============================================================================
233# CGI-I
234# =============================================================================
237class CgiI(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501
238 __tablename__ = "cgi_i"
239 shortname = "CGI-I"
240 extrastring_taskname = "cgi" # shares with CGI
241 info_filename_stem = "cgi"
243 q: Mapped[Optional[int]] = mapped_camcops_column(
244 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
245 comment="Global improvement (1-7, higher worse)",
246 )
248 TASK_FIELDS = ["q"]
250 @staticmethod
251 def longname(req: "CamcopsRequest") -> str:
252 _ = req.gettext
253 return _("Clinical Global Impressions – Improvement")
255 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
256 if not self.is_complete():
257 return CTV_INCOMPLETE
258 return [
259 CtvInfo(
260 content="CGI-I rating: {}".format(self.get_rating_text(req))
261 )
262 ]
264 def is_complete(self) -> bool:
265 return (
266 self.all_fields_not_none(self.TASK_FIELDS)
267 and self.field_contents_valid()
268 )
270 def get_rating_text(self, req: CamcopsRequest) -> str:
271 qdict = self.get_q_dict(req)
272 return get_from_dict(qdict, self.q)
274 def get_q_dict(self, req: CamcopsRequest) -> Dict:
275 return {
276 None: None,
277 0: self.wxstring(req, "q2_option0"),
278 1: self.wxstring(req, "q2_option1"),
279 2: self.wxstring(req, "q2_option2"),
280 3: self.wxstring(req, "q2_option3"),
281 4: self.wxstring(req, "q2_option4"),
282 5: self.wxstring(req, "q2_option5"),
283 6: self.wxstring(req, "q2_option6"),
284 7: self.wxstring(req, "q2_option7"),
285 }
287 def get_task_html(self, req: CamcopsRequest) -> str:
288 return f"""
289 <div class="{CssClass.SUMMARY}">
290 <table class="{CssClass.SUMMARY}">
291 {self.get_is_complete_tr(req)}
292 </table>
293 </div>
294 <table class="{CssClass.TASKDETAIL}">
295 <tr>
296 <th width="50%">Question</th>
297 <th width="50%">Answer</th>
298 </tr>
299 {tr_qa(self.wxstring(req, "i_q"), self.get_rating_text(req))}
300 </table>
301 """