Coverage for tasks/fast.py: 52%
61 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/fast.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26"""
28from typing import Any, cast, List, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.sql.sqltypes import Boolean, Integer
33from camcops_server.cc_modules.cc_constants import CssClass
34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
35from camcops_server.cc_modules.cc_db import add_multiple_columns
36from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa
37from camcops_server.cc_modules.cc_request import CamcopsRequest
38from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
39from camcops_server.cc_modules.cc_summaryelement import SummaryElement
40from camcops_server.cc_modules.cc_task import (
41 get_from_dict,
42 Task,
43 TaskHasPatientMixin,
44)
45from camcops_server.cc_modules.cc_text import SS
46from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
49# =============================================================================
50# FAST
51# =============================================================================
54class Fast( # type: ignore[misc]
55 TaskHasPatientMixin,
56 Task,
57):
58 """
59 Server implementation of the FAST task.
60 """
62 __tablename__ = "fast"
63 shortname = "FAST"
65 NQUESTIONS = 4
67 @classmethod
68 def extend_columns(cls: Type["Fast"], **kwargs: Any) -> None:
69 add_multiple_columns(
70 cls,
71 "q",
72 1,
73 cls.NQUESTIONS,
74 minimum=0,
75 maximum=4,
76 comment_fmt="Q{n}. {s} (0-4, higher worse)",
77 comment_strings=[
78 "M>8, F>6 drinks",
79 "unable to remember",
80 "failed to do what was expected",
81 "others concerned",
82 ],
83 )
85 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
86 MAX_SCORE = 16
88 @staticmethod
89 def longname(req: "CamcopsRequest") -> str:
90 _ = req.gettext
91 return _("Fast Alcohol Screening Test")
93 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
94 return [
95 TrackerInfo(
96 value=self.total_score(),
97 plot_label="FAST total score",
98 axis_label=f"Total score (out of {self.MAX_SCORE})",
99 axis_min=-0.5,
100 axis_max=self.MAX_SCORE + 0.5,
101 )
102 ]
104 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
105 if not self.is_complete():
106 return CTV_INCOMPLETE
107 classification = "positive" if self.is_positive() else "negative"
108 return [
109 CtvInfo(
110 content=(
111 f"FAST total score {self.total_score()}/{self.MAX_SCORE} "
112 f"({classification})"
113 )
114 )
115 ]
117 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
118 return self.standard_task_summary_fields() + [
119 SummaryElement(
120 name="total",
121 coltype=Integer(),
122 value=self.total_score(),
123 comment=f"Total score (/{self.MAX_SCORE})",
124 ),
125 SummaryElement(
126 name="positive",
127 coltype=Boolean(),
128 value=self.is_positive(),
129 comment="FAST positive?",
130 ),
131 ]
133 def is_complete(self) -> bool:
134 return (
135 self.all_fields_not_none(self.TASK_FIELDS)
136 and self.field_contents_valid()
137 )
139 def total_score(self) -> int:
140 return cast(int, self.sum_fields(self.TASK_FIELDS))
142 # noinspection PyUnresolvedReferences
143 def is_positive(self) -> bool:
144 if self.q1 is not None: # type: ignore[attr-defined]
145 if self.q1 == 0: # type: ignore[attr-defined]
146 return False
147 if self.q1 >= 3: # type: ignore[attr-defined]
148 return True
149 return self.total_score() >= 3
151 # noinspection PyUnresolvedReferences
152 def get_task_html(self, req: CamcopsRequest) -> str:
153 main_dict = {
154 None: None,
155 0: "0 — " + self.wxstring(req, "q1to3_option0"),
156 1: "1 — " + self.wxstring(req, "q1to3_option1"),
157 2: "2 — " + self.wxstring(req, "q1to3_option2"),
158 3: "3 — " + self.wxstring(req, "q1to3_option3"),
159 4: "4 — " + self.wxstring(req, "q1to3_option4"),
160 }
161 q4_dict = {
162 None: None,
163 0: "0 — " + self.wxstring(req, "q4_option0"),
164 2: "2 — " + self.wxstring(req, "q4_option2"),
165 4: "4 — " + self.wxstring(req, "q4_option4"),
166 }
167 q_a = tr_qa(
168 self.wxstring(req, "q1"), get_from_dict(main_dict, self.q1) # type: ignore[attr-defined] # noqa: E501
169 )
170 q_a += tr_qa(
171 self.wxstring(req, "q2"), get_from_dict(main_dict, self.q2) # type: ignore[attr-defined] # noqa: E501
172 )
173 q_a += tr_qa(
174 self.wxstring(req, "q3"), get_from_dict(main_dict, self.q3) # type: ignore[attr-defined] # noqa: E501
175 )
176 q_a += tr_qa(self.wxstring(req, "q4"), get_from_dict(q4_dict, self.q4)) # type: ignore[attr-defined] # noqa: E501
178 tr_total_score = tr(
179 req.sstring(SS.TOTAL_SCORE),
180 answer(self.total_score()) + f" / {self.MAX_SCORE}",
181 )
182 tr_positive = tr_qa(
183 self.wxstring(req, "positive") + " <sup>[1]</sup>",
184 get_yes_no(req, self.is_positive()),
185 )
186 return f"""
187 <div class="{CssClass.SUMMARY}">
188 <table class="{CssClass.SUMMARY}">
189 {self.get_is_complete_tr(req)}
190 {tr_total_score}
191 {tr_positive}
192 </table>
193 </div>
194 <table class="{CssClass.TASKDETAIL}">
195 <tr>
196 <th width="60%">Question</th>
197 <th width="40%">Answer</th>
198 </tr>
199 {q_a}
200 </table>
201 <div class="{CssClass.FOOTNOTES}">
202 [1] Negative if Q1 = 0. Positive if Q1 ≥ 3. Otherwise positive
203 if total score ≥ 3.
204 </div>
205 """
207 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
208 codes = [
209 SnomedExpression(
210 req.snomed(SnomedLookup.FAST_PROCEDURE_ASSESSMENT)
211 )
212 ]
213 if self.is_complete():
214 codes.append(
215 SnomedExpression(
216 req.snomed(SnomedLookup.FAST_SCALE),
217 {req.snomed(SnomedLookup.FAST_SCORE): self.total_score()},
218 )
219 )
220 return codes