Coverage for tasks/bprse.py: 66%

50 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/bprse.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Integer 

35 

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_summaryelement import SummaryElement 

42from camcops_server.cc_modules.cc_task import ( 

43 get_from_dict, 

44 Task, 

45 TaskHasClinicianMixin, 

46 TaskHasPatientMixin, 

47) 

48from camcops_server.cc_modules.cc_text import SS 

49from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

50 

51 

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

53# BPRS-E 

54# ============================================================================= 

55 

56 

57class BprseMetaclass(DeclarativeMeta): 

58 # noinspection PyInitNewSignature 

59 def __init__( 

60 cls: Type["Bprse"], 

61 name: str, 

62 bases: Tuple[Type, ...], 

63 classdict: Dict[str, Any], 

64 ) -> None: 

65 add_multiple_columns( 

66 cls, 

67 "q", 

68 1, 

69 cls.NQUESTIONS, 

70 minimum=0, 

71 maximum=7, 

72 comment_fmt="Q{n}, {s} (1-7, higher worse, or 0 for not assessed)", 

73 comment_strings=[ 

74 "somatic concern", 

75 "anxiety", 

76 "depression", 

77 "suicidality", 

78 "guilt", 

79 "hostility", 

80 "elevated mood", 

81 "grandiosity", 

82 "suspiciousness", 

83 "hallucinations", 

84 "unusual thought content", 

85 "bizarre behaviour", 

86 "self-neglect", 

87 "disorientation", 

88 "conceptual disorganisation", 

89 "blunted affect", 

90 "emotional withdrawal", 

91 "motor retardation", 

92 "tension", 

93 "uncooperativeness", 

94 "excitement", 

95 "distractibility", 

96 "motor hyperactivity", 

97 "mannerisms and posturing", 

98 ], 

99 ) 

100 super().__init__(name, bases, classdict) 

101 

102 

103class Bprse( 

104 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=BprseMetaclass 

105): 

106 """ 

107 Server implementation of the BPRS-E task. 

108 """ 

109 

110 __tablename__ = "bprse" 

111 shortname = "BPRS-E" 

112 provides_trackers = True 

113 

114 NQUESTIONS = 24 

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

116 MAX_SCORE = 168 

117 

118 @staticmethod 

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

120 _ = req.gettext 

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

122 

123 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

124 return [ 

125 TrackerInfo( 

126 value=self.total_score(), 

127 plot_label="BPRS-E total score", 

128 axis_label=f"Total score (out of {self.MAX_SCORE})", 

129 axis_min=-0.5, 

130 axis_max=self.MAX_SCORE + 0.5, 

131 ) 

132 ] 

133 

134 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

135 if not self.is_complete(): 

136 return CTV_INCOMPLETE 

137 return [ 

138 CtvInfo( 

139 content=f"BPRS-E total score " 

140 f"{self.total_score()}/{self.MAX_SCORE}" 

141 ) 

142 ] 

143 

144 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

145 return self.standard_task_summary_fields() + [ 

146 SummaryElement( 

147 name="total", 

148 coltype=Integer(), 

149 value=self.total_score(), 

150 comment=f"Total score (/{self.MAX_SCORE})", 

151 ) 

152 ] 

153 

154 def is_complete(self) -> bool: 

155 return ( 

156 self.all_fields_not_none(self.TASK_FIELDS) 

157 and self.field_contents_valid() 

158 ) 

159 

160 def total_score(self) -> int: 

161 return self.sum_fields(self.TASK_FIELDS) 

162 

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

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

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

166 

167 main_dict = { 

168 None: None, 

169 0: "0 — " + bprs_string("old_option0"), 

170 1: "1 — " + bprs_string("old_option1"), 

171 2: "2 — " + bprs_string("old_option2"), 

172 3: "3 — " + bprs_string("old_option3"), 

173 4: "4 — " + bprs_string("old_option4"), 

174 5: "5 — " + bprs_string("old_option5"), 

175 6: "6 — " + bprs_string("old_option6"), 

176 7: "7 — " + bprs_string("old_option7"), 

177 } 

178 

179 q_a = "" 

180 for i in range(1, self.NQUESTIONS + 1): 

181 q_a += tr_qa( 

182 self.wxstring(req, "q" + str(i) + "_s"), 

183 get_from_dict(main_dict, getattr(self, "q" + str(i))), 

184 ) 

185 

186 total_score = tr( 

187 req.sstring(SS.TOTAL_SCORE) 

188 + f" (0–{self.MAX_SCORE}; 24–{self.MAX_SCORE} if all rated)", 

189 answer(self.total_score()), 

190 ) 

191 return f""" 

192 <div class="{CssClass.SUMMARY}"> 

193 <table class="{CssClass.SUMMARY}"> 

194 {self.get_is_complete_tr(req)} 

195 {total_score} 

196 </table> 

197 </div> 

198 <div class="{CssClass.EXPLANATION}"> 

199 Each question has specific answer definitions (see e.g. tablet 

200 app). 

201 </div> 

202 <table class="{CssClass.TASKDETAIL}"> 

203 <tr> 

204 <th width="60%">Question</th> 

205 <th width="40%">Answer <sup>[1]</sup></th> 

206 </tr> 

207 {q_a} 

208 </table> 

209 <div class="{CssClass.FOOTNOTES}"> 

210 [1] All answers are in the range 1–7, or 0 (not assessed, for 

211 some). 

212 </div> 

213 """