Coverage for tasks/bprse.py: 64%
47 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/bprse.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_summaryelement import SummaryElement
39from camcops_server.cc_modules.cc_task import (
40 get_from_dict,
41 Task,
42 TaskHasClinicianMixin,
43 TaskHasPatientMixin,
44)
45from camcops_server.cc_modules.cc_text import SS
46from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
49# =============================================================================
50# BPRS-E
51# =============================================================================
54class Bprse( # type: ignore[misc]
55 TaskHasPatientMixin,
56 TaskHasClinicianMixin,
57 Task,
58):
59 """
60 Server implementation of the BPRS-E task.
61 """
63 __tablename__ = "bprse"
64 shortname = "BPRS-E"
65 provides_trackers = True
67 NQUESTIONS = 24
69 @classmethod
70 def extend_columns(cls: Type["Bprse"], **kwargs: Any) -> None:
71 add_multiple_columns(
72 cls,
73 "q",
74 1,
75 cls.NQUESTIONS,
76 minimum=0,
77 maximum=7,
78 comment_fmt="Q{n}, {s} (1-7, higher worse, or 0 for not assessed)",
79 comment_strings=[
80 "somatic concern",
81 "anxiety",
82 "depression",
83 "suicidality",
84 "guilt",
85 "hostility",
86 "elevated mood",
87 "grandiosity",
88 "suspiciousness",
89 "hallucinations",
90 "unusual thought content",
91 "bizarre behaviour",
92 "self-neglect",
93 "disorientation",
94 "conceptual disorganisation",
95 "blunted affect",
96 "emotional withdrawal",
97 "motor retardation",
98 "tension",
99 "uncooperativeness",
100 "excitement",
101 "distractibility",
102 "motor hyperactivity",
103 "mannerisms and posturing",
104 ],
105 )
107 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
108 MAX_SCORE = 168
110 @staticmethod
111 def longname(req: "CamcopsRequest") -> str:
112 _ = req.gettext
113 return _("Brief Psychiatric Rating Scale, Expanded")
115 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
116 return [
117 TrackerInfo(
118 value=self.total_score(),
119 plot_label="BPRS-E total score",
120 axis_label=f"Total score (out of {self.MAX_SCORE})",
121 axis_min=-0.5,
122 axis_max=self.MAX_SCORE + 0.5,
123 )
124 ]
126 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
127 if not self.is_complete():
128 return CTV_INCOMPLETE
129 return [
130 CtvInfo(
131 content=f"BPRS-E total score "
132 f"{self.total_score()}/{self.MAX_SCORE}"
133 )
134 ]
136 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
137 return self.standard_task_summary_fields() + [
138 SummaryElement(
139 name="total",
140 coltype=Integer(),
141 value=self.total_score(),
142 comment=f"Total score (/{self.MAX_SCORE})",
143 )
144 ]
146 def is_complete(self) -> bool:
147 return (
148 self.all_fields_not_none(self.TASK_FIELDS)
149 and self.field_contents_valid()
150 )
152 def total_score(self) -> int:
153 return cast(int, self.sum_fields(self.TASK_FIELDS))
155 def get_task_html(self, req: CamcopsRequest) -> str:
156 def bprs_string(x: str) -> str:
157 return req.wxstring("bprs", x)
159 main_dict = {
160 None: None,
161 0: "0 — " + bprs_string("old_option0"),
162 1: "1 — " + bprs_string("old_option1"),
163 2: "2 — " + bprs_string("old_option2"),
164 3: "3 — " + bprs_string("old_option3"),
165 4: "4 — " + bprs_string("old_option4"),
166 5: "5 — " + bprs_string("old_option5"),
167 6: "6 — " + bprs_string("old_option6"),
168 7: "7 — " + bprs_string("old_option7"),
169 }
171 q_a = ""
172 for i in range(1, self.NQUESTIONS + 1):
173 q_a += tr_qa(
174 self.wxstring(req, "q" + str(i) + "_s"),
175 get_from_dict(main_dict, getattr(self, "q" + str(i))),
176 )
178 total_score = tr(
179 req.sstring(SS.TOTAL_SCORE)
180 + f" (0–{self.MAX_SCORE}; 24–{self.MAX_SCORE} if all rated)",
181 answer(self.total_score()),
182 )
183 return f"""
184 <div class="{CssClass.SUMMARY}">
185 <table class="{CssClass.SUMMARY}">
186 {self.get_is_complete_tr(req)}
187 {total_score}
188 </table>
189 </div>
190 <div class="{CssClass.EXPLANATION}">
191 Each question has specific answer definitions (see e.g. tablet
192 app).
193 </div>
194 <table class="{CssClass.TASKDETAIL}">
195 <tr>
196 <th width="60%">Question</th>
197 <th width="40%">Answer <sup>[1]</sup></th>
198 </tr>
199 {q_a}
200 </table>
201 <div class="{CssClass.FOOTNOTES}">
202 [1] All answers are in the range 1–7, or 0 (not assessed, for
203 some).
204 </div>
205 """