Coverage for tasks/wemwbs.py: 59%

98 statements  

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

1""" 

2camcops_server/tasks/wemwbs.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 Any, cast, List, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

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_db import add_multiple_columns 

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

37from camcops_server.cc_modules.cc_request import CamcopsRequest 

38from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

39from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import ( 

41 get_from_dict, 

42 Task, 

43 TaskHasPatientMixin, 

44) 

45from camcops_server.cc_modules.cc_text import SS 

46from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

47 

48 

49# ============================================================================= 

50# WEMWBS 

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

52 

53 

54class Wemwbs( # type: ignore[misc] 

55 TaskHasPatientMixin, 

56 Task, 

57): 

58 """ 

59 Server implementation of the WEMWBS task. 

60 """ 

61 

62 __tablename__ = "wemwbs" 

63 shortname = "WEMWBS" 

64 provides_trackers = True 

65 

66 MINQSCORE = 1 

67 MAXQSCORE = 5 

68 N_QUESTIONS = 14 

69 MINTOTALSCORE = N_QUESTIONS * MINQSCORE 

70 MAXTOTALSCORE = N_QUESTIONS * MAXQSCORE 

71 

72 @classmethod 

73 def extend_columns(cls: Type["Wemwbs"], **kwargs: Any) -> None: 

74 add_multiple_columns( 

75 cls, 

76 "q", 

77 1, 

78 cls.N_QUESTIONS, 

79 minimum=1, 

80 maximum=5, 

81 comment_fmt="Q{n} ({s}) (1 none of the time - 5 all of the time)", 

82 comment_strings=[ 

83 "optimistic", 

84 "useful", 

85 "relaxed", 

86 "interested in other people", 

87 "energy", 

88 "dealing with problems well", 

89 "thinking clearly", 

90 "feeling good about myself", 

91 "feeling close to others", 

92 "confident", 

93 "able to make up my own mind", 

94 "feeling loved", 

95 "interested in new things", 

96 "cheerful", 

97 ], 

98 ) 

99 

100 TASK_FIELDS = strseq("q", 1, N_QUESTIONS) 

101 

102 @staticmethod 

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

104 _ = req.gettext 

105 return _("Warwick–Edinburgh Mental Well-Being Scale") 

106 

107 def is_complete(self) -> bool: 

108 return ( 

109 self.all_fields_not_none(self.TASK_FIELDS) 

110 and self.field_contents_valid() 

111 ) 

112 

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

114 return [ 

115 TrackerInfo( 

116 value=self.total_score(), 

117 plot_label="WEMWBS total score (rating mental well-being)", 

118 axis_label=f"Total score ({self.MINTOTALSCORE}-{self.MAXTOTALSCORE})", # noqa 

119 axis_min=self.MINTOTALSCORE - 0.5, 

120 axis_max=self.MAXTOTALSCORE + 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"WEMWBS total score {self.total_score()} " 

130 f"(range {self.MINTOTALSCORE}–{self.MAXTOTALSCORE})" 

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

141 f"{self.MINTOTALSCORE}-{self.MAXTOTALSCORE})", 

142 ) 

143 ] 

144 

145 def total_score(self) -> int: 

146 return cast(int, self.sum_fields(self.TASK_FIELDS)) 

147 

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

149 main_dict = { 

150 None: None, 

151 1: "1 — " + self.wxstring(req, "wemwbs_a1"), 

152 2: "2 — " + self.wxstring(req, "wemwbs_a2"), 

153 3: "3 — " + self.wxstring(req, "wemwbs_a3"), 

154 4: "4 — " + self.wxstring(req, "wemwbs_a4"), 

155 5: "5 — " + self.wxstring(req, "wemwbs_a5"), 

156 } 

157 q_a = "" 

158 for i in range(1, self.N_QUESTIONS + 1): 

159 nstr = str(i) 

160 q_a += tr_qa( 

161 self.wxstring(req, "wemwbs_q" + nstr), 

162 get_from_dict(main_dict, getattr(self, "q" + nstr)), 

163 ) 

164 h = """ 

