Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2# camcops_server/tasks/factg.py 

3 

4""" 

5=============================================================================== 

6 

7 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

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- By Joe Kearney, Rudolf Cardinal. 

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 Boolean, Float 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import ( 

39 answer, 

40 tr_qa, 

41 subheading_spanning_two_columns, 

42 tr 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_sqla_coltypes import ( 

46 BIT_CHECKER, 

47 CamcopsColumn, 

48) 

49from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

50from camcops_server.cc_modules.cc_task import ( 

51 get_from_dict, 

52 Task, 

53 TaskHasPatientMixin, 

54) 

55from camcops_server.cc_modules.cc_text import SS 

56from camcops_server.cc_modules.cc_trackerhelpers import ( 

57 TrackerAxisTick, 

58 TrackerInfo, 

59) 

60 

61 

62# ============================================================================= 

63# Fact-G 

64# ============================================================================= 

65 

66DISPLAY_DP = 2 

67MAX_QSCORE = 4 

68NON_REVERSE_SCORED_EMOTIONAL_QNUM = 2 

69 

70 

71class FactgMetaclass(DeclarativeMeta): 

72 # noinspection PyInitNewSignature 

73 def __init__(cls: Type['Factg'], 

74 name: str, 

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

76 classdict: Dict[str, Any]) -> None: 

77 answer_stem = ( 

78 " (0 not at all, 1 a little bit, 2 somewhat, 3 quite a bit, " 

79 "4 very much)" 

80 ) 

81 add_multiple_columns( 

82 cls, "p_q", 1, cls.N_QUESTIONS_PHYSICAL, 

83 minimum=0, maximum=4, 

84 comment_fmt="Physical well-being Q{n} ({s})" + answer_stem, 

85 comment_strings=[ 

86 "lack of energy", 

87 "nausea", 

88 "trouble meeting family needs", 

89 "pain", 

90 "treatment side effects", 

91 "feel ill", 

92 "bedbound", 

93 ], 

94 ) 

95 add_multiple_columns( 

96 cls, "s_q", 1, cls.N_QUESTIONS_SOCIAL, 

97 minimum=0, maximum=4, 

98 comment_fmt="Social well-being Q{n} ({s})" + answer_stem, 

99 comment_strings=[ 

100 "close to friends", 

101 "emotional support from family", 

102 "support from friends", 

103 "family accepted illness", 

104 "good family comms re illness", 

105 "feel close to partner/main supporter", 

106 "satisfied with sex life", 

107 ], 

108 ) 

109 add_multiple_columns( 

110 cls, "e_q", 1, cls.N_QUESTIONS_EMOTIONAL, 

111 minimum=0, maximum=4, 

112 comment_fmt="Emotional well-being Q{n} ({s})" + answer_stem, 

113 comment_strings=[ 

114 "sad", 

115 "satisfied with coping re illness", 

116 "losing hope in fight against illness", 

117 "nervous" 

118 "worried about dying", 

119 "worried condition will worsen", 

120 ], 

121 ) 

122 add_multiple_columns( 

123 cls, "f_q", 1, cls.N_QUESTIONS_FUNCTIONAL, 

124 minimum=0, maximum=4, 

125 comment_fmt="Functional well-being Q{n} ({s})" + answer_stem, 

126 comment_strings=[ 

127 "able to work", 

128 "work fulfilling", 

129 "able to enjoy life", 

130 "accepted illness", 

131 "sleeping well", 

132 "enjoying usual fun things", 

133 "content with quality of life", 

134 ], 

135 ) 

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

137 

138 

139class FactgGroupInfo(object): 

140 """ 

141 Internal information class for the FACT-G. 

