Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/bprs.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

12 CamCOPS is free software: you can redistribute it and/or modify 

13 it under the terms of the GNU General Public License as published by 

14 the Free Software Foundation, either version 3 of the License, or 

15 (at your option) any later version. 

16 

17 CamCOPS is distributed in the hope that it will be useful, 

18 but WITHOUT ANY WARRANTY; without even the implied warranty of 

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

22 You should have received a copy of the GNU General Public License 

23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

24 

25=============================================================================== 

26 

27""" 

28 

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

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import answer, tr, tr_qa 

39from camcops_server.cc_modules.cc_request import CamcopsRequest 

40from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

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 

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

55 

56class BprsMetaclass(DeclarativeMeta): 

57 # noinspection PyInitNewSignature 

58 def __init__(cls: Type['Bprs'], 

59 name: str, 

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

61 classdict: Dict[str, Any]) -> None: 

62 add_multiple_columns( 

63 cls, "q", 1, cls.NQUESTIONS, 

64 minimum=0, maximum=7, 

65 comment_fmt="Q{n}, {s} (1-7, higher worse, 0 for unable to rate)", 

66 comment_strings=[ 

67 "somatic concern", "anxiety", "emotional withdrawal", 

68 "conceptual disorganisation", "guilt", "tension", 

69 "mannerisms/posturing", "grandiosity", "depressive mood", 

70 "hostility", "suspiciousness", "hallucinatory behaviour", 

71 "motor retardation", "uncooperativeness", 

72 "unusual thought content", "blunted affect", "excitement", 

73 "disorientation", "severity of illness", "global improvement"] 

74 ) 

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

76 

77 

78class Bprs(TaskHasPatientMixin, TaskHasClinicianMixin, Task, 

79 metaclass=BprsMetaclass): 

80 """ 

81 Server implementation of the BPRS task. 

82 """ 

83 __tablename__ = "bprs" 

84 shortname = "BPRS" 

85 provides_trackers = True 

86 

87 NQUESTIONS = 20 

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

89 SCORED_FIELDS = [x for x in TASK_FIELDS if (x != "q19" and x != "q20")] 

90 MAX_SCORE = 126 

91 

92 @staticmethod 

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

94 _ = req.gettext 

95 return _("Brief Psychiatric Rating Scale") 

96 

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

98 return [TrackerInfo( 

99 value=self.total_score(), 

100 plot_label="BPRS total score", 

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

102 axis_min=-0.5, 

103 axis_max=self.MAX_SCORE + 0.5, 

104 )] 

105 

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

107 if not self.is_complete(): 

108 return CTV_INCOMPLETE 

109 return [CtvInfo( 

110 content=f"BPRS total score {self.total_score()}/{self.MAX_SCORE}" 

111 )] 

112 

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

114 return self.standard_task_summary_fields() + [ 

115 SummaryElement(name="total", coltype=Integer(), 

116 value=self.total_score(), 

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

118 ] 

119 

120 def is_complete(self) -> bool: 

121 return ( 

122 self.all_fields_not_none(Bprs.TASK_FIELDS) and 

123 self.field_contents_valid() 

124 ) 

125 

126 def total_score(self) -> int: 

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

128 # "0" means "not rated" 

129 

130 # noinspection PyUnresolvedReferences 

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

132 main_dict = { 

133 None: None, 

134 0: "0 — " + self.wxstring(req, "old_option0"), 

135 1: "1 — " + self.wxstring(req, "old_option1"), 

136 2: "2 — " + self.wxstring(req, "old_option2"), 

137 3: "3 — " + self.wxstring(req, "old_option3"), 

138 4: "4 — " + self.wxstring(req, "old_option4"), 

139 5: "5 — " + self.wxstring(req, "old_option5"), 

140 6: "6 — " + self.wxstring(req, "old_option6"), 

141 7: "7 — " + self.wxstring(req, "old_option7") 

142 } 

143 q19_dict = { 

144 None: None, 

145 1: self.wxstring(req, "q19_option1"), 

146 2: self.wxstring(req, "q19_option2"), 

147 3: self.wxstring(req, "q19_option3"), 

148 4: self.wxstring(req, "q19_option4"), 

149 5: self.wxstring(req, "q19_option5"), 

150 6: self.wxstring(req, "q19_option6"), 

151 7: self.wxstring(req, "q19_option7") 

152 } 

153 q20_dict = { 

154 None: None, 

155 0: self.wxstring(req, "q20_option0"), 

156 1: self.wxstring(req, "q20_option1"), 

157 2: self.wxstring(req, "q20_option2"), 

158 3: self.wxstring(req, "q20_option3"), 

159 4: self.wxstring(req, "q20_option4"), 

160 5: self.wxstring(req, "q20_option5"), 

161 6: self.wxstring(req, "q20_option6"), 

162 7: self.wxstring(req, "q20_option7") 

163 } 

164 

165 q_a = "" 

166 for i in range(1, Bprs.NQUESTIONS - 1): # only does 1-18 

167 q_a += tr_qa( 

168 self.wxstring(req, "q" + str(i) + "_title"), 

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

170 ) 

171 q_a += tr_qa(self.wxstring(req, "q19_title"), 

172 get_from_dict(q19_dict, self.q19)) 

173 q_a += tr_qa(self.wxstring(req, "q20_title"), 

174 get_from_dict(q20_dict, self.q20)) 

175 

176 total_score = tr( 

177 req.sstring(SS.TOTAL_SCORE) + 

178 f" (0–{self.MAX_SCORE}; 18–{self.MAX_SCORE} if all rated) " 

179 "<sup>[1]</sup>", 

180 answer(self.total_score()) 

181 ) 

182 return f""" 

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

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

185 {self.get_is_complete_tr(req)} 

186 {total_score} 

187 </table> 

188 </div> 

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

190 Ratings pertain to the past week, or behaviour during 

191 interview. Each question has specific answer definitions (see 

192 e.g. tablet app). 

193 </div> 

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

195 <tr> 

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

197 <th width="40%">Answer <sup>[2]</sup></th> 

198 </tr> 

199 {q_a} 

200 </table> 

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

202 [1] Only questions 1–18 are scored. 

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

204 some). 

205 </div> 

206 """ 

207 

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

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

210 return codes