165 <div class="{css_summary}"> 

166 <table class="{css_summary}"> 

167 {tr_is_complete} 

168 {tr_total_score} 

169 </table> 

170 </div> 

171 <div class="{css_explanation}"> 

172 Ratings are over the last 2 weeks. 

173 </div> 

174 <table class="{css_taskdetail}"> 

175 <tr> 

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

177 <th width="40%">Answer</th> 

178 </tr> 

179 {q_a} 

180 </table> 

181 <div class="{css_copyright}"> 

182 WEMWBS: from Tennant et al. (2007), <i>Health and Quality of 

183 Life Outcomes</i> 5:63, 

184 <a href="http://www.hqlo.com/content/5/1/63"> 

185 http://www.hqlo.com/content/5/1/63</a>; 

186 © 2007 Tennant et al.; distributed under the terms of the 

187 Creative Commons Attribution License. 

188 </div> 

189 """.format( 

190 css_summary=CssClass.SUMMARY, 

191 tr_is_complete=self.get_is_complete_tr(req), 

192 tr_total_score=tr( 

193 req.sstring(SS.TOTAL_SCORE), 

194 answer(self.total_score()) 

195 + f" (range {self.MINTOTALSCORE}–{self.MAXTOTALSCORE})", 

196 ), 

197 css_explanation=CssClass.EXPLANATION, 

198 css_taskdetail=CssClass.TASKDETAIL, 

199 q_a=q_a, 

200 css_copyright=CssClass.COPYRIGHT, 

201 ) 

202 return h 

203 

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

205 codes = [ 

206 SnomedExpression( 

207 req.snomed(SnomedLookup.WEMWBS_PROCEDURE_ASSESSMENT) 

208 ) 

209 ] 

210 if self.is_complete(): 

211 codes.append( 

212 SnomedExpression( 

213 req.snomed(SnomedLookup.WEMWBS_SCALE), 

214 { 

215 req.snomed( 

216 SnomedLookup.WEMWBS_SCORE 

217 ): self.total_score() 

218 }, 

219 ) 

220 ) 

221 return codes 

222 

223 

224# ============================================================================= 

225# SWEMWBS 

226# ============================================================================= 

227 

228 

229class Swemwbs( # type: ignore[misc] 

230 TaskHasPatientMixin, 

231 Task, 

232): 

233 """ 

234 Server implementation of the SWEMWBS task. 

