Coverage for tasks/cesd.py : 59%

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/cesd.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- By Joe Kearney, Rudolf Cardinal.
29"""
31from typing import Any, Dict, List, Tuple, Type
33from cardinal_pythonlib.classes import classproperty
34from cardinal_pythonlib.stringfunc import strseq
35from semantic_version import Version
36from sqlalchemy.ext.declarative import DeclarativeMeta
37from sqlalchemy.sql.sqltypes import Boolean
39from camcops_server.cc_modules.cc_constants import CssClass
40from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
41from camcops_server.cc_modules.cc_db import add_multiple_columns
42from camcops_server.cc_modules.cc_html import get_yes_no, tr_qa
43from camcops_server.cc_modules.cc_request import CamcopsRequest
45from camcops_server.cc_modules.cc_summaryelement import SummaryElement
46from camcops_server.cc_modules.cc_task import (
47 get_from_dict,
48 Task,
49 TaskHasPatientMixin,
50)
51from camcops_server.cc_modules.cc_text import SS
52from camcops_server.cc_modules.cc_trackerhelpers import (
53 equally_spaced_int,
54 regular_tracker_axis_ticks_int,
55 TrackerInfo,
56 TrackerLabel,
57)
60# =============================================================================
61# CESD
62# =============================================================================
64class CesdMetaclass(DeclarativeMeta):
65 """
66 There is a multilayer metaclass problem; see hads.py for discussion.
67 """
68 # noinspection PyInitNewSignature
69 def __init__(cls: Type['Cesd'],
70 name: str,
71 bases: Tuple[Type, ...],
72 classdict: Dict[str, Any]) -> None:
73 add_multiple_columns(
74 cls, "q", 1, cls.N_QUESTIONS,
75 minimum=0, maximum=4,
76 comment_fmt=(
77 "Q{n} ({s}) (0 rarely/none of the time - 4 all of the time)"
78 ),
79 comment_strings=[
80 "sensitivity/irritability",
81 "poor appetite",
82 "unshakeable blues",
83 "low self-esteem",
84 "poor concentration",
85 "depressed",
86 "everything effortful",
87 "hopeful",
88 "feelings of failure",
89 "fearful",
90 "sleep restless",
91 "happy",
92 "uncommunicative",
93 "lonely",
94 "perceived unfriendliness",
95 "enjoyment",
96 "crying spells",
97 "sadness",
98 "feeling disliked",
99 "could not get going",
100 ]
101 )
102 super().__init__(name, bases, classdict)
105class Cesd(TaskHasPatientMixin, Task,
106 metaclass=CesdMetaclass):
107 """
108 Server implementation of the CESD task.
109 """
110 __tablename__ = 'cesd'
111 shortname = 'CESD'
112 provides_trackers = True
113 extrastring_taskname = "cesd"
114 N_QUESTIONS = 20
115 N_ANSWERS = 4
116 DEPRESSION_RISK_THRESHOLD = 16
117 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS)
118 TASK_FIELDS = SCORED_FIELDS
119 MIN_SCORE = 0
120 MAX_SCORE = 3 * N_QUESTIONS
121 REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16]
123 @staticmethod
124 def longname(req: "CamcopsRequest") -> str:
125 _ = req.gettext
126 return _('Center for Epidemiologic Studies Depression Scale')
128 # noinspection PyMethodParameters
129 @classproperty
130 def minimum_client_version(cls) -> Version:
131 return Version("2.2.8")
133 def is_complete(self) -> bool:
134 return (
135 self.all_fields_not_none(self.TASK_FIELDS) and
136 self.field_contents_valid()
137 )
139 def total_score(self) -> int:
140 # Need to store values as per original then flip here
141 total = 0
142 for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1):
143 score = getattr(self, fieldname)
144 if score is None:
145 continue
146 if qnum in self.REVERSE_SCORED_QUESTIONS:
147 total += 3 - score
148 else:
149 total += score
150 return total
152 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
153 line_step = 20
154 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5
155 # noinspection PyTypeChecker
156 return [TrackerInfo(
157 value=self.total_score(),
158 plot_label="CESD total score",
159 axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})",
160 axis_min=self.MIN_SCORE - 0.5,
161 axis_max=self.MAX_SCORE + 0.5,
162 axis_ticks=regular_tracker_axis_ticks_int(
163 self.MIN_SCORE,
164 self.MAX_SCORE,
165 step=line_step
166 ),
167 horizontal_lines=equally_spaced_int(
168 self.MIN_SCORE + line_step,
169 self.MAX_SCORE - line_step,
170 step=line_step
171 ) + [threshold_line],
172 horizontal_labels=[
173 TrackerLabel(threshold_line,
174 self.wxstring(req, "depression_or_risk_of")),
175 ]
176 )]
178 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
179 if not self.is_complete():
180 return CTV_INCOMPLETE
181 return [CtvInfo(
182 content=f"CESD total score {self.total_score()}"
183 )]
185 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
186 return self.standard_task_summary_fields() + [
187 SummaryElement(
188 name="depression_risk",
189 coltype=Boolean(),
190 value=self.has_depression_risk(),
191 comment="Has depression or at risk of depression"),
192 ]
194 def has_depression_risk(self) -> bool:
195 return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD
197 def get_task_html(self, req: CamcopsRequest) -> str:
198 score = self.total_score()
199 answer_dict = {None: None}
200 for option in range(self.N_ANSWERS):
201 answer_dict[option] = str(option) + " – " + \
202 self.wxstring(req, "a" + str(option))
203 q_a = ""
204 for q in range(1, self.N_QUESTIONS):
205 q_a += tr_qa(
206 self.wxstring(req, "q" + str(q) + "_s"),
207 get_from_dict(answer_dict, getattr(self, "q" + str(q)))
208 )
210 tr_total_score = tr_qa(
211 f"{req.sstring(SS.TOTAL_SCORE)} (0–60)",
212 score
213 ),
214 tr_depression_or_risk_of = tr_qa(
215 self.wxstring(req, "depression_or_risk_of") +
216 "? <sup>[1]</sup>",
217 get_yes_no(req, self.has_depression_risk())
218 ),
219 return f"""
220 <div class="{CssClass.SUMMARY}">
221 <table class="{CssClass.SUMMARY}">
222 {self.get_is_complete_tr(req)}
223 {tr_total_score}
224 {tr_depression_or_risk_of}
225 </table>
226 </div>
227 <table class="{CssClass.TASKDETAIL}">
228 <tr>
229 <th width="70%">Question</th>
230 <th width="30%">Answer</th>
231 </tr>
232 {q_a}
233 </table>
234 <div class="{CssClass.FOOTNOTES}">
235 [1] Presence of depression (or depression risk) is indicated by a
236 score ≥ 16
237 </div>
238 """