Coverage for tasks/bprs.py: 61%
54 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/bprs.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, cast, List, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.sql.sqltypes import Integer
33from camcops_server.cc_modules.cc_constants import CssClass
34from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
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 (
41 get_from_dict,
42 Task,
43 TaskHasClinicianMixin,
44 TaskHasPatientMixin,
45)
46from camcops_server.cc_modules.cc_text import SS
47from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
50# =============================================================================
51# BPRS
52# =============================================================================
55class Bprs( # type: ignore[misc]
56 TaskHasPatientMixin,
57 TaskHasClinicianMixin,
58 Task,
59):
60 """
61 Server implementation of the BPRS task.
62 """
64 __tablename__ = "bprs"
65 shortname = "BPRS"
66 provides_trackers = True
68 NQUESTIONS = 20
70 @classmethod
71 def extend_columns(cls: Type["Bprs"], **kwargs: Any) -> None:
72 add_multiple_columns(
73 cls,
74 "q",
75 1,
76 cls.NQUESTIONS,
77 minimum=0,
78 maximum=7,
79 comment_fmt="Q{n}, {s} (1-7, higher worse, 0 for unable to rate)",
80 comment_strings=[
81 "somatic concern",
82 "anxiety",
83 "emotional withdrawal",
84 "conceptual disorganisation",
85 "guilt",
86 "tension",
87 "mannerisms/posturing",
88 "grandiosity",
89 "depressive mood",
90 "hostility",
91 "suspiciousness",
92 "hallucinatory behaviour",
93 "motor retardation",
94 "uncooperativeness",
95 "unusual thought content",
96 "blunted affect",
97 "excitement",
98 "disorientation",
99 "severity of illness",
100 "global improvement",
101 ],
102 )
104 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
105 SCORED_FIELDS = [x for x in TASK_FIELDS if (x != "q19" and x != "q20")]
106 MAX_SCORE = 126
108 @staticmethod
109 def longname(req: "CamcopsRequest") -> str:
110 _ = req.gettext
111 return _("Brief Psychiatric Rating Scale")
113 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
114 return [
115 TrackerInfo(
116 value=self.total_score(),
117 plot_label="BPRS total score",
118 axis_label=f"Total score (out of {self.MAX_SCORE})",
119 axis_min=-0.5,
120 axis_max=self.MAX_SCORE + 0.5,
121 )
122 ]
124 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
125 if not self.is_complete():
126 return CTV_INCOMPLETE
127 return [
128 CtvInfo(
129 content=f"BPRS total score "
130 f"{self.total_score()}/{self.MAX_SCORE}"
131 )
132 ]
134 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
135 return self.standard_task_summary_fields() + [
136 SummaryElement(
137 name="total",
138 coltype=Integer(),
139 value=self.total_score(),
140 comment=f"Total score (/{self.MAX_SCORE})",
141 )
142 ]
144 def is_complete(self) -> bool:
145 return (
146 self.all_fields_not_none(Bprs.TASK_FIELDS)
147 and self.field_contents_valid()
148 )
150 def total_score(self) -> int:
151 return cast(
152 int, self.sum_fields(Bprs.SCORED_FIELDS, ignorevalues=[0, None])
153 )
154 # "0" means "not rated"
156 # noinspection PyUnresolvedReferences
157 def get_task_html(self, req: CamcopsRequest) -> str:
158 main_dict = {
159 None: None,
160 0: "0 — " + self.wxstring(req, "old_option0"),
161 1: "1 — " + self.wxstring(req, "old_option1"),
162 2: "2 — " + self.wxstring(req, "old_option2"),
163 3: "3 — " + self.wxstring(req, "old_option3"),
164 4: "4 — " + self.wxstring(req, "old_option4"),
165 5: "5 — " + self.wxstring(req, "old_option5"),
166 6: "6 — " + self.wxstring(req, "old_option6"),
167 7: "7 — " + self.wxstring(req, "old_option7"),
168 }
169 q19_dict = {
170 None: None,
171 1: self.wxstring(req, "q19_option1"),
172 2: self.wxstring(req, "q19_option2"),
173 3: self.wxstring(req, "q19_option3"),
174 4: self.wxstring(req, "q19_option4"),
175 5: self.wxstring(req, "q19_option5"),
176 6: self.wxstring(req, "q19_option6"),
177 7: self.wxstring(req, "q19_option7"),
178 }
179 q20_dict = {
180 None: None,
181 0: self.wxstring(req, "q20_option0"),
182 1: self.wxstring(req, "q20_option1"),
183 2: self.wxstring(req, "q20_option2"),
184 3: self.wxstring(req, "q20_option3"),
185 4: self.wxstring(req, "q20_option4"),
186 5: self.wxstring(req, "q20_option5"),
187 6: self.wxstring(req, "q20_option6"),
188 7: self.wxstring(req, "q20_option7"),
189 }
191 q_a = ""
192 for i in range(1, Bprs.NQUESTIONS - 1): # only does 1-18
193 q_a += tr_qa(
194 self.wxstring(req, "q" + str(i) + "_title"),
195 get_from_dict(main_dict, getattr(self, "q" + str(i))),
196 )
197 q_a += tr_qa(
198 self.wxstring(req, "q19_title"), get_from_dict(q19_dict, self.q19) # type: ignore[attr-defined] # noqa: E501
199 )
200 q_a += tr_qa(
201 self.wxstring(req, "q20_title"), get_from_dict(q20_dict, self.q20) # type: ignore[attr-defined] # noqa: E501
202 )
204 total_score = tr(
205 req.sstring(SS.TOTAL_SCORE)
206 + f" (0–{self.MAX_SCORE}; 18–{self.MAX_SCORE} if all rated) "
207 "<sup>[1]</sup>",
208 answer(self.total_score()),
209 )
210 return f"""
211 <div class="{CssClass.SUMMARY}">
212 <table class="{CssClass.SUMMARY}">
213 {self.get_is_complete_tr(req)}
214 {total_score}
215 </table>
216 </div>
217 <div class="{CssClass.EXPLANATION}">
218 Ratings pertain to the past week, or behaviour during
219 interview. Each question has specific answer definitions (see
220 e.g. tablet app).
221 </div>
222 <table class="{CssClass.TASKDETAIL}">
223 <tr>
224 <th width="60%">Question</th>
225 <th width="40%">Answer <sup>[2]</sup></th>
226 </tr>
227 {q_a}
228 </table>
229 <div class="{CssClass.FOOTNOTES}">
230 [1] Only questions 1–18 are scored.
231 [2] All answers are in the range 1–7, or 0 (not assessed, for
232 some).
233 </div>
234 """
236 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
237 codes = [SnomedExpression(req.snomed(SnomedLookup.BPRS1962_SCALE))]
238 return codes