Coverage for tasks/bprs.py: 61%

1"""

2 camcops_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

28 from typing import Any, cast, List, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Integer

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE

35 from camcops_server.cc_modules.cc_db import add_multiple_columns

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

37 from camcops_server.cc_modules.cc_request import CamcopsRequest

38 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

39 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

40 from camcops_server.cc_modules.cc_task import (

41 get_from_dict,

42 Task,

43 TaskHasClinicianMixin,

44 TaskHasPatientMixin,

45)

46 from camcops_server.cc_modules.cc_text import SS

47 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

48

49

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

51# BPRS

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

53

54

55 class 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