Coverage for tasks/ifs.py: 39%
137 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/ifs.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"""
30from typing import Any, Dict, List, Tuple, Type
32from sqlalchemy.ext.declarative import DeclarativeMeta
33from sqlalchemy.sql.sqltypes import Boolean, Float, Integer
35from camcops_server.cc_modules.cc_constants import (
36 CssClass,
37 DATA_COLLECTION_UNLESS_UPGRADED_DIV,
38 INVALID_VALUE,
39)
40from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
41from camcops_server.cc_modules.cc_html import (
42 answer,
43 get_correct_incorrect_none,
44 td,
45 tr,
46 tr_qa,
47)
48from camcops_server.cc_modules.cc_request import CamcopsRequest
49from camcops_server.cc_modules.cc_sqla_coltypes import (
50 BIT_CHECKER,
51 CamcopsColumn,
52 ZERO_TO_ONE_CHECKER,
53 ZERO_TO_TWO_CHECKER,
54 ZERO_TO_THREE_CHECKER,
55)
56from camcops_server.cc_modules.cc_summaryelement import SummaryElement
57from camcops_server.cc_modules.cc_task import (
58 Task,
59 TaskHasClinicianMixin,
60 TaskHasPatientMixin,
61)
62from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
65# =============================================================================
66# IFS
67# =============================================================================
70class IfsMetaclass(DeclarativeMeta):
71 # noinspection PyInitNewSignature
72 def __init__(
73 cls: Type["Ifs"],
74 name: str,
75 bases: Tuple[Type, ...],
76 classdict: Dict[str, Any],
77 ) -> None:
78 for seqlen in cls.Q4_DIGIT_LENGTHS:
79 fname1 = f"q4_len{seqlen}_1"
80 fname2 = f"q4_len{seqlen}_2"
81 setattr(
82 cls,
83 fname1,
84 CamcopsColumn(
85 fname1,
86 Boolean,
87 permitted_value_checker=BIT_CHECKER,
88 comment=f"Q4. Digits backward, length {seqlen}, trial 1",
89 ),
90 )
91 setattr(
92 cls,
93 fname2,
94 CamcopsColumn(
95 fname2,
96 Boolean,
97 permitted_value_checker=BIT_CHECKER,
98 comment=f"Q4. Digits backward, length {seqlen}, trial 2",
99 ),
100 )
101 for n in cls.Q6_SEQUENCE_NUMS:
102 fname = f"q6_seq{n}"
103 setattr(
104 cls,
105 fname,
106 CamcopsColumn(
107 fname,
108 Integer,
109 permitted_value_checker=BIT_CHECKER,
110 comment=f"Q6. Spatial working memory, sequence {n}",
111 ),
112 )
113 for n in cls.Q7_PROVERB_NUMS:
114 fname = "q7_proverb{}".format(n)
115 setattr(
116 cls,
117 fname,
118 CamcopsColumn(
119 fname,
120 Float,
121 permitted_value_checker=ZERO_TO_ONE_CHECKER,
122 comment=f"Q7. Proverb {n} (1 = correct explanation, "
123 f"0.5 = example, 0 = neither)",
124 ),
125 )
126 for n in cls.Q8_SENTENCE_NUMS:
127 fname = "q8_sentence{}".format(n)
128 setattr(
129 cls,
130 fname,
131 CamcopsColumn(
132 fname,
133 Integer,
134 permitted_value_checker=ZERO_TO_TWO_CHECKER,
135 comment=f"Q8. Hayling, sentence {n}",
136 ),
137 )
138 super().__init__(name, bases, classdict)
141class Ifs(
142 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=IfsMetaclass
143):
144 """
145 Server implementation of the IFS task.
146 """
148 __tablename__ = "ifs"
149 shortname = "IFS"
150 provides_trackers = True
152 q1 = CamcopsColumn(
153 "q1",
154 Integer,
155 permitted_value_checker=ZERO_TO_THREE_CHECKER,
156 comment="Q1. Motor series (motor programming)",
157 )
158 q2 = CamcopsColumn(
159 "q2",
160 Integer,
161 permitted_value_checker=ZERO_TO_THREE_CHECKER,
162 comment="Q2. Conflicting instructions (interference sensitivity)",
163 )
164 q3 = CamcopsColumn(
165 "q3",
166 Integer,
167 permitted_value_checker=ZERO_TO_THREE_CHECKER,
168 comment="Q3. Go/no-go (inhibitory control)",
169 )
170 q5 = CamcopsColumn(
171 "q5",
172 Integer,
173 permitted_value_checker=ZERO_TO_TWO_CHECKER,
174 comment="Q5. Verbal working memory",
175 )
177 Q4_DIGIT_LENGTHS = list(range(2, 7 + 1))
178 Q6_SEQUENCE_NUMS = list(range(1, 4 + 1))
179 Q7_PROVERB_NUMS = list(range(1, 3 + 1))
180 Q8_SENTENCE_NUMS = list(range(1, 3 + 1))
181 SIMPLE_Q = (
182 ["q1", "q2", "q3", "q5"]
183 + [f"q6_seq{n}" for n in Q6_SEQUENCE_NUMS]
184 + [f"q7_proverb{n}" for n in Q7_PROVERB_NUMS]
185 + [f"q8_sentence{n}" for n in Q8_SENTENCE_NUMS]
186 )
187 MAX_TOTAL = 30
188 MAX_WM = 10
190 @staticmethod
191 def longname(req: "CamcopsRequest") -> str:
192 _ = req.gettext
193 return _("INECO Frontal Screening")
195 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
196 scoredict = self.get_score()
197 return [
198 TrackerInfo(
199 value=scoredict["total"],
200 plot_label="IFS total score (higher is better)",
201 axis_label=f"Total score (out of {self.MAX_TOTAL})",
202 axis_min=-0.5,
203 axis_max=self.MAX_TOTAL + 0.5,
204 ),
205 TrackerInfo(
206 value=scoredict["wm"],
207 plot_label="IFS working memory index (higher is better)",
208 axis_label=f"Total score (out of {self.MAX_WM})",
209 axis_min=-0.5,
210 axis_max=self.MAX_WM + 0.5,
211 ),
212 ]
214 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
215 scoredict = self.get_score()
216 return self.standard_task_summary_fields() + [
217 SummaryElement(
218 name="total",
219 coltype=Float(),
220 value=scoredict["total"],
221 comment=f"Total (out of {self.MAX_TOTAL}, higher better)",
222 ),
223 SummaryElement(
224 name="wm",
225 coltype=Integer(),
226 value=scoredict["wm"],
227 comment=f"Working memory index (out of {self.MAX_WM}; "
228 f"sum of Q4 + Q6",
229 ),
230 ]
232 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
233 scoredict = self.get_score()
234 if not self.is_complete():
235 return CTV_INCOMPLETE
236 return [
237 CtvInfo(
238 content=(
239 f"Total: {scoredict['total']}/{self.MAX_TOTAL}; "
240 f"working memory index {scoredict['wm']}/{self.MAX_WM}"
241 )
242 )
243 ]
245 def get_score(self) -> Dict:
246 q1 = getattr(self, "q1", 0) or 0
247 q2 = getattr(self, "q2", 0) or 0
248 q3 = getattr(self, "q3", 0) or 0
249 q4 = 0
250 for seqlen in self.Q4_DIGIT_LENGTHS:
251 val1 = getattr(self, f"q4_len{seqlen}_1")
252 val2 = getattr(self, f"q4_len{seqlen}_2")
253 if val1 or val2:
254 q4 += 1
255 if not val1 and not val2:
256 break
257 q5 = getattr(self, "q5", 0) or 0
258 q6 = self.sum_fields(["q6_seq" + str(s) for s in range(1, 4 + 1)])
259 q7 = self.sum_fields(["q7_proverb" + str(s) for s in range(1, 3 + 1)])
260 q8 = self.sum_fields(["q8_sentence" + str(s) for s in range(1, 3 + 1)])
261 total = q1 + q2 + q3 + q4 + q5 + q6 + q7 + q8
262 wm = q4 + q6 # working memory index (though not verbal)
263 return dict(total=total, wm=wm)
265 def is_complete(self) -> bool:
266 if not self.field_contents_valid():
267 return False
268 if self.any_fields_none(self.SIMPLE_Q):
269 return False
270 for seqlen in self.Q4_DIGIT_LENGTHS:
271 val1 = getattr(self, f"q4_len{seqlen}_1")
272 val2 = getattr(self, f"q4_len{seqlen}_2")
273 if val1 is None or val2 is None:
274 return False
275 if not val1 and not val2:
276 return True # all done
277 return True
279 def get_simple_tr_qa(self, req: CamcopsRequest, qprefix: str) -> str:
280 q = self.wxstring(req, qprefix + "_title")
281 val = getattr(self, qprefix)
282 if val is not None:
283 a = self.wxstring(req, qprefix + "_a" + str(val))
284 else:
285 a = None
286 return tr_qa(q, a)
288 def get_task_html(self, req: CamcopsRequest) -> str:
289 scoredict = self.get_score()
291 # Q1
292 q_a = self.get_simple_tr_qa(req, "q1")
293 # Q2
294 q_a += self.get_simple_tr_qa(req, "q2")
295 # Q3
296 q_a += self.get_simple_tr_qa(req, "q3")
297 # Q4
298 q_a += tr(
299 td(self.wxstring(req, "q4_title")),
300 td("", td_class=CssClass.SUBHEADING),
301 literal=True,
302 )
303 required = True
304 for n in self.Q4_DIGIT_LENGTHS:
305 val1 = getattr(self, f"q4_len{n}_1")
306 val2 = getattr(self, f"q4_len{n}_2")
307 q = (
308 "… "
309 + self.wxstring(req, f"q4_seq_len{n}_1")
310 + " / "
311 + self.wxstring(req, f"q4_seq_len{n}_2")
312 )
313 if required:
314 score = 1 if val1 or val2 else 0
315 a = (
316 answer(get_correct_incorrect_none(val1))
317 + " / "
318 + answer(get_correct_incorrect_none(val2))
319 + f" (scores {score})"
320 )
321 else:
322 a = ""
323 q_a += tr(q, a)
324 if not val1 and not val2:
325 required = False
326 # Q5
327 q_a += self.get_simple_tr_qa(req, "q5")
328 # Q6
329 q_a += tr(
330 td(self.wxstring(req, "q6_title")),
331 td("", td_class=CssClass.SUBHEADING),
332 literal=True,
333 )
334 for n in self.Q6_SEQUENCE_NUMS:
335 nstr = str(n)
336 val = getattr(self, "q6_seq" + nstr)
337 q_a += tr_qa("… " + self.wxstring(req, "q6_seq" + nstr), val)
338 # Q7
339 q7map = {
340 None: None,
341 1: self.wxstring(req, "q7_a_1"),
342 0.5: self.wxstring(req, "q7_a_half"),
343 0: self.wxstring(req, "q7_a_0"),
344 }
345 q_a += tr(
346 td(self.wxstring(req, "q7_title")),
347 td("", td_class=CssClass.SUBHEADING),
348 literal=True,
349 )
350 for n in self.Q7_PROVERB_NUMS:
351 nstr = str(n)
352 val = getattr(self, "q7_proverb" + nstr)
353 a = q7map.get(val, INVALID_VALUE)
354 q_a += tr_qa("… " + self.wxstring(req, "q7_proverb" + nstr), a)
355 # Q8
356 q8map = {
357 None: None,
358 2: self.wxstring(req, "q8_a2"),
359 1: self.wxstring(req, "q8_a1"),
360 0: self.wxstring(req, "q8_a0"),
361 }
362 q_a += tr(
363 td(self.wxstring(req, "q8_title")),
364 td("", td_class=CssClass.SUBHEADING),
365 literal=True,
366 )
367 for n in self.Q8_SENTENCE_NUMS:
368 nstr = str(n)
369 val = getattr(self, "q8_sentence" + nstr)
370 a = q8map.get(val, INVALID_VALUE)
371 q_a += tr_qa("… " + self.wxstring(req, "q8_sentence_" + nstr), a)
373 return f"""
374 <div class="{CssClass.SUMMARY}">
375 <table class="{CssClass.SUMMARY}">
376 {self.get_is_complete_tr(req)}
377 <tr>
378 <td>Total (higher better)</td>
379 <td>{answer(scoredict['total'])} / {self.MAX_TOTAL}</td>
380 </td>
381 <tr>
382 <td>Working memory index <sup>1</sup></td>
383 <td>{answer(scoredict['wm'])} / {self.MAX_WM}</td>
384 </td>
385 </table>
386 </div>
387 <table class="{CssClass.TASKDETAIL}">
388 <tr>
389 <th width="50%">Question</th>
390 <th width="50%">Answer</th>
391 </tr>
392 {q_a}
393 </table>
394 <div class="{CssClass.FOOTNOTES}">
395 [1] Sum of scores for Q4 + Q6.
396 </div>
397 {DATA_COLLECTION_UNLESS_UPGRADED_DIV}
398 """ # noqa: E501