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/slums.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 List, Optional 

30 

31from sqlalchemy.sql.schema import Column 

32from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

33 

34from camcops_server.cc_modules.cc_blob import ( 

35 Blob, 

36 blob_relationship, 

37 get_blob_img_html, 

38) 

39from camcops_server.cc_modules.cc_constants import CssClass 

40from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

41from camcops_server.cc_modules.cc_html import ( 

42 answer, 

43 get_yes_no_none, 

44 subheading_spanning_two_columns, 

45 td, 

46 tr, 

47 tr_qa, 

48) 

49from camcops_server.cc_modules.cc_request import CamcopsRequest 

50from camcops_server.cc_modules.cc_sqla_coltypes import ( 

51 BIT_CHECKER, 

52 CamcopsColumn, 

53 PermittedValueChecker, 

54 SummaryCategoryColType, 

55 ZERO_TO_THREE_CHECKER, 

56) 

57from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

58from camcops_server.cc_modules.cc_task import ( 

59 Task, 

60 TaskHasClinicianMixin, 

61 TaskHasPatientMixin, 

62) 

63from camcops_server.cc_modules.cc_text import SS 

64from camcops_server.cc_modules.cc_trackerhelpers import ( 

65 TrackerInfo, 

66 TrackerLabel, 

67) 

68 

69 

70# ============================================================================= 

71# SLUMS 

72# ============================================================================= 

73 

74ZERO_OR_TWO_CHECKER = PermittedValueChecker(permitted_values=[0, 2]) 

75 

76 

