Coverage for tasks/mfi20.py: 62%

86 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/mfi20.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**Multidimensional Fatigue Inventory (MFI-20) task.** 

29 

30""" 

31 

32from camcops_server.cc_modules.cc_constants import CssClass 

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

34from camcops_server.cc_modules.cc_request import CamcopsRequest 

35from camcops_server.cc_modules.cc_sqla_coltypes import ( 

36 CamcopsColumn, 

37 ONE_TO_FIVE_CHECKER, 

38) 

39 

40from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

41from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

42from camcops_server.cc_modules.cc_text import SS 

43from cardinal_pythonlib.stringfunc import strseq 

44from sqlalchemy import Integer 

45from sqlalchemy.ext.declarative import DeclarativeMeta 

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

47 

48 

49class Mfi20Metaclass(DeclarativeMeta): 

50 # noinspection PyInitNewSignature 

51 def __init__( 

52 cls: Type["Mfi20"], 

53 name: str, 

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

55 classdict: Dict[str, Any], 

56 ) -> None: 

57 

58 comment_strings = [ 

59 "feel fit", 

60 "physically little", 

61 "feel active", 

62 "nice things", 

63 "tired", 

64 "do a lot", 

65 "keep thought on", 

66 "take on a lot", 

67 "dread", 

68 "think little", 

69 "concentrate", 

70 "rested", 

71 "effort concentrate", 

72 "bad condition", 

73 "plans", 

74 "tire", 

75 "get little done", 

76 "don't feel like", 

77 "thoughts wander", 

78 "excellent condition", 

79 ] 

80 score_comment = "(1 yes - 5 no)" 

81 

82 for q_index in range(0, cls.N_QUESTIONS): 

83 q_num = q_index + 1 

84 q_field = "q{}".format(q_num) 

85 

86 setattr( 

87 cls, 

88 q_field, 

89 CamcopsColumn( 

90 q_field, 

91 Integer, 

92 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

93 comment="Q{} ({}) {}".format( 

94 q_num, comment_strings[q_index], score_comment 

95 ), 

96 ), 

97 ) 

98 

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

100 

101 

102class Mfi20(TaskHasPatientMixin, Task, metaclass=Mfi20Metaclass): 

103 __tablename__ = "mfi20" 

104 shortname = "MFI-20" 

105 

106 prohibits_clinical = True 

107 prohibits_commercial = True 

108 

109 N_QUESTIONS = 20 

110 MIN_SCORE_PER_Q = 1 

111 MAX_SCORE_PER_Q = 5 

112 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS 

113 MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS 

114 N_Q_PER_SUBSCALE = 4 # always 

115 MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE 

116 MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE 

117 ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) 

118 REVERSE_QUESTIONS = Task.fieldnames_from_list( 

119 "q", {2, 5, 9, 10, 13, 14, 16, 17, 18, 19} 

120 ) 

121 

122 GENERAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {1, 5, 12, 16}) 

123 PHYSICAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {2, 8, 14, 20}) 

124 REDUCED_ACTIVITY_QUESTIONS = Task.fieldnames_from_list("q", {3, 6, 10, 17}) 

125 REDUCED_MOTIVATION_QUESTIONS = Task.fieldnames_from_list( 

126 "q", {4, 9, 15, 18} 

127 ) 

128 MENTAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {7, 11, 13, 19}) 

129 

130 @staticmethod 

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

132 _ = req.gettext 

133 return _("Multidimensional Fatigue Inventory") 

134 

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

136 subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" 

137 return self.standard_task_summary_fields() + [ 

138 SummaryElement( 

139 name="total", 

140 coltype=Integer(), 

141 value=self.total_score(), 

142 comment=f"Total score [{self.MIN_SCORE}–{self.MAX_SCORE}]", 

143 ), 

144 SummaryElement( 

145 name="general_fatigue", 

146 coltype=Integer(), 

147 value=self.general_fatigue_score(), 

148 comment=f"General fatigue {subscale_range}", 

149 ), 

150 SummaryElement( 

151 name="physical_fatigue", 

152 coltype=Integer(), 

153 value=self.physical_fatigue_score(), 

154 comment=f"Physical fatigue {subscale_range}", 

155 ), 

156 SummaryElement( 

157 name="reduced_activity", 

158 coltype=Integer(), 

159 value=self.reduced_activity_score(), 

160 comment=f"Reduced activity {subscale_range}", 

161 ), 

162 SummaryElement( 

163 name="reduced_motivation", 

164 coltype=Integer(), 

165 value=self.reduced_motivation_score(), 

166 comment=f"Reduced motivation {subscale_range}", 

167 ), 

168 SummaryElement( 

169 name="mental_fatigue", 

170 coltype=Integer(), 

171 value=self.mental_fatigue_score(), 

172 comment=f"Mental fatigue {subscale_range}", 

173 ), 

174 ] 

175 

176 def is_complete(self) -> bool: 

177 if self.any_fields_none(self.ALL_QUESTIONS): 

178 return False 

179 if not self.field_contents_valid(): 

180 return False 

181 return True 

182 

183 def score_fields(self, fields: List[str]) -> int: 

184 total = 0 

185 for f in fields: 

186 value = getattr(self, f) 

187 if value is not None: 

188 if f in self.REVERSE_QUESTIONS: 

189 value = self.MAX_SCORE_PER_Q + 1 - value 

190 

191 total += value if value is not None else 0 

192 

193 return total 

194 

195 def total_score(self) -> int: 

196 return self.score_fields(self.ALL_QUESTIONS) 

197 

198 def general_fatigue_score(self) -> int: 

199 return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS) 

200 

201 def physical_fatigue_score(self) -> int: 

202 return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS) 

203 

204 def reduced_activity_score(self) -> int: 

205 return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS) 

206 

207 def reduced_motivation_score(self) -> int: 

208 return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS) 

209 

210 def mental_fatigue_score(self) -> int: 

211 return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS) 

212 

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

214 fullscale_range = f"[{self.MIN_SCORE}–{self.MAX_SCORE}]" 

215 subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" 

216 

217 rows = "" 

218 for q_num in range(1, self.N_QUESTIONS + 1): 

219 q_field = "q" + str(q_num) 

220 question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) 

221 

222 score = getattr(self, q_field) 

223 

224 rows += tr_qa(question_cell, score) 

225 

226 html = """ 

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

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