142 """ 

143 def __init__(self, 

144 heading_xstring_name: str, 

145 question_prefix: str, 

146 fieldnames: List[str], 

147 summary_fieldname: str, 

148 summary_description: str, 

149 max_score: int, 

150 reverse_score_all: bool = False, 

151 reverse_score_all_but_q2: bool = False) -> None: 

152 self.heading_xstring_name = heading_xstring_name 

153 self.question_prefix = question_prefix 

154 self.fieldnames = fieldnames 

155 self.summary_fieldname = summary_fieldname 

156 self.summary_description = summary_description 

157 self.max_score = max_score 

158 self.reverse_score_all = reverse_score_all 

159 self.reverse_score_all_but_q2 = reverse_score_all_but_q2 

160 self.n_questions = len(fieldnames) 

161 

162 def subscore(self, task: "Factg") -> float: 

163 answered = 0 

164 scoresum = 0 

165 for qnum, fieldname in enumerate(self.fieldnames, start=1): 

166 answer_val = getattr(task, fieldname) 

167 try: 

168 answer_int = int(answer_val) 

169 except (TypeError, ValueError): 

170 continue 

171 answered += 1 

172 if (self.reverse_score_all or 

173 (self.reverse_score_all_but_q2 and 

174 qnum != NON_REVERSE_SCORED_EMOTIONAL_QNUM)): 

175 # reverse-scored 

176 scoresum += MAX_QSCORE - answer_int 

177 else: 

178 # normally scored 

179 scoresum += answer_int 

180 if answered == 0: 

181 return 0 

182 return scoresum * self.n_questions / answered 

183 

184 

185class Factg(TaskHasPatientMixin, Task, 

186 metaclass=FactgMetaclass): 

187 """ 

188 Server implementation of the Fact-G task. 

