Coverage for tasks/mfi20.py: 60%

83 statements  

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

1""" 

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

27 

28""" 

29 

30from typing import Any, List, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy import Integer 

34 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

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

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_sqla_coltypes import ( 

40 camcops_column, 

41 ONE_TO_FIVE_CHECKER, 

42) 

43 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

46from camcops_server.cc_modules.cc_text import SS 

47 

48 

49class Mfi20( # type: ignore[misc] 

50 TaskHasPatientMixin, 

51 Task, 

52): 

53 __tablename__ = "mfi20" 

54 shortname = "MFI-20" 

55 

56 prohibits_clinical = True 

57 prohibits_commercial = True 

58 

59 N_QUESTIONS = 20 

60 MIN_SCORE_PER_Q = 1 

61 MAX_SCORE_PER_Q = 5 

62 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS 

63 MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS 

64 N_Q_PER_SUBSCALE = 4 # always 

65 MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE 

66 MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE 

67 

68 @classmethod 

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

70 

71 comment_strings = [ 

72 "feel fit", 

73 "physically little", 

74 "feel active", 

75 "nice things", 

76 "tired", 

77 "do a lot", 

78 "keep thought on", 

79 "take on a lot", 

80 "dread", 

81 "think little", 

82 "concentrate", 

83 "rested", 

84 "effort concentrate", 

85 "bad condition", 

86 "plans", 

87 "tire", 

88 "get little done", 

89 "don't feel like", 

90 "thoughts wander", 

91 "excellent condition", 

92 ] 

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

94 

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

96 q_num = q_index + 1 

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

98 

99 setattr( 

100 cls, 

101 q_field, 

102 camcops_column( 

103 q_field, 

104 Integer, 

105 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

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

107 q_num, comment_strings[q_index], score_comment 

108 ), 

109 ), 

110 ) 

111 

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

113 REVERSE_QUESTIONS = Task.fieldnames_from_list( 

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

115 ) 

116 

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

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

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

120 REDUCED_MOTIVATION_QUESTIONS = Task.fieldnames_from_list( 

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

122 ) 

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

124 

125 @staticmethod 

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

127 _ = req.gettext 

128 return _("Multidimensional Fatigue Inventory") 

129 

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

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

132 return self.standard_task_summary_fields() + [ 

133 SummaryElement( 

134 name="total", 

135 coltype=Integer(), 

136 value=self.total_score(), 

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

138 ), 

139 SummaryElement( 

140 name="general_fatigue", 

141 coltype=Integer(), 

142 value=self.general_fatigue_score(), 

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

144 ), 

145 SummaryElement( 

146 name="physical_fatigue", 

147 coltype=Integer(), 

148 value=self.physical_fatigue_score(), 

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

150 ), 

151 SummaryElement( 

152 name="reduced_activity", 

153 coltype=Integer(), 

154 value=self.reduced_activity_score(), 

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

156 ), 

157 SummaryElement( 

158 name="reduced_motivation", 

159 coltype=Integer(), 

160 value=self.reduced_motivation_score(), 

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

162 ), 

163 SummaryElement( 

164 name="mental_fatigue", 

165 coltype=Integer(), 

166 value=self.mental_fatigue_score(), 

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

168 ), 

169 ] 

170 

171 def is_complete(self) -> bool: 

172 if self.any_fields_none(self.ALL_QUESTIONS): 

173 return False 

174 if not self.field_contents_valid(): 

175 return False 

176 return True 

177 

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

179 total = 0 

180 for f in fields: 

181 value = getattr(self, f) 

182 if value is not None: 

183 if f in self.REVERSE_QUESTIONS: 

184 value = self.MAX_SCORE_PER_Q + 1 - value 

185 

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

187 

188 return total 

189 

190 def total_score(self) -> int: 

191 return self.score_fields(self.ALL_QUESTIONS) 

192 

193 def general_fatigue_score(self) -> int: 

194 return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS) 

195 

196 def physical_fatigue_score(self) -> int: 

197 return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS) 

198 

199 def reduced_activity_score(self) -> int: 

200 return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS) 

201 

202 def reduced_motivation_score(self) -> int: 

203 return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS) 

204 

205 def mental_fatigue_score(self) -> int: 

206 return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS) 

207 

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

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

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

211 

212 rows = "" 

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

214 q_field = "q" + str(q_num) 

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

216 

217 score = getattr(self, q_field) 

218 

219 rows += tr_qa(question_cell, score) 

220 

221 html = """ 

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

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

224 {tr_is_complete} 

225 {total_score} 

226 {general_fatigue_score} 

227 {physical_fatigue_score} 

228 {reduced_activity_score} 

229 {reduced_motivation_score} 

230 {mental_fatigue_score} 

231 </table> 

232 </div> 

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

234 <tr> 

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

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

237 </tr> 

238 {rows} 

239 </table> 

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

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

242 reverse-scored when summing. 

243 [2] Sum for questions 1–20. 

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

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

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

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

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

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

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

251 </div> 

252 """.format( 

253 CssClass=CssClass, 

254 tr_is_complete=self.get_is_complete_tr(req), 

255 total_score=tr( 

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

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

258 ), 

259 general_fatigue_score=tr( 

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

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

262 ), 

263 physical_fatigue_score=tr( 

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

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

266 ), 

267 reduced_activity_score=tr( 

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

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

270 ), 

271 reduced_motivation_score=tr( 

272 self.wxstring(req, "reduced_motivation") 

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

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

275 ), 

276 mental_fatigue_score=tr( 

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

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

279 ), 

280 rows=rows, 

281 ) 

282 return html