Coverage for tasks/audit.py : 45%

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