Coverage for tasks/fast.py : 55%

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/fast.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 Boolean, 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 answer, get_yes_no, tr, tr_qa
39from camcops_server.cc_modules.cc_request import CamcopsRequest
40from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
41from camcops_server.cc_modules.cc_summaryelement import SummaryElement
42from camcops_server.cc_modules.cc_task import (
43 get_from_dict,
44 Task,
45 TaskHasPatientMixin,
46)
47from camcops_server.cc_modules.cc_text import SS
48from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
51# =============================================================================
52# FAST
53# =============================================================================
55class FastMetaclass(DeclarativeMeta):
56 # noinspection PyInitNewSignature
57 def __init__(cls: Type['Fast'],
58 name: str,
59 bases: Tuple[Type, ...],
60 classdict: Dict[str, Any]) -> None:
61 add_multiple_columns(
62 cls, "q", 1, cls.NQUESTIONS,
63 minimum=0, maximum=4,
64 comment_fmt="Q{n}. {s} (0-4, higher worse)",
65 comment_strings=[
66 "M>8, F>6 drinks", "unable to remember",
67 "failed to do what was expected", "others concerned"
68 ]
69 )
70 super().__init__(name, bases, classdict)
73class Fast(TaskHasPatientMixin, Task,
74 metaclass=FastMetaclass):
75 """
76 Server implementation of the FAST task.
77 """
78 __tablename__ = "fast"
79 shortname = "FAST"
81 NQUESTIONS = 4
82 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
83 MAX_SCORE = 16
85 @staticmethod
86 def longname(req: "CamcopsRequest") -> str:
87 _ = req.gettext
88 return _("Fast Alcohol Screening Test")
90 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
91 return [TrackerInfo(
92 value=self.total_score(),
93 plot_label="FAST total score",
94 axis_label=f"Total score (out of {self.MAX_SCORE})",
95 axis_min=-0.5,
96 axis_max=self.MAX_SCORE + 0.5
97 )]
99 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
100 if not self.is_complete():
101 return CTV_INCOMPLETE
102 classification = "positive" if self.is_positive() else "negative"
103 return [CtvInfo(content=(
104 f"FAST total score {self.total_score()}/{self.MAX_SCORE} "
105 f"({classification})"
106 ))]
108 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
109 return self.standard_task_summary_fields() + [
110 SummaryElement(name="total",
111 coltype=Integer(),
112 value=self.total_score(),
113 comment=f"Total score (/{self.MAX_SCORE})"),
114 SummaryElement(name="positive",
115 coltype=Boolean(),
116 value=self.is_positive(),
117 comment="FAST positive?"),
118 ]
120 def is_complete(self) -> bool:
121 return (
122 self.all_fields_not_none(self.TASK_FIELDS) and
123 self.field_contents_valid()
124 )
126 def total_score(self) -> int:
127 return self.sum_fields(self.TASK_FIELDS)
129 # noinspection PyUnresolvedReferences
130 def is_positive(self) -> bool:
131 if self.q1 is not None:
132 if self.q1 == 0:
133 return False
134 if self.q1 >= 3:
135 return True
136 return self.total_score() >= 3
138 # noinspection PyUnresolvedReferences
139 def get_task_html(self, req: CamcopsRequest) -> str:
140 main_dict = {
141 None: None,
142 0: "0 — " + self.wxstring(req, "q1to3_option0"),
143 1: "1 — " + self.wxstring(req, "q1to3_option1"),
144 2: "2 — " + self.wxstring(req, "q1to3_option2"),
145 3: "3 — " + self.wxstring(req, "q1to3_option3"),
146 4: "4 — " + self.wxstring(req, "q1to3_option4"),
147 }
148 q4_dict = {
149 None: None,
150 0: "0 — " + self.wxstring(req, "q4_option0"),
151 2: "2 — " + self.wxstring(req, "q4_option2"),
152 4: "4 — " + self.wxstring(req, "q4_option4"),
153 }
154 q_a = tr_qa(self.wxstring(req, "q1"), get_from_dict(main_dict, self.q1)) # noqa
155 q_a += tr_qa(self.wxstring(req, "q2"), get_from_dict(main_dict, self.q2)) # noqa
156 q_a += tr_qa(self.wxstring(req, "q3"), get_from_dict(main_dict, self.q3)) # noqa
157 q_a += tr_qa(self.wxstring(req, "q4"), get_from_dict(q4_dict, self.q4))
159 tr_total_score = tr(
160 req.sstring(SS.TOTAL_SCORE),
161 answer(self.total_score()) + f" / {self.MAX_SCORE}"
162 )
163 tr_positive = tr_qa(
164 self.wxstring(req, "positive") + " <sup>[1]</sup>",
165 get_yes_no(req, self.is_positive())
166 )
167 return f"""
168 <div class="{CssClass.SUMMARY}">
169 <table class="{CssClass.SUMMARY}">
170 {self.get_is_complete_tr(req)}
171 {tr_total_score}
172 {tr_positive}
173 </table>
174 </div>
175 <table class="{CssClass.TASKDETAIL}">
176 <tr>
177 <th width="60%">Question</th>
178 <th width="40%">Answer</th>
179 </tr>
180 {q_a}
181 </table>
182 <div class="{CssClass.FOOTNOTES}">
183 [1] Negative if Q1 = 0. Positive if Q1 ≥ 3. Otherwise positive
184 if total score ≥ 3.
185 </div>
186 """
188 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
189 codes = [SnomedExpression(req.snomed(SnomedLookup.FAST_PROCEDURE_ASSESSMENT))] # noqa
190 if self.is_complete():
191 codes.append(SnomedExpression(
192 req.snomed(SnomedLookup.FAST_SCALE),
193 {
194 req.snomed(SnomedLookup.FAST_SCORE): self.total_score(),
195 }
196 ))
197 return codes