Coverage for tasks/wemwbs.py: 61%

102 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/wemwbs.py 

5 

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

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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

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

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

16 (at your option) any later version. 

17 

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

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

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

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

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

25 

26=============================================================================== 

27 

28""" 

29 

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

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import ( 

44 get_from_dict, 

45 Task, 

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# WEMWBS 

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

55 

56 

57class WemwbsMetaclass(DeclarativeMeta): 

58 # noinspection PyInitNewSignature 

59 def __init__( 

60 cls: Type["Wemwbs"], 

61 name: str, 

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

63 classdict: Dict[str, Any], 

64 ) -> None: 

65 add_multiple_columns( 

66 cls, 

67 "q", 

68 1, 

69 cls.N_QUESTIONS, 

70 minimum=1, 

71 maximum=5, 

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

73 comment_strings=[ 

74 "optimistic", 

75 "useful", 

76 "relaxed", 

77 "interested in other people", 

78 "energy", 

79 "dealing with problems well", 

80 "thinking clearly", 

81 "feeling good about myself", 

82 "feeling close to others", 

83 "confident", 

84 "able to make up my own mind", 

85 "feeling loved", 

86 "interested in new things", 

87 "cheerful", 

88 ], 

89 ) 

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

91 

92 

93class Wemwbs(TaskHasPatientMixin, Task, metaclass=WemwbsMetaclass): 

94 """ 

95 Server implementation of the WEMWBS task. 

96 """ 

97 

98 __tablename__ = "wemwbs" 

99 shortname = "WEMWBS" 

100 provides_trackers = True 

101 

102 MINQSCORE = 1 

103 MAXQSCORE = 5 

104 N_QUESTIONS = 14 

105 MINTOTALSCORE = N_QUESTIONS * MINQSCORE 

106 MAXTOTALSCORE = N_QUESTIONS * MAXQSCORE 

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

108 

109 @staticmethod 

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

111 _ = req.gettext 

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

113 

114 def is_complete(self) -> bool: 

115 return ( 

116 self.all_fields_not_none(self.TASK_FIELDS) 

117 and self.field_contents_valid() 

118 ) 

119 

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

121 return [ 

122 TrackerInfo( 

123 value=self.total_score(), 

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

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

126 axis_min=self.MINTOTALSCORE - 0.5, 

127 axis_max=self.MAXTOTALSCORE + 0.5, 

128 ) 

129 ] 

130 

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

132 if not self.is_complete(): 

133 return CTV_INCOMPLETE 

134 return [ 

135 CtvInfo( 

136 content=f"WEMWBS total score {self.total_score()} " 

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

138 ) 

139 ] 

140 

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

142 return self.standard_task_summary_fields() + [ 

143 SummaryElement( 

144 name="total", 

145 coltype=Integer(), 

146 value=self.total_score(), 

147 comment=f"Total score (range " 

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

149 ) 

150 ] 

151 

152 def total_score(self) -> int: 

153 return self.sum_fields(self.TASK_FIELDS) 

154 

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

156 main_dict = { 

157 None: None, 

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

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

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

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

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

163 } 

164 q_a = "" 

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

166 nstr = str(i) 

167 q_a += tr_qa( 

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

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

170 ) 

171 h = """ 

172 <div class="{css_summary}"> 

173 <table class="{css_summary}"> 

174 {tr_is_complete} 

175 {tr_total_score} 

176 </table> 

177 </div> 

178 <div class="{css_explanation}"> 

179 Ratings are over the last 2 weeks. 

180 </div> 

181 <table class="{css_taskdetail}"> 

182 <tr> 

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

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

185 </tr> 

186 {q_a} 

187 </table> 

188 <div class="{css_copyright}"> 

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

190 Life Outcomes</i> 5:63, 

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

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

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

194 Creative Commons Attribution License. 

195 </div> 

