Coverage for tasks/cgi_task.py: 58%

1"""

2 camcops_server/tasks/cgi_task.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 cast, Dict, List, Optional

29

30 from sqlalchemy.orm import Mapped

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 CTV_INCOMPLETE, CtvInfo

35 from camcops_server.cc_modules.cc_html import answer, italic, tr, tr_qa

36 from camcops_server.cc_modules.cc_request import CamcopsRequest

37 from camcops_server.cc_modules.cc_sqla_coltypes import (

38 mapped_camcops_column,

39 PermittedValueChecker,

40)

41 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

42 from camcops_server.cc_modules.cc_task import (

43 get_from_dict,

44 Task,

45 TaskHasClinicianMixin,

46 TaskHasPatientMixin,

47)

48 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

49

50

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

52# CGI

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

54

55

56 class Cgi(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501

57 """

58 Server implementation of the CGI task.

59 """

60

61 __tablename__ = "cgi"

62 shortname = "CGI"

63 provides_trackers = True

64

65 q1: Mapped[Optional[int]] = mapped_camcops_column(

66 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),

67 comment="Q1. Severity (1-7, higher worse, 0 not assessed)",

68 )

69 q2: Mapped[Optional[int]] = mapped_camcops_column(

70 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),

71 comment="Q2. Global improvement (1-7, higher worse, 0 not assessed)",

72 )

73 q3t: Mapped[Optional[int]] = mapped_camcops_column(

74 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4),

75 comment="Q3T. Therapeutic effects (1-4, higher worse, 0 not assessed)",

76 )

77 q3s: Mapped[Optional[int]] = mapped_camcops_column(

78 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4),

79 comment="Q3S. Side effects (1-4, higher worse, 0 not assessed)",

80 )

81 q3: Mapped[Optional[int]] = mapped_camcops_column(

82 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=16),

83 comment="Q3 (calculated). Efficacy index [(Q3T - 1) * 4 + Q3S].",

84 )

85

86 TASK_FIELDS = ["q1", "q2", "q3t", "q3s", "q3"]

87 MAX_SCORE = 30

88

89 @staticmethod

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

91 _ = req.gettext

92 return _("Clinical Global Impressions")

93

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

95 return [

96 TrackerInfo(

97 value=self.total_score(),

98 plot_label="CGI total score",

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

100 axis_min=-0.5,

101 axis_max=self.MAX_SCORE + 0.5,

102 )

103 ]

104

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

106 if not self.is_complete():

107 return CTV_INCOMPLETE

108 return [

109 CtvInfo(

110 content=(

111 f"CGI total score {self.total_score()}/{self.MAX_SCORE}"

112 )

113 )

114 ]

115

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

117 return self.standard_task_summary_fields() + [

118 SummaryElement(

119 name="total", coltype=Integer(), value=self.total_score()

120 )

121 ]

122

123 def is_complete(self) -> bool:

124 if not (

125 self.all_fields_not_none(self.TASK_FIELDS)

126 and self.field_contents_valid()

127 ):

128 return False

129 # Requirement for everything to be non-zero removed in v2.0.0

130 # if self.q1 == 0 or self.q2 == 0 or self.q3t == 0 or self.q3s == 0:

131 # return False

132 return True

133

134 def total_score(self) -> int:

135 return cast(int, self.sum_fields(["q1", "q2", "q3"]))

136

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

138 q1_dict = {

139 None: None,

140 0: self.wxstring(req, "q1_option0"),

141 1: self.wxstring(req, "q1_option1"),

142 2: self.wxstring(req, "q1_option2"),

143 3: self.wxstring(req, "q1_option3"),

144 4: self.wxstring(req, "q1_option4"),

145 5: self.wxstring(req, "q1_option5"),

146 6: self.wxstring(req, "q1_option6"),

147 7: self.wxstring(req, "q1_option7"),

148 }

149 q2_dict = {

150 None: None,

151 0: self.wxstring(req, "q2_option0"),

152 1: self.wxstring(req, "q2_option1"),

153 2: self.wxstring(req, "q2_option2"),

154 3: self.wxstring(req, "q2_option3"),

155 4: self.wxstring(req, "q2_option4"),

156 5: self.wxstring(req, "q2_option5"),

157 6: self.wxstring(req, "q2_option6"),

158 7: self.wxstring(req, "q2_option7"),

159 }

160 q3t_dict = {

161 None: None,

162 0: self.wxstring(req, "q3t_option0"),

163 1: self.wxstring(req, "q3t_option1"),

164 2: self.wxstring(req, "q3t_option2"),

165 3: self.wxstring(req, "q3t_option3"),

166 4: self.wxstring(req, "q3t_option4"),

167 }

168 q3s_dict = {

169 None: None,

170 0: self.wxstring(req, "q3s_option0"),

171 1: self.wxstring(req, "q3s_option1"),

172 2: self.wxstring(req, "q3s_option2"),

173 3: self.wxstring(req, "q3s_option3"),

174 4: self.wxstring(req, "q3s_option4"),

175 }

176

177 tr_total_score = tr(

178 "Total score <sup>[1]</sup>", answer(self.total_score())

179 )

180 tr_q1 = tr_qa(

181 self.wxstring(req, "q1_s") + " <sup>[2]</sup>",

182 get_from_dict(q1_dict, self.q1),

183 )

184 tr_q2 = tr_qa(

185 self.wxstring(req, "q2_s") + " <sup>[2]</sup>",

186 get_from_dict(q2_dict, self.q2),

187 )

188 tr_q3t = tr_qa(

189 self.wxstring(req, "q3t_s") + " <sup>[3]</sup>",

190 get_from_dict(q3t_dict, self.q3t),

191 )

192 tr_q3s = tr_qa(

193 self.wxstring(req, "q3s_s") + " <sup>[3]</sup>",

194 get_from_dict(q3s_dict, self.q3s),

195 )

196 tr_q3 = tr(

197 f"""