229 {tr_is_complete} 

230 {total_score} 

231 {general_fatigue_score} 

232 {physical_fatigue_score} 

233 {reduced_activity_score} 

234 {reduced_motivation_score} 

235 {mental_fatigue_score} 

236 </table> 

237 </div> 

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

239 <tr> 

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

241 <th width="40%">Answer <sup>[8]</sup></th> 

242 </tr> 

243 {rows} 

244 </table> 

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

246 [1] Questions 2, 5, 9, 10, 13, 14, 16, 17, 18, 19 

247 reverse-scored when summing. 

248 [2] Sum for questions 1–20. 

249 [3] General fatigue: Sum for questions 1, 5, 12, 16. 

250 [4] Physical fatigue: Sum for questions 2, 8, 14, 20. 

251 [5] Reduced activity: Sum for questions 3, 6, 10, 17. 

252 [6] Reduced motivation: Sum for questions 4, 9, 15, 18. 

253 [7] Mental fatigue: Sum for questions 7, 11, 13, 19. 

254 [8] All questions are rated from “1 – yes, that is true” to 

255 “5 – no, that is not true”. 

256 </div> 

257 """.format( 

258 CssClass=CssClass, 

259 tr_is_complete=self.get_is_complete_tr(req), 

260 total_score=tr( 

261 req.sstring(SS.TOTAL_SCORE) + " <sup>[1][2]</sup>", 

262 f"{answer(self.total_score())} {fullscale_range}", 

263 ), 

264 general_fatigue_score=tr( 

265 self.wxstring(req, "general_fatigue") + " <sup>[1][3]</sup>", 

266 f"{answer(self.general_fatigue_score())} {subscale_range}", 

267 ), 

268 physical_fatigue_score=tr( 

269 self.wxstring(req, "physical_fatigue") + " <sup>[1][4]</sup>", 

270 f"{answer(self.physical_fatigue_score())} {subscale_range}", 

271 ), 

272 reduced_activity_score=tr( 

273 self.wxstring(req, "reduced_activity") + " <sup>[1][5]</sup>", 

274 f"{answer(self.reduced_activity_score())} {subscale_range}", 

275 ), 

276 reduced_motivation_score=tr( 

277 self.wxstring(req, "reduced_motivation") 

278 + " <sup>[1][6]</sup>", 

279 f"{answer(self.reduced_motivation_score())} {subscale_range}", 

280 ), 

281 mental_fatigue_score=tr( 

282 self.wxstring(req, "mental_fatigue") + " <sup>[1][7]</sup>", 

283 f"{answer(self.mental_fatigue_score())} {subscale_range}", 

284 ), 

285 rows=rows, 

286 ) 

287 return html