Coverage for tasks/bprse.py: 64%

47 statements  

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

1""" 

2camcops_server/tasks/bprse.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_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 

47 

48 

49# ============================================================================= 

50# BPRS-E 

51# ============================================================================= 

52 

53 

54class Bprse( # type: ignore[misc] 

55 TaskHasPatientMixin, 

56 TaskHasClinicianMixin, 

57 Task, 

58): 

59 """ 

60 Server implementation of the BPRS-E task. 

61 """ 

62 

63 __tablename__ = "bprse" 

64 shortname = "BPRS-E" 

65 provides_trackers = True 

66 

67 NQUESTIONS = 24 

68 

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 ) 

106 

107 TASK_FIELDS = strseq("q", 1, NQUESTIONS) 

108 MAX_SCORE = 168 

109 

110 @staticmethod 

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

112 _ = req.gettext 

113 return _("Brief Psychiatric Rating Scale, Expanded") 

114 

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 ] 

125 

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 ] 

135 

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 ] 

145 

146 def is_complete(self) -> bool: 

147 return ( 

148 self.all_fields_not_none(self.TASK_FIELDS) 

149 and self.field_contents_valid() 

150 ) 

151 

152 def total_score(self) -> int: 

153 return cast(int, self.sum_fields(self.TASK_FIELDS)) 

154 

155 def get_task_html(self, req: CamcopsRequest) -> str: 

156 def bprs_string(x: str) -> str: 

157 return req.wxstring("bprs", x) 

158 

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 } 

170 

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 ) 

177 

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