Coverage for tasks/cgi_task.py: 58%

76 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

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

28from typing import cast, Dict, List, Optional 

29 

30from sqlalchemy.orm import Mapped 

31from sqlalchemy.sql.sqltypes import Integer 

32 

33from camcops_server.cc_modules.cc_constants import CssClass 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

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

36from camcops_server.cc_modules.cc_request import CamcopsRequest 

37from camcops_server.cc_modules.cc_sqla_coltypes import ( 

38 mapped_camcops_column, 

39 PermittedValueChecker, 

40) 

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_trackerhelpers import TrackerInfo 

49 

50 

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

52# CGI 

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

54 

55 

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

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