198 {self.wxstring(req, "q3_s")} <sup>[4]</sup>

199 <div class="{CssClass.SMALLPRINT}">

200 [(Q3T – 1) × 4 + Q3S]

201 </div>

202 """,

203 answer(self.q3, formatter_answer=italic),

204 )

205 return f"""

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

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

208 {self.get_is_complete_tr(req)}

209 {tr_total_score}

210 </table>

211 </div>

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

213 <tr>

214 <th width="30%">Question</th>

215 <th width="70%">Answer</th>

216 </tr>

217 {tr_q1}

218 {tr_q2}

219 {tr_q3t}

220 {tr_q3s}

221 {tr_q3}

222 </table>

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

224 [1] Total score: Q1 + Q2 + Q3. Range 3–30 when complete.

225 [2] Questions 1 and 2 are scored 1–7 (0 for not assessed).

226 [3] Questions 3T and 3S are scored 1–4 (0 for not assessed).

227 [4] Q3 is scored 1–16 if Q3T/Q3S complete.

228 </div>

229 """

230

231

232# =============================================================================

233# CGI-I

234# =============================================================================

235

236

237 class CgiI(TaskHasPatientMixin, TaskHasClinicianMixin, Task): # type: ignore[misc] # noqa: E501

238 __tablename__ = "cgi_i"

239 shortname = "CGI-I"

240 extrastring_taskname = "cgi" # shares with CGI

241 info_filename_stem = "cgi"

242

243 q: Mapped[Optional[int]] = mapped_camcops_column(

244 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),

245 comment="Global improvement (1-7, higher worse)",

246 )

247

248 TASK_FIELDS = ["q"]

249

250 @staticmethod

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

252 _ = req.gettext

253 return _("Clinical Global Impressions – Improvement")

254

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

256 if not self.is_complete():

257 return CTV_INCOMPLETE

258 return [

259 CtvInfo(

260 content="CGI-I rating: {}".format(self.get_rating_text(req))

261 )

262 ]

263

264 def is_complete(self) -> bool:

265 return (

266 self.all_fields_not_none(self.TASK_FIELDS)

267 and self.field_contents_valid()

268 )

269

270 def get_rating_text(self, req: CamcopsRequest) -> str:

271 qdict = self.get_q_dict(req)

272 return get_from_dict(qdict, self.q)

273

274 def get_q_dict(self, req: CamcopsRequest) -> Dict:

275 return {

276 None: None,

277 0: self.wxstring(req, "q2_option0"),

278 1: self.wxstring(req, "q2_option1"),

279 2: self.wxstring(req, "q2_option2"),

280 3: self.wxstring(req, "q2_option3"),

281 4: self.wxstring(req, "q2_option4"),

282 5: self.wxstring(req, "q2_option5"),

283 6: self.wxstring(req, "q2_option6"),

284 7: self.wxstring(req, "q2_option7"),

285 }

286

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

288 return f"""

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

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

291 {self.get_is_complete_tr(req)}

292 </table>

293 </div>

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

295 <tr>

296 <th width="50%">Question</th>

297 <th width="50%">Answer</th>

298 </tr>

299 {tr_qa(self.wxstring(req, "i_q"), self.get_rating_text(req))}

300 </table>

301 """