Coverage for tasks/mfi20.py: 60%
83 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/mfi20.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26**Multidimensional Fatigue Inventory (MFI-20) task.**
28"""
30from typing import Any, List, Type
32from cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy import Integer
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)
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
49class Mfi20( # type: ignore[misc]
50 TaskHasPatientMixin,
51 Task,
52):
53 __tablename__ = "mfi20"
54 shortname = "MFI-20"
56 prohibits_clinical = True
57 prohibits_commercial = True
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
68 @classmethod
69 def extend_columns(cls: Type["Mfi20"], **kwargs: Any) -> None:
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)"
95 for q_index in range(0, cls.N_QUESTIONS):
96 q_num = q_index + 1
97 q_field = "q{}".format(q_num)
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 )
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 )
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})
125 @staticmethod
126 def longname(req: "CamcopsRequest") -> str:
127 _ = req.gettext
128 return _("Multidimensional Fatigue Inventory")
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 ]
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
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
186 total += value if value is not None else 0
188 return total
190 def total_score(self) -> int:
191 return self.score_fields(self.ALL_QUESTIONS)
193 def general_fatigue_score(self) -> int:
194 return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS)
196 def physical_fatigue_score(self) -> int:
197 return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS)
199 def reduced_activity_score(self) -> int:
200 return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS)
202 def reduced_motivation_score(self) -> int:
203 return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS)
205 def mental_fatigue_score(self) -> int:
206 return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS)
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}]"
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))
217 score = getattr(self, q_field)
219 rows += tr_qa(question_cell, score)
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