77class Slums(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

78 """ 

79 Server implementation of the SLUMS task. 

80 """ 

81 __tablename__ = "slums" 

82 shortname = "SLUMS" 

83 provides_trackers = True 

84 

85 alert = CamcopsColumn( 

86 "alert", Integer, 

87 permitted_value_checker=BIT_CHECKER, 

88 comment="Is the patient alert? (0 no, 1 yes)") 

89 highschooleducation = CamcopsColumn( 

90 "highschooleducation", Integer, 

91 permitted_value_checker=BIT_CHECKER, 

92 comment="Does that patient have at least a high-school level of " 

93 "education? (0 no, 1 yes)" 

94 ) 

95 

96 q1 = CamcopsColumn( 

97 "q1", Integer, 

98 permitted_value_checker=BIT_CHECKER, 

99 comment="Q1 (day) (0-1)" 

100 ) 

101 q2 = CamcopsColumn( 

102 "q2", Integer, 

103 permitted_value_checker=BIT_CHECKER, 

104 comment="Q2 (year) (0-1)" 

105 ) 

106 q3 = CamcopsColumn( 

107 "q3", Integer, 

108 permitted_value_checker=BIT_CHECKER, 

109 comment="Q3 (state) (0-1)" 

110 ) 

111 q5a = CamcopsColumn( 

112 "q5a", Integer, 

113 permitted_value_checker=BIT_CHECKER, 

114 comment="Q5a (money spent) (0-1)" 

115 ) 

116 q5b = CamcopsColumn( 

117 "q5b", Integer, 

118 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

119 comment="Q5b (money left) (0 or 2)" 

120 ) # worth 2 points 

121 q6 = CamcopsColumn( 

122 "q6", Integer, 

123 permitted_value_checker=ZERO_TO_THREE_CHECKER, 

124 comment="Q6 (animal naming) (0-3)" 

125 ) # from 0 to 3 points 

126 q7a = CamcopsColumn( 

127 "q7a", Integer, 

128 permitted_value_checker=BIT_CHECKER, 

129 comment="Q7a (recall apple) (0-1)" 

130 ) 

131 q7b = CamcopsColumn( 

132 "q7b", Integer, 

133 permitted_value_checker=BIT_CHECKER, 

134 comment="Q7b (recall pen) (0-1)" 

135 ) 

136 q7c = CamcopsColumn( 

137 "q7c", Integer, 

138 permitted_value_checker=BIT_CHECKER, 

139 comment="Q7c (recall tie) (0-1)" 

140 ) 

141 q7d = CamcopsColumn( 

142 "q7d", Integer, 

143 permitted_value_checker=BIT_CHECKER, 

144 comment="Q7d (recall house) (0-1)" 

145 ) 

146 q7e = CamcopsColumn( 

147 "q7e", Integer, 

148 permitted_value_checker=BIT_CHECKER, 

149 comment="Q7e (recall car) (0-1)" 

150 ) 

151 q8b = CamcopsColumn( 

152 "q8b", Integer, 

153 permitted_value_checker=BIT_CHECKER, 

154 comment="Q8b (reverse 648) (0-1)" 

155 ) 

156 q8c = CamcopsColumn( 

157 "q8c", Integer, 

158 permitted_value_checker=BIT_CHECKER, 

159 comment="Q8c (reverse 8537) (0-1)" 

160 ) 

161 q9a = CamcopsColumn( 

162 "q9a", Integer, 

163 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

164 comment="Q9a (clock - hour markers) (0 or 2)" 

165 ) # worth 2 points 

166 q9b = CamcopsColumn( 

167 "q9b", Integer, 

168 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

169 comment="Q9b (clock - time) (0 or 2)" 

170 ) # worth 2 points 

171 q10a = CamcopsColumn( 

172 "q10a", Integer, 

173 permitted_value_checker=BIT_CHECKER, 

174 comment="Q10a (X in triangle) (0-1)" 

175 ) 

176 q10b = CamcopsColumn( 

177 "q10b", Integer, 

178 permitted_value_checker=BIT_CHECKER, 

179 comment="Q10b (biggest figure) (0-1)" 

180 ) 

181 q11a = CamcopsColumn( 

182 "q11a", Integer, 

183 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

184 comment="Q11a (story - name) (0 or 2)" 

185 ) # worth 2 points 

186 q11b = CamcopsColumn( 

187 "q11b", Integer, 

188 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

189 comment="Q11b (story - occupation) (0 or 2)" 

190 ) # worth 2 points 

191 q11c = CamcopsColumn( 

192 "q11c", Integer, 

193 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

194 comment="Q11c (story - back to work) (0 or 2)" 

195 ) # worth 2 points 

196 q11d = CamcopsColumn( 

197 "q11d", Integer, 

198 permitted_value_checker=ZERO_OR_TWO_CHECKER, 

199 comment="Q11d (story - state) (0 or 2)" 

200 ) # worth 2 points 

201 

202 clockpicture_blobid = CamcopsColumn( 

203 "clockpicture_blobid", Integer, 

204 is_blob_id_field=True, blob_relationship_attr_name="clockpicture", 

205 comment="BLOB ID of clock picture" 

206 ) 

207 shapespicture_blobid = CamcopsColumn( 

208 "shapespicture_blobid", Integer, 

209 is_blob_id_field=True, blob_relationship_attr_name="shapespicture", 

210 comment="BLOB ID of shapes picture" 

211 ) 

212 comments = Column( 

213 "comments", UnicodeText, 

214 comment="Clinician's comments" 

215 ) 

216 

217 clockpicture = blob_relationship("Slums", "clockpicture_blobid") # type: Optional[Blob] # noqa 

218 shapespicture = blob_relationship("Slums", "shapespicture_blobid") # type: Optional[Blob] # noqa 

219 

220 PREAMBLE_FIELDS = ["alert", "highschooleducation"] 

221 SCORED_FIELDS = [ 

222 "q1", "q2", "q3", 

223 "q5a", "q5b", 

224 "q6", 

225 "q7a", "q7b", "q7c", "q7d", "q7e", 

226 "q8b", "q8c", 

227 "q9a", "q9b", 

228 "q10a", "q10b", 

229 "q11a", "q11b", "q11c", "q11d" 

230 ] 

231 MAX_SCORE = 30 

232 

233 @staticmethod 

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

235 _ = req.gettext 

236 return _("St Louis University Mental Status") 

237 

238 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

239 if self.highschooleducation == 1: 

240 hlines = [26.5, 20.5] 

241 y_upper = 28.25 

242 y_middle = 23.5 

243 else: 

244 hlines = [24.5, 19.5] 

245 y_upper = 27.25 

246 y_middle = 22 

247 return [TrackerInfo( 

248 value=self.total_score(), 

249 plot_label="SLUMS total score", 

250 axis_label=f"Total score (out of {self.MAX_SCORE})", 

251 axis_min=-0.5, 

252 axis_max=self.MAX_SCORE + 0.5, 

253 horizontal_lines=hlines, 

254 horizontal_labels=[ 

255 TrackerLabel(y_upper, req.sstring(SS.NORMAL)), 

256 TrackerLabel(y_middle, self.wxstring(req, "category_mci")), 

257 TrackerLabel(17, self.wxstring(req, "category_dementia")), 

258 ] 

259 )] 

260 

261 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

262 if not self.is_complete(): 

263 return CTV_INCOMPLETE 

264 return [CtvInfo( 

265 content=f"SLUMS total score {self.total_score()}/{self.MAX_SCORE} " 

266 f"({self.category(req)})" 

267 )] 

268 

269 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

270 return self.standard_task_summary_fields() + [ 

271 SummaryElement(name="total", 

272 coltype=Integer(), 

273 value=self.total_score(), 

274 comment=f"Total score (/{self.MAX_SCORE})"), 

275 SummaryElement(name="category", 

276 coltype=SummaryCategoryColType, 

277 value=self.category(req), 

278 comment="Category"), 

279 ] 

280 

281 def is_complete(self) -> bool: 

282 return ( 

283 self.all_fields_not_none(self.PREAMBLE_FIELDS + 

284 self.SCORED_FIELDS) and 

285 self.field_contents_valid() 

286 ) 

287 

288 def total_score(self) -> int: 

289 return self.sum_fields(self.SCORED_FIELDS) 

290 

291 def category(self, req: CamcopsRequest) -> str: 

292 score = self.total_score() 

293 if self.highschooleducation == 1: 

294 if score >= 27: 

295 return req.sstring(SS.NORMAL) 

296 elif score >= 21: 

297 return self.wxstring(req, "category_mci") 

298 else: 

299 return self.wxstring(req, "category_dementia") 

300 else: 

301 if score >= 25: 

302 return req.sstring(SS.NORMAL) 

303 elif score >= 20: 

304 return self.wxstring(req, "category_mci") 

305 else: 

306 return self.wxstring(req, "category_dementia") 

307 

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

309 score = self.total_score() 

310 category = self.category(req) 

311 h = """ 

312 {clinician_comments} 

313 <div class="{CssClass.SUMMARY}"> 

314 <table class="{CssClass.SUMMARY}"> 

315 {tr_is_complete} 

316 {total_score} 

317 {category} 

318 </table> 

319 </div> 

320 <table class="{CssClass.TASKDETAIL}"> 

321 <tr> 

322 <th width="80%">Question</th> 

323 <th width="20%">Score</th> 

324 </tr> 

325 """.format( 

326 clinician_comments=self.get_standard_clinician_comments_block( 

327 req, self.comments), 

328 CssClass=CssClass, 

329 tr_is_complete=self.get_is_complete_tr(req), 

330 total_score=tr( 

331 req.sstring(SS.TOTAL_SCORE), 

332 answer(score) + f" / {self.MAX_SCORE}" 

333 ), 

334 category=tr_qa( 

335 req.sstring(SS.CATEGORY) + " <sup>[1]</sup>", 

336 category 

337 ), 

338 ) 

339 h += tr_qa(self.wxstring(req, "alert_s"), 

340 get_yes_no_none(req, self.alert)) 

341 h += tr_qa(self.wxstring(req, "highschool_s"), 

342 get_yes_no_none(req, self.highschooleducation)) 

343 h += tr_qa(self.wxstring(req, "q1_s"), self.q1) 

344 h += tr_qa(self.wxstring(req, "q2_s"), self.q2) 

345 h += tr_qa(self.wxstring(req, "q3_s"), self.q3) 

346 h += tr("Q5 <sup>[2]</sup> (money spent, money left " 

347 "[<i>scores 2</i>]", 

348 ", ".join([answer(x) for x in [self.q5a, self.q5b]])) 

349 h += tr_qa("Q6 (animal fluency) [<i>≥15 scores 3, 10–14 scores 2, " 

350 "5–9 scores 1, 0–4 scores 0</i>]", 

351 self.q6) 

352 h += tr("Q7 (recall: apple, pen, tie, house, car)", 

353 ", ".join([answer(x) for x in [self.q7a, self.q7b, self.q7c, 

354 self.q7d, self.q7e]])) 

355 h += tr("Q8 (backwards: 648, 8537)", 

356 ", ".join([answer(x) for x in [self.q8b, self.q8c]])) 

357 h += tr("Q9 (clock: hour markers, time [<i>score 2 each</i>]", 

358 ", ".join([answer(x) for x in [self.q9a, self.q9b]])) 

359 h += tr("Q10 (X in triangle; which is biggest?)", 

360 ", ".join([answer(x) for x in [self.q10a, self.q10b]])) 

361 h += tr("Q11 (story: Female’s name? Job? When back to work? " 

362 "State she lived in? [<i>score 2 each</i>])", 

363 ", ".join([answer(x) for x in [self.q11a, self.q11b, 

364 self.q11c, self.q11d]])) 

365 h += f""" 

366 </table> 

367 <table class="{CssClass.TASKDETAIL}"> 

368 """ 

369 h += subheading_spanning_two_columns("Images of tests: clock, shapes") 

370 # noinspection PyTypeChecker 

371 h += tr( 

372 td(get_blob_img_html(self.clockpicture), 

373 td_width="50%", td_class=CssClass.PHOTO), 

374 td(get_blob_img_html(self.shapespicture), 

375 td_width="50%", td_class=CssClass.PHOTO), 

376 literal=True 

377 ) 

378 h += f""" 

379 </table> 

380 <div class="{CssClass.FOOTNOTES}"> 

381 [1] With high school education: 

382 ≥27 normal, ≥21 MCI, ≤20 dementia. 

383 Without high school education: 

384 ≥25 normal, ≥20 MCI, ≤19 dementia. 

385 (Tariq et al. 2006, PubMed ID 17068312.) 

386 [2] Q4 (learning the five words) isn’t scored. 

387 </div> 

388 """ 

389 return h