Coverage for tasks/bprs.py: 61%

54 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/bprs.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26""" 

27 

28from typing import Any, cast, List, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Integer 

32 

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 

48 

49 

50# ============================================================================= 

51# BPRS 

52# ============================================================================= 

53 

54 

55class Bprs( # type: ignore[misc] 

56 TaskHasPatientMixin, 

57 TaskHasClinicianMixin, 

58 Task, 

59): 

60 """ 

61 Server implementation of the BPRS task. 

62 """ 

63 

64 __tablename__ = "bprs" 

65 shortname = "BPRS" 

66 provides_trackers = True 

67 

68 NQUESTIONS = 20 

69 

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 ) 

103 

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 

107 

108 @staticmethod 

109 def longname(req: "CamcopsRequest") -> str: 

110 _ = req.gettext 

111 return _("Brief Psychiatric Rating Scale") 

112 

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 ] 

123 

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 ] 

133 

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 ] 

143 

144 def is_complete(self) -> bool: 

145 return ( 

146 self.all_fields_not_none(Bprs.TASK_FIELDS) 

147 and self.field_contents_valid() 

148 ) 

149 

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" 

155 

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 } 

190 

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 ) 

203 

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 """ 

235 

236 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

237 codes = [SnomedExpression(req.snomed(SnomedLookup.BPRS1962_SCALE))] 

238 return codes