Coverage for tasks/fast.py: 52%

1"""

2 camcops_server/tasks/fast.py

3

4===============================================================================

5

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

8

9 This file is part of CamCOPS.

10

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.

15

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.

20

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/>.

23

24===============================================================================

25

26"""

27

28 from typing import Any, cast, List, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Boolean, Integer

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

35 from camcops_server.cc_modules.cc_db import add_multiple_columns

36 from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa

37 from camcops_server.cc_modules.cc_request import CamcopsRequest

38 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

39 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

40 from camcops_server.cc_modules.cc_task import (

41 get_from_dict,

42 Task,

43 TaskHasPatientMixin,

44)

45 from camcops_server.cc_modules.cc_text import SS

46 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

47

48

49# =============================================================================

50# FAST

51# =============================================================================

52

53

54 class Fast( # type: ignore[misc]

55 TaskHasPatientMixin,

56 Task,

57):

58 """

59 Server implementation of the FAST task.

60 """

61

62 __tablename__ = "fast"

63 shortname = "FAST"

64

65 NQUESTIONS = 4

66

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 )

84

85 TASK_FIELDS = strseq("q", 1, NQUESTIONS)

86 MAX_SCORE = 16

87

88 @staticmethod

89 def longname(req: "CamcopsRequest") -> str:

90 _ = req.gettext

91 return _("Fast Alcohol Screening Test")

92

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 ]

103

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 ]

116

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 ]

132

133 def is_complete(self) -> bool:

134 return (

135 self.all_fields_not_none(self.TASK_FIELDS)

136 and self.field_contents_valid()

137 )

138

139 def total_score(self) -> int:

140 return cast(int, self.sum_fields(self.TASK_FIELDS))

141

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

150

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

177

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 """

206

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