196 """.format( 

197 css_summary=CssClass.SUMMARY, 

198 tr_is_complete=self.get_is_complete_tr(req), 

199 tr_total_score=tr( 

200 req.sstring(SS.TOTAL_SCORE), 

201 answer(self.total_score()) 

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

203 ), 

204 css_explanation=CssClass.EXPLANATION, 

205 css_taskdetail=CssClass.TASKDETAIL, 

206 q_a=q_a, 

207 css_copyright=CssClass.COPYRIGHT, 

208 ) 

209 return h 

210 

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

212 codes = [ 

213 SnomedExpression( 

214 req.snomed(SnomedLookup.WEMWBS_PROCEDURE_ASSESSMENT) 

215 ) 

216 ] 

217 if self.is_complete(): 

218 codes.append( 

219 SnomedExpression( 

220 req.snomed(SnomedLookup.WEMWBS_SCALE), 

221 { 

222 req.snomed( 

223 SnomedLookup.WEMWBS_SCORE 

224 ): self.total_score() 

225 }, 

226 ) 

227 ) 

228 return codes 

229 

230 

231# ============================================================================= 

232# SWEMWBS 

233# ============================================================================= 

234 

235 

236class SwemwbsMetaclass(DeclarativeMeta): 

237 # noinspection PyInitNewSignature 

238 def __init__( 

239 cls: Type["Swemwbs"], 

240 name: str, 

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

242 classdict: Dict[str, Any], 

243 ) -> None: 

244 add_multiple_columns( 

245 cls, 

246 "q", 

247 1, 

248 cls.N_QUESTIONS, 

249 minimum=1, 

250 maximum=5, 

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

252 comment_strings=[ 

253 "optimistic", 

254 "useful", 

255 "relaxed", 

256 "interested in other people", 

257 "energy", 

258 "dealing with problems well", 

259 "thinking clearly", 

260 "feeling good about myself", 

261 "feeling close to others", 

262 "confident", 

263 "able to make up my own mind", 

264 "feeling loved", 

265 "interested in new things", 

266 "cheerful", 

267 ], 

268 ) 

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

270 

271 

272class Swemwbs(TaskHasPatientMixin, Task, metaclass=SwemwbsMetaclass): 

273 """ 

274 Server implementation of the SWEMWBS task. 

275 """ 

276 

277 __tablename__ = "swemwbs" 

278 shortname = "SWEMWBS" 

279 extrastring_taskname = "wemwbs" # shares 

280 info_filename_stem = extrastring_taskname 

281 

282 MINQSCORE = 1 

283 MAXQSCORE = 5 

284 N_QUESTIONS = 7 

285 MINTOTALSCORE = N_QUESTIONS * MINQSCORE 

286 MAXTOTALSCORE = N_QUESTIONS * MAXQSCORE 

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

288 

289 @staticmethod 

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

291 _ = req.gettext 

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

293 

294 def is_complete(self) -> bool: 

295 return ( 

296 self.all_fields_not_none(self.TASK_FIELDS) 

297 and self.field_contents_valid() 

298 ) 

299 

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

301 return [ 

302 TrackerInfo( 

303 value=self.total_score(), 

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

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

306 axis_min=self.MINTOTALSCORE - 0.5, 

307 axis_max=self.MAXTOTALSCORE + 0.5, 

308 ) 

309 ] 

310 

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

312 if not self.is_complete(): 

313 return CTV_INCOMPLETE 

314 return [ 

315 CtvInfo( 

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

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

318 ) 

319 ] 

320 

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

322 return self.standard_task_summary_fields() + [ 

323 SummaryElement( 

324 name="total", 

325 coltype=Integer(), 

326 value=self.total_score(), 

327 comment=f"Total score (range " 

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

329 ) 

330 ] 

331 

332 def total_score(self) -> int: 

333 return self.sum_fields(self.TASK_FIELDS) 

334 

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

336 main_dict = { 

337 None: None, 

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

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

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

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

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

343 } 

344 q_a = "" 

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

346 nstr = str(i) 

347 q_a += tr_qa( 

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

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

350 ) 

351 

352 h = """ 

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

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

355 {tr_is_complete} 

356 {total_score} 

357 </table> 

358 </div> 

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

360 Ratings are over the last 2 weeks. 

361 </div> 

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

363 <tr> 

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

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

366 </tr> 

367 {q_a} 

368 </table> 

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

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

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

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

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

374 Creative Commons Attribution License. 

375 </div> 

376 """.format( 

377 CssClass=CssClass, 

378 tr_is_complete=self.get_is_complete_tr(req), 

379 total_score=tr( 

380 req.sstring(SS.TOTAL_SCORE), 

381 answer(self.total_score()) 

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

383 ), 

384 q_a=q_a, 

385 ) 

386 return h 

387 

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

389 codes = [ 

390 SnomedExpression( 

391 req.snomed(SnomedLookup.SWEMWBS_PROCEDURE_ASSESSMENT) 

392 ) 

393 ] 

394 if self.is_complete(): 

395 codes.append( 

396 SnomedExpression( 

397 req.snomed(SnomedLookup.SWEMWBS_SCALE), 

398 { 

399 req.snomed( 

400 SnomedLookup.SWEMWBS_SCORE 

401 ): self.total_score() 

402 }, 

403 ) 

404 ) 

405 return codes