Coverage for tasks/core10.py : 61%

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