235 """ 

236 

237 __tablename__ = "swemwbs" 

238 shortname = "SWEMWBS" 

239 extrastring_taskname = "wemwbs" # shares 

240 info_filename_stem = extrastring_taskname 

241 

242 MINQSCORE = 1 

243 MAXQSCORE = 5 

244 N_QUESTIONS = 7 

245 MINTOTALSCORE = N_QUESTIONS * MINQSCORE 

246 MAXTOTALSCORE = N_QUESTIONS * MAXQSCORE 

247 

248 @classmethod 

249 def extend_columns(cls: Type["Swemwbs"], **kwargs: Any) -> None: 

250 add_multiple_columns( 

251 cls, 

252 "q", 

253 1, 

254 cls.N_QUESTIONS, 

255 minimum=1, 

256 maximum=5, 

257 comment_fmt="Q{n} ({s}) (1 none of the time - 5 all of the time)", 

258 comment_strings=[ 

259 "optimistic", 

260 "useful", 

261 "relaxed", 

262 "interested in other people", 

263 "energy", 

264 "dealing with problems well", 

265 "thinking clearly", 

266 "feeling good about myself", 

267 "feeling close to others", 

268 "confident", 

269 "able to make up my own mind", 

270 "feeling loved", 

271 "interested in new things", 

272 "cheerful", 

273 ], 

274 ) 

275 

276 TASK_FIELDS = strseq("q", 1, N_QUESTIONS) 

277 

278 @staticmethod 

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

280 _ = req.gettext 

281 return _("Short Warwick–Edinburgh Mental Well-Being Scale") 

282 

283 def is_complete(self) -> bool: 

284 return ( 

285 self.all_fields_not_none(self.TASK_FIELDS) 

286 and self.field_contents_valid() 

287 ) 

288 

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

290 return [ 

291 TrackerInfo( 

292 value=self.total_score(), 

293 plot_label="SWEMWBS total score (rating mental well-being)", 

294 axis_label=f"Total score ({self.MINTOTALSCORE}-{self.MAXTOTALSCORE})", # noqa 

295 axis_min=self.MINTOTALSCORE - 0.5, 

296 axis_max=self.MAXTOTALSCORE + 0.5, 

297 ) 

298 ] 

299 

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

301 if not self.is_complete(): 

302 return CTV_INCOMPLETE 

303 return [ 

304 CtvInfo( 

305 content=f"SWEMWBS total score {self.total_score()} " 

306 f"(range {self.MINTOTALSCORE}–{self.MAXTOTALSCORE})" 

307 ) 

308 ] 

309 

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

311 return self.standard_task_summary_fields() + [ 

312 SummaryElement( 

313 name="total", 

314 coltype=Integer(), 

315 value=self.total_score(), 

316 comment=f"Total score (range " 

317 f"{self.MINTOTALSCORE}-{self.MAXTOTALSCORE})", 

318 ) 

319 ] 

320 

321 def total_score(self) -> int: 

322 return cast(int, self.sum_fields(self.TASK_FIELDS)) 

323 

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

325 main_dict = { 

326 None: None, 

327 1: "1 — " + self.wxstring(req, "wemwbs_a1"), 

328 2: "2 — " + self.wxstring(req, "wemwbs_a2"), 

329 3: "3 — " + self.wxstring(req, "wemwbs_a3"), 

330 4: "4 — " + self.wxstring(req, "wemwbs_a4"), 

331 5: "5 — " + self.wxstring(req, "wemwbs_a5"), 

332 } 

333 q_a = "" 

334 for i in range(1, self.N_QUESTIONS + 1): 

335 nstr = str(i) 

336 q_a += tr_qa( 

337 self.wxstring(req, "swemwbs_q" + nstr), 

338 get_from_dict(main_dict, getattr(self, "q" + nstr)), 

339 ) 

340 

341 h = """ 

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

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

344 {tr_is_complete} 

345 {total_score} 

346 </table> 

347 </div> 

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

349 Ratings are over the last 2 weeks. 

350 </div> 

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

352 <tr> 

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

354 <th width="40%">Answer</th> 

355 </tr> 

356 {q_a} 

357 </table> 

358 <div class="{CssClass.COPYRIGHT}"> 

359 SWEMWBS: from Stewart-Brown et al. (2009), <i>Health and 

360 Quality of Life Outcomes</i> 7:15, 

361 http://www.hqlo.com/content/7/1/15; 

362 © 2009 Stewart-Brown et al.; distributed under the terms of the 

363 Creative Commons Attribution License. 

364 </div> 

365 """.format( 

366 CssClass=CssClass, 

367 tr_is_complete=self.get_is_complete_tr(req), 

368 total_score=tr( 

369 req.sstring(SS.TOTAL_SCORE), 

370 answer(self.total_score()) 

371 + f" (range {self.MINTOTALSCORE}–{self.MAXTOTALSCORE})", 

372 ), 

373 q_a=q_a, 

374 ) 

375 return h 

376 

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

378 codes = [ 

379 SnomedExpression( 

380 req.snomed(SnomedLookup.SWEMWBS_PROCEDURE_ASSESSMENT) 

381 ) 

382 ] 

383 if self.is_complete(): 

384 codes.append( 

385 SnomedExpression( 

386 req.snomed(SnomedLookup.SWEMWBS_SCALE), 

387 { 

388 req.snomed( 

389 SnomedLookup.SWEMWBS_SCORE 

390 ): self.total_score() 

391 }, 

392 ) 

393 ) 

394 return codes