Coverage for tasks/bprs.py: 63%
57 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/bprs.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28"""
30from typing import Any, Dict, List, Tuple, Type
32from cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy.ext.declarative import DeclarativeMeta
34from sqlalchemy.sql.sqltypes import Integer
36from camcops_server.cc_modules.cc_constants import CssClass
37from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
38from camcops_server.cc_modules.cc_db import add_multiple_columns
39from camcops_server.cc_modules.cc_html import answer, tr, tr_qa
40from camcops_server.cc_modules.cc_request import CamcopsRequest
41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
42from camcops_server.cc_modules.cc_summaryelement import SummaryElement
43from camcops_server.cc_modules.cc_task import (
44 get_from_dict,
45 Task,
46 TaskHasClinicianMixin,
47 TaskHasPatientMixin,
48)
49from camcops_server.cc_modules.cc_text import SS
50from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
53# =============================================================================
54# BPRS
55# =============================================================================
58class BprsMetaclass(DeclarativeMeta):
59 # noinspection PyInitNewSignature
60 def __init__(
61 cls: Type["Bprs"],
62 name: str,
63 bases: Tuple[Type, ...],
64 classdict: Dict[str, Any],
65 ) -> None:
66 add_multiple_columns(
67 cls,
68 "q",
69 1,
70 cls.NQUESTIONS,
71 minimum=0,
72 maximum=7,
73 comment_fmt="Q{n}, {s} (1-7, higher worse, 0 for unable to rate)",
74 comment_strings=[
75 "somatic concern",
76 "anxiety",
77 "emotional withdrawal",
78 "conceptual disorganisation",
79 "guilt",
80 "tension",
81 "mannerisms/posturing",
82 "grandiosity",
83 "depressive mood",
84 "hostility",
85 "suspiciousness",
86 "hallucinatory behaviour",
87 "motor retardation",
88 "uncooperativeness",
89 "unusual thought content",
90 "blunted affect",
91 "excitement",
92 "disorientation",
93 "severity of illness",
94 "global improvement",
95 ],
96 )
97 super().__init__(name, bases, classdict)
100class Bprs(
101 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=BprsMetaclass
102):
103 """
104 Server implementation of the BPRS task.
105 """
107 __tablename__ = "bprs"
108 shortname = "BPRS"
109 provides_trackers = True
111 NQUESTIONS = 20
112 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
113 SCORED_FIELDS = [x for x in TASK_FIELDS if (x != "q19" and x != "q20")]
114 MAX_SCORE = 126
116 @staticmethod
117 def longname(req: "CamcopsRequest") -> str:
118 _ = req.gettext
119 return _("Brief Psychiatric Rating Scale")
121 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
122 return [
123 TrackerInfo(
124 value=self.total_score(),
125 plot_label="BPRS total score",
126 axis_label=f"Total score (out of {self.MAX_SCORE})",
127 axis_min=-0.5,
128 axis_max=self.MAX_SCORE + 0.5,
129 )
130 ]
132 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
133 if not self.is_complete():
134 return CTV_INCOMPLETE
135 return [
136 CtvInfo(
137 content=f"BPRS total score "
138 f"{self.total_score()}/{self.MAX_SCORE}"
139 )
140 ]
142 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
143 return self.standard_task_summary_fields() + [
144 SummaryElement(
145 name="total",
146 coltype=Integer(),
147 value=self.total_score(),
148 comment=f"Total score (/{self.MAX_SCORE})",
149 )
150 ]
152 def is_complete(self) -> bool:
153 return (
154 self.all_fields_not_none(Bprs.TASK_FIELDS)
155 and self.field_contents_valid()
156 )
158 def total_score(self) -> int:
159 return self.sum_fields(Bprs.SCORED_FIELDS, ignorevalue=0)
160 # "0" means "not rated"
162 # noinspection PyUnresolvedReferences
163 def get_task_html(self, req: CamcopsRequest) -> str:
164 main_dict = {
165 None: None,
166 0: "0 — " + self.wxstring(req, "old_option0"),
167 1: "1 — " + self.wxstring(req, "old_option1"),
168 2: "2 — " + self.wxstring(req, "old_option2"),
169 3: "3 — " + self.wxstring(req, "old_option3"),
170 4: "4 — " + self.wxstring(req, "old_option4"),
171 5: "5 — " + self.wxstring(req, "old_option5"),
172 6: "6 — " + self.wxstring(req, "old_option6"),
173 7: "7 — " + self.wxstring(req, "old_option7"),
174 }
175 q19_dict = {
176 None: None,
177 1: self.wxstring(req, "q19_option1"),
178 2: self.wxstring(req, "q19_option2"),
179 3: self.wxstring(req, "q19_option3"),
180 4: self.wxstring(req, "q19_option4"),
181 5: self.wxstring(req, "q19_option5"),
182 6: self.wxstring(req, "q19_option6"),
183 7: self.wxstring(req, "q19_option7"),
184 }
185 q20_dict = {
186 None: None,
187 0: self.wxstring(req, "q20_option0"),
188 1: self.wxstring(req, "q20_option1"),
189 2: self.wxstring(req, "q20_option2"),
190 3: self.wxstring(req, "q20_option3"),
191 4: self.wxstring(req, "q20_option4"),
192 5: self.wxstring(req, "q20_option5"),
193 6: self.wxstring(req, "q20_option6"),
194 7: self.wxstring(req, "q20_option7"),
195 }
197 q_a = ""
198 for i in range(1, Bprs.NQUESTIONS - 1): # only does 1-18
199 q_a += tr_qa(
200 self.wxstring(req, "q" + str(i) + "_title"),
201 get_from_dict(main_dict, getattr(self, "q" + str(i))),
202 )
203 q_a += tr_qa(
204 self.wxstring(req, "q19_title"), get_from_dict(q19_dict, self.q19)
205 )
206 q_a += tr_qa(
207 self.wxstring(req, "q20_title"), get_from_dict(q20_dict, self.q20)
208 )
210 total_score = tr(
211 req.sstring(SS.TOTAL_SCORE)
212 + f" (0–{self.MAX_SCORE}; 18–{self.MAX_SCORE} if all rated) "
213 "<sup>[1]</sup>",
214 answer(self.total_score()),
215 )
216 return f"""
217 <div class="{CssClass.SUMMARY}">
218 <table class="{CssClass.SUMMARY}">
219 {self.get_is_complete_tr(req)}
220 {total_score}
221 </table>
222 </div>
223 <div class="{CssClass.EXPLANATION}">
224 Ratings pertain to the past week, or behaviour during
225 interview. Each question has specific answer definitions (see
226 e.g. tablet app).
227 </div>
228 <table class="{CssClass.TASKDETAIL}">
229 <tr>
230 <th width="60%">Question</th>
231 <th width="40%">Answer <sup>[2]</sup></th>
232 </tr>
233 {q_a}
234 </table>
235 <div class="{CssClass.FOOTNOTES}">
236 [1] Only questions 1–18 are scored.
237 [2] All answers are in the range 1–7, or 0 (not assessed, for
238 some).
239 </div>
240 """
242 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
243 codes = [SnomedExpression(req.snomed(SnomedLookup.BPRS1962_SCALE))]
244 return codes