Coverage for tasks/core10.py: 61%
96 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/core10.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"""
30import logging
31from typing import Dict, List, Optional, Type
33from cardinal_pythonlib.classes import classproperty
34from cardinal_pythonlib.stringfunc import strseq
35from semantic_version import Version
36from sqlalchemy.sql.sqltypes import Integer
38from camcops_server.cc_modules.cc_constants import CssClass
39from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
40from camcops_server.cc_modules.cc_html import answer, tr, tr_qa
41from camcops_server.cc_modules.cc_report import (
42 AverageScoreReport,
43 ScoreDetails,
44)
45from camcops_server.cc_modules.cc_request import CamcopsRequest
46from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
47from camcops_server.cc_modules.cc_sqla_coltypes import (
48 CamcopsColumn,
49 ZERO_TO_FOUR_CHECKER,
50)
51from camcops_server.cc_modules.cc_summaryelement import SummaryElement
52from camcops_server.cc_modules.cc_task import (
53 get_from_dict,
54 Task,
55 TaskHasPatientMixin,
56)
57from camcops_server.cc_modules.cc_trackerhelpers import (
58 TrackerAxisTick,
59 TrackerInfo,
60)
62log = logging.getLogger(__name__)
65# =============================================================================
66# CORE-10
67# =============================================================================
70class Core10(TaskHasPatientMixin, Task):
71 """
72 Server implementation of the CORE-10 task.
73 """
75 __tablename__ = "core10"
76 shortname = "CORE-10"
77 provides_trackers = True
79 COMMENT_NORMAL = " (0 not at all - 4 most or all of the time)"
80 COMMENT_REVERSED = " (0 most or all of the time - 4 not at all)"
82 q1 = CamcopsColumn(
83 "q1",
84 Integer,
85 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
86 comment="Q1 (tension/anxiety)" + COMMENT_NORMAL,
87 )
88 q2 = CamcopsColumn(
89 "q2",
90 Integer,
91 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
92 comment="Q2 (support)" + COMMENT_REVERSED,
93 )
94 q3 = CamcopsColumn(
95 "q3",
96 Integer,
97 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
98 comment="Q3 (coping)" + COMMENT_REVERSED,
99 )
100 q4 = CamcopsColumn(
101 "q4",
102 Integer,
103 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
104 comment="Q4 (talking is too much)" + COMMENT_NORMAL,
105 )
106 q5 = CamcopsColumn(
107 "q5",
108 Integer,
109 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
110 comment="Q5 (panic)" + COMMENT_NORMAL,
111 )
112 q6 = CamcopsColumn(
113 "q6",
114 Integer,
115 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
116 comment="Q6 (suicidality)" + COMMENT_NORMAL,
117 )
118 q7 = CamcopsColumn(
119 "q7",
120 Integer,
121 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
122 comment="Q7 (sleep problems)" + COMMENT_NORMAL,
123 )
124 q8 = CamcopsColumn(
125 "q8",
126 Integer,
127 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
128 comment="Q8 (despair/hopelessness)" + COMMENT_NORMAL,
129 )
130 q9 = CamcopsColumn(
131 "q9",
132 Integer,
133 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
134 comment="Q9 (unhappy)" + COMMENT_NORMAL,
135 )
136 q10 = CamcopsColumn(
137 "q10",
138 Integer,
139 permitted_value_checker=ZERO_TO_FOUR_CHECKER,
140 comment="Q10 (unwanted images)" + COMMENT_NORMAL,
141 )
143 N_QUESTIONS = 10
144 MAX_SCORE = 4 * N_QUESTIONS
145 QUESTION_FIELDNAMES = strseq("q", 1, N_QUESTIONS)
147 @staticmethod
148 def longname(req: "CamcopsRequest") -> str:
149 _ = req.gettext
150 return _("Clinical Outcomes in Routine Evaluation, 10-item measure")
152 # noinspection PyMethodParameters
153 @classproperty
154 def minimum_client_version(cls) -> Version:
155 return Version("2.2.8")
157 def is_complete(self) -> bool:
158 return self.all_fields_not_none(self.QUESTION_FIELDNAMES)
160 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
161 return [
162 TrackerInfo(
163 value=self.clinical_score(),
164 plot_label="CORE-10 clinical score (rating distress)",
165 axis_label=f"Clinical score (out of {self.MAX_SCORE})",
166 axis_min=-0.5,
167 axis_max=self.MAX_SCORE + 0.5,
168 axis_ticks=[
169 TrackerAxisTick(40, "40"),
170 TrackerAxisTick(35, "35"),
171 TrackerAxisTick(30, "30"),
172 TrackerAxisTick(25, "25"),
173 TrackerAxisTick(20, "20"),
174 TrackerAxisTick(15, "15"),
175 TrackerAxisTick(10, "10"),
176 TrackerAxisTick(5, "5"),
177 TrackerAxisTick(0, "0"),
178 ],
179 horizontal_lines=[30, 20, 10],
180 )
181 ]
183 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
184 if not self.is_complete():
185 return CTV_INCOMPLETE
186 return [
187 CtvInfo(
188 content=(
189 f"CORE-10 clinical score "
190 f"{self.clinical_score()}/{self.MAX_SCORE}"
191 )
192 )
193 ]
194 # todo: CORE10: add suicidality to clinical text?
196 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
197 return self.standard_task_summary_fields() + [
198 SummaryElement(
199 name="clinical_score",
200 coltype=Integer(),
201 value=self.clinical_score(),
202 comment=f"Clinical score (/{self.MAX_SCORE})",
203 )
204 ]
206 def total_score(self) -> int:
207 return self.sum_fields(self.QUESTION_FIELDNAMES)
209 def n_questions_complete(self) -> int:
210 return self.n_fields_not_none(self.QUESTION_FIELDNAMES)
212 def clinical_score(self) -> float:
213 n_q_completed = self.n_questions_complete()
214 if n_q_completed == 0:
215 # avoid division by zero
216 return 0
217 return self.N_QUESTIONS * self.total_score() / n_q_completed
219 def get_task_html(self, req: CamcopsRequest) -> str:
220 normal_dict = {
221 None: None,
222 0: "0 — " + self.wxstring(req, "a0"),
223 1: "1 — " + self.wxstring(req, "a1"),
224 2: "2 — " + self.wxstring(req, "a2"),
225 3: "3 — " + self.wxstring(req, "a3"),
226 4: "4 — " + self.wxstring(req, "a4"),
227 }
228 reversed_dict = {
229 None: None,
230 0: "0 — " + self.wxstring(req, "a4"),
231 1: "1 — " + self.wxstring(req, "a3"),
232 2: "2 — " + self.wxstring(req, "a2"),
233 3: "3 — " + self.wxstring(req, "a1"),
234 4: "4 — " + self.wxstring(req, "a0"),
235 }
237 def get_tr_qa(qnum_: int, mapping: Dict[Optional[int], str]) -> str:
238 nstr = str(qnum_)
239 return tr_qa(
240 self.wxstring(req, "q" + nstr),
241 get_from_dict(mapping, getattr(self, "q" + nstr)),
242 )
244 q_a = get_tr_qa(1, normal_dict)
245 for qnum in (2, 3):
246 q_a += get_tr_qa(qnum, reversed_dict)
247 for qnum in range(4, self.N_QUESTIONS + 1):
248 q_a += get_tr_qa(qnum, normal_dict)
250 tr_clinical_score = tr(
251 "Clinical score <sup>[1]</sup>",
252 answer(self.clinical_score()) + " / {}".format(self.MAX_SCORE),
253 )
254 return f"""
255 <div class="{CssClass.SUMMARY}">
256 <table class="{CssClass.SUMMARY}">
257 {self.get_is_complete_tr(req)}
258 {tr_clinical_score}
259 </table>
260 </div>
261 <div class="{CssClass.EXPLANATION}">
262 Ratings are over the last week.
263 </div>
264 <table class="{CssClass.TASKDETAIL}">
265 <tr>
266 <th width="60%">Question</th>
267 <th width="40%">Answer</th>
268 </tr>
269 {q_a}
270 </table>
271 <div class="{CssClass.FOOTNOTES}">
272 [1] Clinical score is: number of questions × total score
273 ÷ number of questions completed. If all questions are
274 completed, it's just the total score.
275 </div>
276 """
278 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
279 codes = [
280 SnomedExpression(
281 req.snomed(SnomedLookup.CORE10_PROCEDURE_ASSESSMENT)
282 )
283 ]
284 if self.is_complete():
285 codes.append(
286 SnomedExpression(
287 req.snomed(SnomedLookup.CORE10_SCALE),
288 {
289 req.snomed(
290 SnomedLookup.CORE10_SCORE
291 ): self.total_score()
292 },
293 )
294 )
295 return codes
298class Core10Report(AverageScoreReport):
299 """
300 An average score of the people seen at the start of treatment
301 an average final measure and an average progress score.
302 """
304 # noinspection PyMethodParameters
305 @classproperty
306 def report_id(cls) -> str:
307 return "core10"
309 @classmethod
310 def title(cls, req: "CamcopsRequest") -> str:
311 _ = req.gettext
312 return _("CORE-10 — Average scores")
314 # noinspection PyMethodParameters
315 @classproperty
316 def task_class(cls) -> Type[Task]:
317 return Core10
319 @classmethod
320 def scoretypes(cls, req: "CamcopsRequest") -> List[ScoreDetails]:
321 _ = req.gettext
322 return [
323 ScoreDetails(
324 name=_("CORE-10 clinical score"),
325 scorefunc=Core10.clinical_score,
326 minimum=0,
327 maximum=Core10.MAX_SCORE,
328 higher_score_is_better=False,
329 )
330 ]