Coverage for tasks/slums.py: 55%
107 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/slums.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 List, Optional
32from sqlalchemy.sql.schema import Column
33from sqlalchemy.sql.sqltypes import Integer, UnicodeText
35from camcops_server.cc_modules.cc_blob import (
36 Blob,
37 blob_relationship,
38 get_blob_img_html,
39)
40from camcops_server.cc_modules.cc_constants import CssClass
41from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
42from camcops_server.cc_modules.cc_html import (
43 answer,
44 get_yes_no_none,
45 subheading_spanning_two_columns,
46 td,
47 tr,
48 tr_qa,
49)
50from camcops_server.cc_modules.cc_request import CamcopsRequest
51from camcops_server.cc_modules.cc_sqla_coltypes import (
52 BIT_CHECKER,
53 CamcopsColumn,
54 PermittedValueChecker,
55 SummaryCategoryColType,
56 ZERO_TO_THREE_CHECKER,
57)
58from camcops_server.cc_modules.cc_summaryelement import SummaryElement
59from camcops_server.cc_modules.cc_task import (
60 Task,
61 TaskHasClinicianMixin,
62 TaskHasPatientMixin,
63)
64from camcops_server.cc_modules.cc_text import SS
65from camcops_server.cc_modules.cc_trackerhelpers import (
66 TrackerInfo,
67 TrackerLabel,
68)
71# =============================================================================
72# SLUMS
73# =============================================================================
75ZERO_OR_TWO_CHECKER = PermittedValueChecker(permitted_values=[0, 2])
78class Slums(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
79 """
80 Server implementation of the SLUMS task.
81 """
83 __tablename__ = "slums"
84 shortname = "SLUMS"
85 provides_trackers = True
87 alert = CamcopsColumn(
88 "alert",
89 Integer,
90 permitted_value_checker=BIT_CHECKER,
91 comment="Is the patient alert? (0 no, 1 yes)",
92 )
93 highschooleducation = CamcopsColumn(
94 "highschooleducation",
95 Integer,
96 permitted_value_checker=BIT_CHECKER,
97 comment="Does that patient have at least a high-school level of "
98 "education? (0 no, 1 yes)",
99 )
101 q1 = CamcopsColumn(
102 "q1",
103 Integer,
104 permitted_value_checker=BIT_CHECKER,
105 comment="Q1 (day) (0-1)",
106 )
107 q2 = CamcopsColumn(
108 "q2",
109 Integer,
110 permitted_value_checker=BIT_CHECKER,
111 comment="Q2 (year) (0-1)",
112 )
113 q3 = CamcopsColumn(
114 "q3",
115 Integer,
116 permitted_value_checker=BIT_CHECKER,
117 comment="Q3 (state) (0-1)",
118 )
119 q5a = CamcopsColumn(
120 "q5a",
121 Integer,
122 permitted_value_checker=BIT_CHECKER,
123 comment="Q5a (money spent) (0-1)",
124 )
125 q5b = CamcopsColumn(
126 "q5b",
127 Integer,
128 permitted_value_checker=ZERO_OR_TWO_CHECKER,
129 comment="Q5b (money left) (0 or 2)",
130 ) # worth 2 points
131 q6 = CamcopsColumn(
132 "q6",
133 Integer,
134 permitted_value_checker=ZERO_TO_THREE_CHECKER,
135 comment="Q6 (animal naming) (0-3)",
136 ) # from 0 to 3 points
137 q7a = CamcopsColumn(
138 "q7a",
139 Integer,
140 permitted_value_checker=BIT_CHECKER,
141 comment="Q7a (recall apple) (0-1)",
142 )
143 q7b = CamcopsColumn(
144 "q7b",
145 Integer,
146 permitted_value_checker=BIT_CHECKER,
147 comment="Q7b (recall pen) (0-1)",
148 )
149 q7c = CamcopsColumn(
150 "q7c",
151 Integer,
152 permitted_value_checker=BIT_CHECKER,
153 comment="Q7c (recall tie) (0-1)",
154 )
155 q7d = CamcopsColumn(
156 "q7d",
157 Integer,
158 permitted_value_checker=BIT_CHECKER,
159 comment="Q7d (recall house) (0-1)",
160 )
161 q7e = CamcopsColumn(
162 "q7e",
163 Integer,
164 permitted_value_checker=BIT_CHECKER,
165 comment="Q7e (recall car) (0-1)",
166 )
167 q8b = CamcopsColumn(
168 "q8b",
169 Integer,
170 permitted_value_checker=BIT_CHECKER,
171 comment="Q8b (reverse 648) (0-1)",
172 )
173 q8c = CamcopsColumn(
174 "q8c",
175 Integer,
176 permitted_value_checker=BIT_CHECKER,
177 comment="Q8c (reverse 8537) (0-1)",
178 )
179 q9a = CamcopsColumn(
180 "q9a",
181 Integer,
182 permitted_value_checker=ZERO_OR_TWO_CHECKER,
183 comment="Q9a (clock - hour markers) (0 or 2)",
184 ) # worth 2 points
185 q9b = CamcopsColumn(
186 "q9b",
187 Integer,
188 permitted_value_checker=ZERO_OR_TWO_CHECKER,
189 comment="Q9b (clock - time) (0 or 2)",
190 ) # worth 2 points
191 q10a = CamcopsColumn(
192 "q10a",
193 Integer,
194 permitted_value_checker=BIT_CHECKER,
195 comment="Q10a (X in triangle) (0-1)",
196 )
197 q10b = CamcopsColumn(
198 "q10b",
199 Integer,
200 permitted_value_checker=BIT_CHECKER,
201 comment="Q10b (biggest figure) (0-1)",
202 )
203 q11a = CamcopsColumn(
204 "q11a",
205 Integer,
206 permitted_value_checker=ZERO_OR_TWO_CHECKER,
207 comment="Q11a (story - name) (0 or 2)",
208 ) # worth 2 points
209 q11b = CamcopsColumn(
210 "q11b",
211 Integer,
212 permitted_value_checker=ZERO_OR_TWO_CHECKER,
213 comment="Q11b (story - occupation) (0 or 2)",
214 ) # worth 2 points
215 q11c = CamcopsColumn(
216 "q11c",
217 Integer,
218 permitted_value_checker=ZERO_OR_TWO_CHECKER,
219 comment="Q11c (story - back to work) (0 or 2)",
220 ) # worth 2 points
221 q11d = CamcopsColumn(
222 "q11d",
223 Integer,
224 permitted_value_checker=ZERO_OR_TWO_CHECKER,
225 comment="Q11d (story - state) (0 or 2)",
226 ) # worth 2 points
228 clockpicture_blobid = CamcopsColumn(
229 "clockpicture_blobid",
230 Integer,
231 is_blob_id_field=True,
232 blob_relationship_attr_name="clockpicture",
233 comment="BLOB ID of clock picture",
234 )
235 shapespicture_blobid = CamcopsColumn(
236 "shapespicture_blobid",
237 Integer,
238 is_blob_id_field=True,
239 blob_relationship_attr_name="shapespicture",
240 comment="BLOB ID of shapes picture",
241 )
242 comments = Column("comments", UnicodeText, comment="Clinician's comments")
244 clockpicture = blob_relationship(
245 "Slums", "clockpicture_blobid"
246 ) # type: Optional[Blob] # noqa
247 shapespicture = blob_relationship(
248 "Slums", "shapespicture_blobid"
249 ) # type: Optional[Blob] # noqa
251 PREAMBLE_FIELDS = ["alert", "highschooleducation"]
252 SCORED_FIELDS = [
253 "q1",
254 "q2",
255 "q3",
256 "q5a",
257 "q5b",
258 "q6",
259 "q7a",
260 "q7b",
261 "q7c",
262 "q7d",
263 "q7e",
264 "q8b",
265 "q8c",
266 "q9a",
267 "q9b",
268 "q10a",
269 "q10b",
270 "q11a",
271 "q11b",
272 "q11c",
273 "q11d",
274 ]
275 MAX_SCORE = 30
277 @staticmethod
278 def longname(req: "CamcopsRequest") -> str:
279 _ = req.gettext
280 return _("St Louis University Mental Status")
282 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
283 if self.highschooleducation == 1:
284 hlines = [26.5, 20.5]
285 y_upper = 28.25
286 y_middle = 23.5
287 else:
288 hlines = [24.5, 19.5]
289 y_upper = 27.25
290 y_middle = 22
291 return [
292 TrackerInfo(
293 value=self.total_score(),
294 plot_label="SLUMS total score",
295 axis_label=f"Total score (out of {self.MAX_SCORE})",
296 axis_min=-0.5,
297 axis_max=self.MAX_SCORE + 0.5,
298 horizontal_lines=hlines,
299 horizontal_labels=[
300 TrackerLabel(y_upper, req.sstring(SS.NORMAL)),
301 TrackerLabel(y_middle, self.wxstring(req, "category_mci")),
302 TrackerLabel(17, self.wxstring(req, "category_dementia")),
303 ],
304 )
305 ]
307 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
308 if not self.is_complete():
309 return CTV_INCOMPLETE
310 return [
311 CtvInfo(
312 content=f"SLUMS total score "
313 f"{self.total_score()}/{self.MAX_SCORE} "
314 f"({self.category(req)})"
315 )
316 ]
318 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
319 return self.standard_task_summary_fields() + [
320 SummaryElement(
321 name="total",
322 coltype=Integer(),
323 value=self.total_score(),
324 comment=f"Total score (/{self.MAX_SCORE})",
325 ),
326 SummaryElement(
327 name="category",
328 coltype=SummaryCategoryColType,
329 value=self.category(req),
330 comment="Category",
331 ),
332 ]
334 def is_complete(self) -> bool:
335 return (
336 self.all_fields_not_none(self.PREAMBLE_FIELDS + self.SCORED_FIELDS)
337 and self.field_contents_valid()
338 )
340 def total_score(self) -> int:
341 return self.sum_fields(self.SCORED_FIELDS)
343 def category(self, req: CamcopsRequest) -> str:
344 score = self.total_score()
345 if self.highschooleducation == 1:
346 if score >= 27:
347 return req.sstring(SS.NORMAL)
348 elif score >= 21:
349 return self.wxstring(req, "category_mci")
350 else:
351 return self.wxstring(req, "category_dementia")
352 else:
353 if score >= 25:
354 return req.sstring(SS.NORMAL)
355 elif score >= 20:
356 return self.wxstring(req, "category_mci")
357 else:
358 return self.wxstring(req, "category_dementia")
360 def get_task_html(self, req: CamcopsRequest) -> str:
361 score = self.total_score()
362 category = self.category(req)
363 h = """
364 {clinician_comments}
365 <div class="{CssClass.SUMMARY}">
366 <table class="{CssClass.SUMMARY}">
367 {tr_is_complete}
368 {total_score}
369 {category}
370 </table>
371 </div>
372 <table class="{CssClass.TASKDETAIL}">
373 <tr>
374 <th width="80%">Question</th>
375 <th width="20%">Score</th>
376 </tr>
377 """.format(
378 clinician_comments=self.get_standard_clinician_comments_block(
379 req, self.comments
380 ),
381 CssClass=CssClass,
382 tr_is_complete=self.get_is_complete_tr(req),
383 total_score=tr(
384 req.sstring(SS.TOTAL_SCORE),
385 answer(score) + f" / {self.MAX_SCORE}",
386 ),
387 category=tr_qa(
388 req.sstring(SS.CATEGORY) + " <sup>[1]</sup>", category
389 ),
390 )
391 h += tr_qa(
392 self.wxstring(req, "alert_s"), get_yes_no_none(req, self.alert)
393 )
394 h += tr_qa(
395 self.wxstring(req, "highschool_s"),
396 get_yes_no_none(req, self.highschooleducation),
397 )
398 h += tr_qa(self.wxstring(req, "q1_s"), self.q1)
399 h += tr_qa(self.wxstring(req, "q2_s"), self.q2)
400 h += tr_qa(self.wxstring(req, "q3_s"), self.q3)
401 h += tr(
402 "Q5 <sup>[2]</sup> (money spent, money left " "[<i>scores 2</i>]",
403 ", ".join(answer(x) for x in (self.q5a, self.q5b)),
404 )
405 h += tr_qa(
406 "Q6 (animal fluency) [<i>≥15 scores 3, 10–14 scores 2, "
407 "5–9 scores 1, 0–4 scores 0</i>]",
408 self.q6,
409 )
410 h += tr(
411 "Q7 (recall: apple, pen, tie, house, car)",
412 ", ".join(
413 answer(x)
414 for x in (self.q7a, self.q7b, self.q7c, self.q7d, self.q7e)
415 ),
416 )
417 h += tr(
418 "Q8 (backwards: 648, 8537)",
419 ", ".join(answer(x) for x in (self.q8b, self.q8c)),
420 )
421 h += tr(
422 "Q9 (clock: hour markers, time [<i>score 2 each</i>]",
423 ", ".join(answer(x) for x in (self.q9a, self.q9b)),
424 )
425 h += tr(
426 "Q10 (X in triangle; which is biggest?)",
427 ", ".join(answer(x) for x in (self.q10a, self.q10b)),
428 )
429 h += tr(
430 "Q11 (story: Female’s name? Job? When back to work? "
431 "State she lived in? [<i>score 2 each</i>])",
432 ", ".join(
433 answer(x) for x in (self.q11a, self.q11b, self.q11c, self.q11d)
434 ),
435 )
436 h += f"""
437 </table>
438 <table class="{CssClass.TASKDETAIL}">
439 """
440 h += subheading_spanning_two_columns("Images of tests: clock, shapes")
441 # noinspection PyTypeChecker
442 h += tr(
443 td(
444 get_blob_img_html(self.clockpicture),
445 td_width="50%",
446 td_class=CssClass.PHOTO,
447 ),
448 td(
449 get_blob_img_html(self.shapespicture),
450 td_width="50%",
451 td_class=CssClass.PHOTO,
452 ),
453 literal=True,
454 )
455 h += f"""
456 </table>
457 <div class="{CssClass.FOOTNOTES}">
458 [1] With high school education:
459 ≥27 normal, ≥21 MCI, ≤20 dementia.
460 Without high school education:
461 ≥25 normal, ≥20 MCI, ≤19 dementia.
462 (Tariq et al. 2006, PubMed ID 17068312.)
463 [2] Q4 (learning the five words) isn’t scored.
464 </div>
465 """
466 return h