Coverage for tasks/ifs.py : 39%

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