Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/tasks/ifs.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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/>. 

24 

25=============================================================================== 

26 

27""" 

28 

29from typing import Any, Dict, List, Tuple, Type 

30 

31from sqlalchemy.ext.declarative import DeclarativeMeta 

32from sqlalchemy.sql.sqltypes import Boolean, Float, Integer 

33 

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 

62 

63 

64# ============================================================================= 

65# IFS 

66# ============================================================================= 

67 

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) 

130 

131 

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 

140 

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 ) 

161 

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 

174 

175 @staticmethod 

176 def longname(req: "CamcopsRequest") -> str: 

177 _ = req.gettext 

178 return _("INECO Frontal Screening") 

179 

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 ] 

198 

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 ] 

214 

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 )] 

225 

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 ) 

248 

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 

262 

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) 

271 

272 def get_task_html(self, req: CamcopsRequest) -> str: 

273 scoredict = self.get_score() 

274 

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) 

346 

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 """