189 """ 

190 __tablename__ = "factg" 

191 shortname = "FACT-G" 

192 provides_trackers = True 

193 

194 N_QUESTIONS_PHYSICAL = 7 

195 N_QUESTIONS_SOCIAL = 7 

196 N_QUESTIONS_EMOTIONAL = 6 

197 N_QUESTIONS_FUNCTIONAL = 7 

198 

199 MAX_SCORE_PHYSICAL = 28 

200 MAX_SCORE_SOCIAL = 28 

201 MAX_SCORE_EMOTIONAL = 24 

202 MAX_SCORE_FUNCTIONAL = 28 

203 

204 N_ALL = ( 

205 N_QUESTIONS_PHYSICAL + N_QUESTIONS_SOCIAL + 

206 N_QUESTIONS_EMOTIONAL + N_QUESTIONS_FUNCTIONAL 

207 ) 

208 

209 MAX_SCORE_TOTAL = N_ALL * MAX_QSCORE 

210 

211 PHYSICAL_PREFIX = "p_q" 

212 SOCIAL_PREFIX = "s_q" 

213 EMOTIONAL_PREFIX = "e_q" 

214 FUNCTIONAL_PREFIX = "f_q" 

215 

216 QUESTIONS_PHYSICAL = strseq(PHYSICAL_PREFIX, 1, N_QUESTIONS_PHYSICAL) 

217 QUESTIONS_SOCIAL = strseq(SOCIAL_PREFIX, 1, N_QUESTIONS_SOCIAL) 

218 QUESTIONS_EMOTIONAL = strseq(EMOTIONAL_PREFIX, 1, N_QUESTIONS_EMOTIONAL) 

219 QUESTIONS_FUNCTIONAL = strseq(FUNCTIONAL_PREFIX, 1, N_QUESTIONS_FUNCTIONAL) 

220 

221 GROUPS = [ 

222 FactgGroupInfo("h1", PHYSICAL_PREFIX, QUESTIONS_PHYSICAL, 

223 "physical_wellbeing", "Physical wellbeing subscore", 

224 MAX_SCORE_PHYSICAL, 

225 reverse_score_all=True), 

226 FactgGroupInfo("h2", SOCIAL_PREFIX, QUESTIONS_SOCIAL, 

227 "social_family_wellbeing", 

228 "Social/family wellbeing subscore", 

229 MAX_SCORE_SOCIAL), 

230 FactgGroupInfo("h3", EMOTIONAL_PREFIX, QUESTIONS_EMOTIONAL, 

231 "emotional_wellbeing", "Emotional wellbeing subscore", 

232 MAX_SCORE_EMOTIONAL, 

233 reverse_score_all_but_q2=True), 

234 FactgGroupInfo("h4", FUNCTIONAL_PREFIX, QUESTIONS_FUNCTIONAL, 

235 "functional_wellbeing", "Functional wellbeing subscore", 

236 MAX_SCORE_FUNCTIONAL), 

237 ] 

238 

239 OPTIONAL_Q = "s_q7" 

240 

241 ignore_s_q7 = CamcopsColumn("ignore_s_q7", Boolean, 

242 permitted_value_checker=BIT_CHECKER) 

243 

244 @staticmethod 

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

246 _ = req.gettext 

247 return _("Functional Assessment of Cancer Therapy — General") 

248 

249 def is_complete(self) -> bool: 

250 questions_social = self.QUESTIONS_SOCIAL.copy() 

251 if self.ignore_s_q7: 

252 questions_social.remove(self.OPTIONAL_Q) 

253 

254 all_qs = [self.QUESTIONS_PHYSICAL, questions_social, 

255 self.QUESTIONS_EMOTIONAL, self.QUESTIONS_FUNCTIONAL] 

256 

257 for qlist in all_qs: 

258 if self.any_fields_none(qlist): 

259 return False 

260 

261 if not self.field_contents_valid(): 

262 return False 

263 

264 return True 

265 

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

267 return [TrackerInfo( 

268 value=self.total_score(), 

269 plot_label="FACT-G total score (rating well-being)", 

270 axis_label=f"Total score (out of {self.MAX_SCORE_TOTAL})", 

271 axis_min=-0.5, 

272 axis_max=self.MAX_SCORE_TOTAL + 0.5, 

273 axis_ticks=[ 

274 TrackerAxisTick(108, "108"), 

275 TrackerAxisTick(100, "100"), 

276 TrackerAxisTick(80, "80"), 

277 TrackerAxisTick(60, "60"), 

278 TrackerAxisTick(40, "40"), 

279 TrackerAxisTick(20, "20"), 

280 TrackerAxisTick(0, "0"), 

281 ], 

282 horizontal_lines=[ 

283 80, 

284 60, 

285 40, 

286 20 

287 ], 

288 )] 

289 

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

291 elements = self.standard_task_summary_fields() 

292 for info in self.GROUPS: 

293 subscore = info.subscore(self) 

294 elements.append(SummaryElement( 

295 name=info.summary_fieldname, coltype=Float(), 

296 value=subscore, 

297 comment=f"{info.summary_description} (out of {info.max_score})" 

298 )) 

299 elements.append(SummaryElement( 

300 name="total_score", coltype=Float(), 

301 value=self.total_score(), 

302 comment=f"Total score (out of {self.MAX_SCORE_TOTAL})" 

303 )) 

304 return elements 

305 

306 def subscores(self) -> List[float]: 

307 sscores = [] 

308 for info in self.GROUPS: 

309 sscores.append(info.subscore(self)) 

310 return sscores 

311 

312 def total_score(self) -> float: 

313 return sum(self.subscores()) 

314 

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

316 answers = { 

317 None: None, 

318 0: "0 — " + self.wxstring(req, "a0"), 

319 1: "1 — " + self.wxstring(req, "a1"), 

320 2: "2 — " + self.wxstring(req, "a2"), 

321 3: "3 — " + self.wxstring(req, "a3"), 

322 4: "4 — " + self.wxstring(req, "a4"), 

323 } 

324 subscore_html = "" 

325 answer_html = "" 

326 

327 for info in self.GROUPS: 

328 heading = self.wxstring(req, info.heading_xstring_name) 

329 subscore = info.subscore(self) 

330 subscore_html += tr( 

331 heading, 

332 ( 

333 answer(round(subscore, DISPLAY_DP)) + 

334 f" / {info.max_score}" 

335 ) 

336 ) 

337 answer_html += subheading_spanning_two_columns(heading) 

338 for q in info.fieldnames: 

339 if q == self.OPTIONAL_Q: 

340 # insert additional row 

341 answer_html += tr_qa( 

342 self.xstring(req, "prefer_no_answer"), 

343 self.ignore_s_q7) 

344 answer_val = getattr(self, q) 

345 answer_html += tr_qa(self.wxstring(req, q), 

346 get_from_dict(answers, answer_val)) 

347 

348 tscore = round(self.total_score(), DISPLAY_DP) 

349 

350 tr_total_score = tr( 

351 req.sstring(SS.TOTAL_SCORE), 

352 answer(tscore) + f" / {self.MAX_SCORE_TOTAL}" 

353 ) 

354 return f""" 

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

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

357 {self.get_is_complete_tr(req)} 

358 {tr_total_score} 

359 {subscore_html} 

360 </table> 

361 </div> 

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

363 <tr> 

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

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

366 </tr> 

367 {answer_html} 

368 </table> 

369 """