Coverage for tasks/gds.py: 57%
61 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/gds.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 Any, List, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.sql.sqltypes import Integer, String
33from camcops_server.cc_modules.cc_constants import CssClass, NO_CHAR, YES_CHAR
34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
35from camcops_server.cc_modules.cc_db import add_multiple_columns
36from camcops_server.cc_modules.cc_html import answer, tr, tr_qa
37from camcops_server.cc_modules.cc_request import CamcopsRequest
38from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
39from camcops_server.cc_modules.cc_summaryelement import SummaryElement
40from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
41from camcops_server.cc_modules.cc_text import SS
42from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
45# =============================================================================
46# GDS-15
47# =============================================================================
50class Gds15( # type: ignore[misc]
51 TaskHasPatientMixin,
52 Task,
53):
54 """
55 Server implementation of the GDS-15 task.
56 """
58 __tablename__ = "gds15"
59 shortname = "GDS-15"
60 info_filename_stem = "gds"
61 provides_trackers = True
63 NQUESTIONS = 15
65 @classmethod
66 def extend_columns(cls: Type["Gds15"], **kwargs: Any) -> None:
67 add_multiple_columns(
68 cls,
69 "q",
70 1,
71 cls.NQUESTIONS,
72 String(length=1),
73 pv=[NO_CHAR, YES_CHAR],
74 comment_fmt="Q{n}, {s} ('Y' or 'N')",
75 comment_strings=[
76 "satisfied",
77 "dropped activities",
78 "life empty",
79 "bored",
80 "good spirits", # 5
81 "afraid",
82 "happy",
83 "helpless",
84 "stay at home",
85 "memory problems", # 10
86 "wonderful to be alive",
87 "worthless",
88 "full of energy",
89 "hopeless",
90 "others better off", # 15
91 ],
92 )
94 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
95 SCORE_IF_YES = [2, 3, 4, 6, 8, 9, 10, 12, 14, 15]
96 SCORE_IF_NO = [1, 5, 7, 11, 13]
97 MAX_SCORE = 15
99 @staticmethod
100 def longname(req: "CamcopsRequest") -> str:
101 _ = req.gettext
102 return _("Geriatric Depression Scale, 15-item version")
104 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
105 return [
106 TrackerInfo(
107 value=self.total_score(),
108 plot_label="GDS-15 total score",
109 axis_label=f"Total score (out of {self.MAX_SCORE})",
110 axis_min=-0.5,
111 axis_max=self.MAX_SCORE + 0.5,
112 )
113 ]
115 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
116 if not self.is_complete():
117 return CTV_INCOMPLETE
118 return [
119 CtvInfo(
120 content=f"GDS-15 total score "
121 f"{self.total_score()}/{self.MAX_SCORE}"
122 )
123 ]
125 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
126 return self.standard_task_summary_fields() + [
127 SummaryElement(
128 name="total",
129 coltype=Integer(),
130 value=self.total_score(),
131 comment=f"Total score (/{self.MAX_SCORE})",
132 )
133 ]
135 def is_complete(self) -> bool:
136 return (
137 self.all_fields_not_none(self.TASK_FIELDS)
138 and self.field_contents_valid()
139 )
141 def total_score(self) -> int:
142 score = 0
143 for q in self.SCORE_IF_YES:
144 if getattr(self, "q" + str(q)) == YES_CHAR:
145 score += 1
146 for q in self.SCORE_IF_NO:
147 if getattr(self, "q" + str(q)) == NO_CHAR:
148 score += 1
149 return score
151 def get_task_html(self, req: CamcopsRequest) -> str:
152 score = self.total_score()
154 q_a = ""
155 for q in range(1, self.NQUESTIONS + 1):
156 suffix = " †" if q in self.SCORE_IF_YES else " *"
157 q_a += tr_qa(
158 str(q) + ". " + self.wxstring(req, "q" + str(q)) + suffix,
159 getattr(self, "q" + str(q)),
160 )
162 return f"""
163 <div class="{CssClass.SUMMARY}">
164 <table class="{CssClass.SUMMARY}">
165 {self.get_is_complete_tr(req)}
166 {tr(req.sstring(SS.TOTAL_SCORE),
167 answer(score) + f" / {self.MAX_SCORE}")}
168 </table>
169 </div>
170 <div class="{CssClass.EXPLANATION}">
171 Ratings are over the last 1 week.
172 </div>
173 <table class="{CssClass.TASKDETAIL}">
174 <tr>
175 <th width="70%">Question</th>
176 <th width="30%">Answer</th>
177 </tr>
178 {q_a}
179 </table>
180 <div class="{CssClass.FOOTNOTES}">
181 (†) ‘Y’ scores 1; ‘N’ scores 0.
182 (*) ‘Y’ scores 0; ‘N’ scores 1.
183 </div>
184 """
186 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
187 codes = [
188 SnomedExpression(
189 req.snomed(SnomedLookup.GDS15_PROCEDURE_ASSESSMENT)
190 )
191 ]
192 if self.is_complete():
193 codes.append(
194 SnomedExpression(
195 req.snomed(SnomedLookup.GDS15_SCALE),
196 {req.snomed(SnomedLookup.GDS15_SCORE): self.total_score()},
197 )
198 )
199 return codes