Coverage for tasks/mfi20.py: 60%

1"""

2 camcops_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

30 from typing import Any, List, Type

31

32 from cardinal_pythonlib.stringfunc import strseq

33 from sqlalchemy import Integer

34

35

36 from camcops_server.cc_modules.cc_constants import CssClass

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

38 from camcops_server.cc_modules.cc_request import CamcopsRequest

39 from camcops_server.cc_modules.cc_sqla_coltypes import (

40 camcops_column,

41 ONE_TO_FIVE_CHECKER,

42)

43

44 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

45 from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task

46 from camcops_server.cc_modules.cc_text import SS

47

48

49 class 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