Coverage for tasks/audit.py: 45%
120 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/audit.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 cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy.ext.declarative import DeclarativeMeta
34from sqlalchemy.sql.sqltypes import Integer
36from camcops_server.cc_modules.cc_constants import CssClass
37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
38from camcops_server.cc_modules.cc_db import add_multiple_columns
39from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa
40from camcops_server.cc_modules.cc_request import CamcopsRequest
41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
42from camcops_server.cc_modules.cc_summaryelement import SummaryElement
43from camcops_server.cc_modules.cc_task import (
44 get_from_dict,
45 Task,
46 TaskHasPatientMixin,
47)
48from camcops_server.cc_modules.cc_text import SS
49from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
52# =============================================================================
53# AUDIT
54# =============================================================================
57class AuditMetaclass(DeclarativeMeta):
58 # noinspection PyInitNewSignature
59 def __init__(
60 cls: Type["Audit"],
61 name: str,
62 bases: Tuple[Type, ...],
63 classdict: Dict[str, Any],
64 ) -> None:
65 add_multiple_columns(
66 cls,
67 "q",
68 1,
69 cls.NQUESTIONS,
70 minimum=0,
71 maximum=4,
72 comment_fmt="Q{n}, {s} (0-4, higher worse)",
73 comment_strings=[
74 "how often drink",
75 "drinks per day",
76 "how often six drinks",
77 "unable to stop",
78 "unable to do what was expected",
79 "eye opener",
80 "guilt",
81 "unable to remember",
82 "injuries",
83 "others concerned",
84 ],
85 )
86 super().__init__(name, bases, classdict)
89class Audit(TaskHasPatientMixin, Task, metaclass=AuditMetaclass):
90 """
91 Server implementation of the AUDIT task.
92 """
94 __tablename__ = "audit"
95 shortname = "AUDIT"
96 provides_trackers = True
98 prohibits_commercial = True
100 NQUESTIONS = 10
101 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
103 @staticmethod
104 def longname(req: "CamcopsRequest") -> str:
105 _ = req.gettext
106 return _("WHO Alcohol Use Disorders Identification Test")
108 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
109 return [
110 TrackerInfo(
111 value=self.total_score(),
112 plot_label="AUDIT total score",
113 axis_label="Total score (out of 40)",
114 axis_min=-0.5,
115 axis_max=40.5,
116 horizontal_lines=[7.5],
117 )
118 ]
120 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
121 if not self.is_complete():
122 return CTV_INCOMPLETE
123 return [CtvInfo(content=f"AUDIT total score {self.total_score()}/40")]
125 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
126 return self.standard_task_summary_fields() + [
127 SummaryElement(
128 name="total",
129 coltype=Integer(),
130 value=self.total_score(),
131 comment="Total score (/40)",
132 )
133 ]
135 # noinspection PyUnresolvedReferences
136 def is_complete(self) -> bool:
137 if not self.field_contents_valid():
138 return False
139 if self.q1 is None or self.q9 is None or self.q10 is None:
140 return False
141 if self.q1 == 0:
142 # Special limited-information completeness
143 return True
144 if (
145 self.q2 is not None
146 and self.q3 is not None
147 and (self.q2 + self.q3 == 0)
148 ):
149 # Special limited-information completeness
150 return True
151 # Otherwise, any null values cause problems
152 return self.all_fields_not_none(self.TASK_FIELDS)
154 def total_score(self) -> int:
155 return self.sum_fields(self.TASK_FIELDS)
157 # noinspection PyUnresolvedReferences
158 def get_task_html(self, req: CamcopsRequest) -> str:
159 score = self.total_score()
160 exceeds_cutoff = score >= 8
161 q1_dict = {None: None}
162 q2_dict = {None: None}
163 q3_to_8_dict = {None: None}
164 q9_to_10_dict = {None: None}
165 for option in range(0, 5):
166 q1_dict[option] = (
167 str(option)
168 + " – "
169 + self.wxstring(req, "q1_option" + str(option))
170 )
171 q2_dict[option] = (
172 str(option)
173 + " – "
174 + self.wxstring(req, "q2_option" + str(option))
175 )
176 q3_to_8_dict[option] = (
177 str(option)
178 + " – "
179 + self.wxstring(req, "q3to8_option" + str(option))
180 )
181 if option != 1 and option != 3:
182 q9_to_10_dict[option] = (
183 str(option)
184 + " – "
185 + self.wxstring(req, "q9to10_option" + str(option))
186 )
188 q_a = tr_qa(
189 self.wxstring(req, "q1_s"), get_from_dict(q1_dict, self.q1)
190 )
191 q_a += tr_qa(
192 self.wxstring(req, "q2_s"), get_from_dict(q2_dict, self.q2)
193 )
194 for q in range(3, 8 + 1):
195 q_a += tr_qa(
196 self.wxstring(req, "q" + str(q) + "_s"),
197 get_from_dict(q3_to_8_dict, getattr(self, "q" + str(q))),
198 )
199 q_a += tr_qa(
200 self.wxstring(req, "q9_s"), get_from_dict(q9_to_10_dict, self.q9)
201 )
202 q_a += tr_qa(
203 self.wxstring(req, "q10_s"), get_from_dict(q9_to_10_dict, self.q10)
204 )
206 return f"""
207 <div class="{CssClass.SUMMARY}">
208 <table class="{CssClass.SUMMARY}">
209 {self.get_is_complete_tr(req)}
210 {tr(req.wsstring(SS.TOTAL_SCORE),
211 answer(score) + " / 40")}
212 {tr_qa(self.wxstring(req, "exceeds_standard_cutoff"),
213 get_yes_no(req, exceeds_cutoff))}
214 </table>
215 </div>
216 <table class="{CssClass.TASKDETAIL}">
217 <tr>
218 <th width="50%">Question</th>
219 <th width="50%">Answer</th>
220 </tr>
221 {q_a}
222 </table>
223 <div class="{CssClass.COPYRIGHT}">
224 AUDIT: Copyright © World Health Organization.
225 Reproduced here under the permissions granted for
226 NON-COMMERCIAL use only. You must obtain permission from the
227 copyright holder for any other use.
228 </div>
229 """
231 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
232 codes = [
233 SnomedExpression(
234 req.snomed(SnomedLookup.AUDIT_PROCEDURE_ASSESSMENT)
235 )
236 ]
237 if self.is_complete():
238 codes.append(
239 SnomedExpression(
240 req.snomed(SnomedLookup.AUDIT_SCALE),
241 {req.snomed(SnomedLookup.AUDIT_SCORE): self.total_score()},
242 )
243 )
244 return codes
247# =============================================================================
248# AUDIT-C
249# =============================================================================
252class AuditCMetaclass(DeclarativeMeta):
253 # noinspection PyInitNewSignature
254 def __init__(
255 cls: Type["AuditC"],
256 name: str,
257 bases: Tuple[Type, ...],
258 classdict: Dict[str, Any],
259 ) -> None:
260 add_multiple_columns(
261 cls,
262 "q",
263 1,
264 cls.NQUESTIONS,
265 minimum=0,
266 maximum=4,
267 comment_fmt="Q{n}, {s} (0-4, higher worse)",
268 comment_strings=[
269 "how often drink",
270 "drinks per day",
271 "how often six drinks",
272 ],
273 )
274 super().__init__(name, bases, classdict)
277class AuditC(TaskHasPatientMixin, Task, metaclass=AuditMetaclass):
278 __tablename__ = "audit_c"
279 shortname = "AUDIT-C"
280 extrastring_taskname = "audit" # shares strings with AUDIT
281 info_filename_stem = extrastring_taskname
283 prohibits_commercial = True
285 NQUESTIONS = 3
286 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
288 @staticmethod
289 def longname(req: "CamcopsRequest") -> str:
290 _ = req.gettext
291 return _("AUDIT Alcohol Consumption Questions")
293 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
294 return [
295 TrackerInfo(
296 value=self.total_score(),
297 plot_label="AUDIT-C total score",
298 axis_label="Total score (out of 12)",
299 axis_min=-0.5,
300 axis_max=12.5,
301 )
302 ]
304 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
305 if not self.is_complete():
306 return CTV_INCOMPLETE
307 return [
308 CtvInfo(content=f"AUDIT-C total score {self.total_score()}/12")
309 ]
311 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
312 return self.standard_task_summary_fields() + [
313 SummaryElement(
314 name="total",
315 coltype=Integer(),
316 value=self.total_score(),
317 comment="Total score (/12)",
318 )
319 ]
321 def is_complete(self) -> bool:
322 return self.all_fields_not_none(self.TASK_FIELDS)
324 def total_score(self) -> int:
325 return self.sum_fields(self.TASK_FIELDS)
327 def get_task_html(self, req: CamcopsRequest) -> str:
328 score = self.total_score()
329 q1_dict = {None: None}
330 q2_dict = {None: None}
331 q3_dict = {None: None}
332 for option in range(0, 5):
333 q1_dict[option] = (
334 str(option)
335 + " – "
336 + self.wxstring(req, "q1_option" + str(option))
337 )
338 if option == 0: # special!
339 q2_dict[option] = (
340 str(option) + " – " + self.wxstring(req, "c_q2_option0")
341 )
342 else:
343 q2_dict[option] = (
344 str(option)
345 + " – "
346 + self.wxstring(req, "q2_option" + str(option))
347 )
348 q3_dict[option] = (
349 str(option)
350 + " – "
351 + self.wxstring(req, "q3to8_option" + str(option))
352 )
354 # noinspection PyUnresolvedReferences
355 return f"""
356 <div class="{CssClass.SUMMARY}">
357 <table class="{CssClass.SUMMARY}">
358 {self.get_is_complete_tr(req)}
359 {tr(req.sstring(SS.TOTAL_SCORE),
360 answer(score) + " / 12")}
361 </table>
362 </div>
363 <table class="{CssClass.TASKDETAIL}">
364 <tr>
365 <th width="50%">Question</th>
366 <th width="50%">Answer</th>
367 </tr>
368 {tr_qa(self.wxstring(req, "c_q1_question"),
369 get_from_dict(q1_dict, self.q1))}
370 {tr_qa(self.wxstring(req, "c_q2_question"),
371 get_from_dict(q2_dict, self.q2))}
372 {tr_qa(self.wxstring(req, "c_q3_question"),
373 get_from_dict(q3_dict, self.q3))}
374 </table>
375 <div class="{CssClass.COPYRIGHT}">
376 AUDIT: Copyright © World Health Organization.
377 Reproduced here under the permissions granted for
378 NON-COMMERCIAL use only. You must obtain permission from the
379 copyright holder for any other use.
381 AUDIT-C: presumed to have the same restrictions.
382 </div>
383 """
385 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
386 codes = [
387 SnomedExpression(
388 req.snomed(SnomedLookup.AUDITC_PROCEDURE_ASSESSMENT)
389 )
390 ]
391 if self.is_complete():
392 codes.append(
393 SnomedExpression(
394 req.snomed(SnomedLookup.AUDITC_SCALE),
395 {
396 req.snomed(
397 SnomedLookup.AUDITC_SCORE
398 ): self.total_score()
399 },
400 )
401 )
402 return codes