Coverage for tasks/mfi20.py : 62%

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
3"""
4camcops_server/tasks/mfi20.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27**Multidimensional Fatigue Inventory (MFI-20) task.**
29"""
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)
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
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:
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)"
79 for q_index in range(0, cls.N_QUESTIONS):
80 q_num = q_index + 1
81 q_field = "q{}".format(q_num)
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 ))
90 super().__init__(name, bases, classdict)
93class Mfi20(TaskHasPatientMixin,
94 Task,
95 metaclass=Mfi20Metaclass):
96 __tablename__ = "mfi20"
97 shortname = "MFI-20"
99 prohibits_clinical = True
100 prohibits_commercial = True
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})
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})
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", 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 ]
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
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
174 total += value if value is not None else 0
176 return total
178 def total_score(self) -> int:
179 return self.score_fields(self.ALL_QUESTIONS)
181 def general_fatigue_score(self) -> int:
182 return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS)
184 def physical_fatigue_score(self) -> int:
185 return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS)
187 def reduced_activity_score(self) -> int:
188 return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS)
190 def reduced_motivation_score(self) -> int:
191 return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS)
193 def mental_fatigue_score(self) -> int:
194 return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS)
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}]"
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))
205 score = getattr(self, q_field)
207 rows += tr_qa(question_cell, score)
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