Coverage for tasks/bprs.py: 63%

57 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/bprs.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_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 

51 

52 

53# ============================================================================= 

54# BPRS 

55# ============================================================================= 

56 

57 

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) 

98 

99 

100class Bprs( 

101 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=BprsMetaclass 

102): 

103 """ 

104 Server implementation of the BPRS task. 

105 """ 

106 

107 __tablename__ = "bprs" 

108 shortname = "BPRS" 

109 provides_trackers = True 

110 

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 

115 

116 @staticmethod 

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

118 _ = req.gettext 

119 return _("Brief Psychiatric Rating Scale") 

120 

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 ] 

131 

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 ] 

141 

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 ] 

151 

152 def is_complete(self) -> bool: 

153 return ( 

154 self.all_fields_not_none(Bprs.TASK_FIELDS) 

155 and self.field_contents_valid() 

156 ) 

157 

158 def total_score(self) -> int: 

159 return self.sum_fields(Bprs.SCORED_FIELDS, ignorevalue=0) 

160 # "0" means "not rated" 

161 

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 } 

196 

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 ) 

209 

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

241 

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

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

244 return codes