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 

3""" 

4camcops_server/tasks/mfi20.py 

5 

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

7 

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

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27**Multidimensional Fatigue Inventory (MFI-20) task.** 

28 

29""" 

30 

31from camcops_server.cc_modules.cc_constants import CssClass 

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

33from camcops_server.cc_modules.cc_request import CamcopsRequest 

34from camcops_server.cc_modules.cc_sqla_coltypes import ( 

35 CamcopsColumn, 

36 ONE_TO_FIVE_CHECKER, 

37) 

38 

39from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

41from camcops_server.cc_modules.cc_text import SS 

42from cardinal_pythonlib.stringfunc import strseq 

43from sqlalchemy import Integer 

44from sqlalchemy.ext.declarative import DeclarativeMeta 

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

46 

47 

48class Mfi20Metaclass(DeclarativeMeta): 

49 # noinspection PyInitNewSignature 

50 def __init__(cls: Type['Mfi20'], 

51 name: str, 

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

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

54 

55 comment_strings = [ 

56 "feel fit", 

57 "physically little", 

58 "feel active", 

59 "nice things", 

60 "tired", 

61 "do a lot", 

62 "keep thought on", 

63 "take on a lot", 

64 "dread", 

65 "think little", 

66 "concentrate", 

67 "rested", 

68 "effort concentrate", 

69 "bad condition", 

70 "plans", 

71 "tire", 

72 "get little done", 

73 "don't feel like", 

74 "thoughts wander", 

75 "excellent condition", 

76 ] 

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

78 

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

80 q_num = q_index + 1 

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

82 

83 setattr(cls, q_field, CamcopsColumn( 

84 q_field, Integer, 

85 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

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

87 q_num, comment_strings[q_index], score_comment) 

88 )) 

89 

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

91 

92 

93class Mfi20(TaskHasPatientMixin, 

94 Task, 

95 metaclass=Mfi20Metaclass): 

96 __tablename__ = "mfi20" 

97 shortname = "MFI-20" 

98 

99 prohibits_clinical = True 

100 prohibits_commercial = True 

101 

102 N_QUESTIONS = 20 

103 MIN_SCORE_PER_Q = 1 

104 MAX_SCORE_PER_Q = 5 

105 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS 

106 MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS 

107 N_Q_PER_SUBSCALE = 4 # always 

108 MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE 

109 MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE 

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

111 REVERSE_QUESTIONS = Task.fieldnames_from_list( 

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

113 

114 GENERAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list( 

115 "q", {1, 5, 12, 16}) 

116 PHYSICAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list( 

117 "q", {2, 8, 14, 20}) 

118 REDUCED_ACTIVITY_QUESTIONS = Task.fieldnames_from_list( 

119 "q", {7, 11, 13, 19}) 

120 REDUCED_MOTIVATION_QUESTIONS = Task.fieldnames_from_list( 

121 "q", {3, 6, 10, 17}) 

122 MENTAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list( 

123 "q", {4, 9, 15, 18}) 

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", coltype=Integer(), 

135 value=self.total_score(), 

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

137 SummaryElement( 

138 name="general_fatigue", coltype=Integer(), 

139 value=self.general_fatigue_score(), 

140 comment=f"General fatigue {subscale_range}"), 

141 SummaryElement( 

142 name="physical_fatigue", coltype=Integer(), 

143 value=self.physical_fatigue_score(), 

144 comment=f"Physical fatigue {subscale_range}"), 

145 SummaryElement( 

146 name="reduced_activity", coltype=Integer(), 

147 value=self.reduced_activity_score(), 

148 comment=f"Reduced activity {subscale_range}"), 

149 SummaryElement( 

150 name="reduced_motivation", coltype=Integer(), 

151 value=self.reduced_motivation_score(), 

152 comment=f"Reduced motivation {subscale_range}"), 

153 SummaryElement( 

154 name="mental_fatigue", coltype=Integer(), 

155 value=self.mental_fatigue_score(), 

156 comment=f"Mental fatigue {subscale_range}"), 

157 ] 

158 

159 def is_complete(self) -> bool: 

160 if self.any_fields_none(self.ALL_QUESTIONS): 

161 return False 

162 if not self.field_contents_valid(): 

163 return False 

164 return True 

165 

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

167 total = 0 

168 for f in fields: 

169 value = getattr(self, f) 

170 if value is not None: 

171 if f in self.REVERSE_QUESTIONS: 

172 value = self.MAX_SCORE_PER_Q + 1 - value 

173 

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

175 

176 return total 

177 

178 def total_score(self) -> int: 

179 return self.score_fields(self.ALL_QUESTIONS) 

180 

181 def general_fatigue_score(self) -> int: 

182 return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS) 

183 

184 def physical_fatigue_score(self) -> int: 

185 return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS) 

186 

187 def reduced_activity_score(self) -> int: 

188 return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS) 

189 

190 def reduced_motivation_score(self) -> int: 

191 return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS) 

192 

193 def mental_fatigue_score(self) -> int: 

194 return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS) 

195 

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

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

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

199 

200 rows = "" 

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

202 q_field = "q" + str(q_num) 

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

204 

205 score = getattr(self, q_field) 

206 

207 rows += tr_qa(question_cell, score) 

208 

209 html = """ 

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

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

212 {tr_is_complete} 

213 {total_score} 

214 {general_fatigue_score} 

215 {physical_fatigue_score} 

216 {reduced_activity_score} 

217 {reduced_motivation_score} 

218 {mental_fatigue_score} 

219 </table> 

220 </div> 

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

222 <tr> 

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

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

225 </tr> 

226 {rows} 

227 </table> 

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

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

230 reverse-scored when summing. 

231 [2] Sum for questions 1–20. 

232 [3] Sum for questions 1, 5, 12, 16. 

233 [4] Sum for questions 2, 8, 14, 20. 

234 [5] Sum for questions 7, 11, 13, 19. 

235 [6] Sum for questions 3, 6, 10, 17. 

236 [7] Sum for questions 4, 9, 15, 18. 

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

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

239 </div> 

240 """.format( 

241 CssClass=CssClass, 

242 tr_is_complete=self.get_is_complete_tr(req), 

243 total_score=tr( 

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

245 f"{answer(self.total_score())} {fullscale_range}" 

246 ), 

247 general_fatigue_score=tr( 

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

249 f"{answer(self.general_fatigue_score())} {subscale_range}" 

250 ), 

251 physical_fatigue_score=tr( 

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

253 f"{answer(self.physical_fatigue_score())} {subscale_range}" 

254 ), 

255 reduced_activity_score=tr( 

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

257 f"{answer(self.reduced_activity_score())} {subscale_range}" 

258 ), 

259 reduced_motivation_score=tr( 

260 self.wxstring(req, 

261 "reduced_motivation") + " <sup>[1][6]</sup>", 

262 f"{answer(self.reduced_motivation_score())} {subscale_range}" 

263 ), 

264 mental_fatigue_score=tr( 

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

266 f"{answer(self.mental_fatigue_score())} {subscale_range}" 

267 ), 

268 rows=rows, 

269 ) 

270 return html