Coverage for tasks/mfi20.py: 62%
86 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/mfi20.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28**Multidimensional Fatigue Inventory (MFI-20) task.**
30"""
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)
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
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:
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)"
82 for q_index in range(0, cls.N_QUESTIONS):
83 q_num = q_index + 1
84 q_field = "q{}".format(q_num)
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 )
99 super().__init__(name, bases, classdict)
102class Mfi20(TaskHasPatientMixin, Task, metaclass=Mfi20Metaclass):
103 __tablename__ = "mfi20"
104 shortname = "MFI-20"
106 prohibits_clinical = True
107 prohibits_commercial = True
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 )
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})
130 @staticmethod
131 def longname(req: "CamcopsRequest") -> str:
132 _ = req.gettext
133 return _("Multidimensional Fatigue Inventory")
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 ]
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
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
191 total += value if value is not None else 0
193 return total
195 def total_score(self) -> int:
196 return self.score_fields(self.ALL_QUESTIONS)
198 def general_fatigue_score(self) -> int:
199 return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS)
201 def physical_fatigue_score(self) -> int:
202 return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS)
204 def reduced_activity_score(self) -> int:
205 return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS)
207 def reduced_motivation_score(self) -> int:
208 return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS)
210 def mental_fatigue_score(self) -> int:
211 return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS)
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}]"
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))
222 score = getattr(self, q_field)
224 rows += tr_qa(question_cell, score)
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