Coverage for tasks/smast.py: 49%

1"""

2 camcops_server/tasks/smast.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, List, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Integer

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE

35 from camcops_server.cc_modules.cc_db import add_multiple_columns

36 from camcops_server.cc_modules.cc_html import answer, 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_sqla_coltypes import (

40 CharColType,

41 SummaryCategoryColType,

42)

43 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

44 from camcops_server.cc_modules.cc_task import (

45 get_from_dict,

46 Task,

47 TaskHasPatientMixin,

48)

49 from camcops_server.cc_modules.cc_text import SS

50 from camcops_server.cc_modules.cc_trackerhelpers import (

51 TrackerLabel,

52 TrackerInfo,

53)

54

55

56# =============================================================================

57# SMAST

58# =============================================================================

59

60

61 class Smast( # type: ignore[misc]

62 TaskHasPatientMixin,

63 Task,

64):

65 """

66 Server implementation of the SMAST task.

67 """

68

69 __tablename__ = "smast"

70 shortname = "SMAST"

71 info_filename_stem = "mast"

72 provides_trackers = True

73

74 NQUESTIONS = 13

75

76 @classmethod

77 def extend_columns(cls: Type["Smast"], **kwargs: Any) -> None:

78 add_multiple_columns(

79 cls,

80 "q",

81 1,

82 cls.NQUESTIONS,

83 CharColType,

84 pv=["Y", "N"],

85 comment_fmt="Q{n}: {s} (Y or N)",

86 comment_strings=[

87 "believe you are a normal drinker",

88 "near relative worries/complains",

89 "feel guilty",

90 "friends/relative think you are a normal drinker",

91 "stop when you want to",

92 "ever attended Alcoholics Anonymous",

93 "problems with close relative",

94 "trouble at work",

95 "neglected obligations for >=2 days",

96 "sought help",

97 "hospitalized",

98 "arrested for drink-driving",

99 "arrested for other drunken behaviour",

100 ],

101 )

102

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

104

105 @staticmethod

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

107 _ = req.gettext

108 return _("Short Michigan Alcohol Screening Test")

109

110 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:

111 return [

112 TrackerInfo(

113 value=self.total_score(),

114 plot_label="SMAST total score",

115 axis_label=f"Total score (out of {self.NQUESTIONS})",

116 axis_min=-0.5,

117 axis_max=self.NQUESTIONS + 0.5,

118 horizontal_lines=[2.5, 1.5],

119 horizontal_labels=[

120 TrackerLabel(4, self.wxstring(req, "problem_probable")),

121 TrackerLabel(2, self.wxstring(req, "problem_possible")),

122 TrackerLabel(0.75, self.wxstring(req, "problem_unlikely")),

123 ],

124 )

125 ]

126

127 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:

128 if not self.is_complete():

129 return CTV_INCOMPLETE

130 return [

131 CtvInfo(

132 content=(

133 f"SMAST total score "

134 f"{self.total_score()}/{self.NQUESTIONS} "

135 f"({self.likelihood(req)})"

136 )

137 )

138 ]

139

140 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:

141 return self.standard_task_summary_fields() + [

142 SummaryElement(

143 name="total",

144 coltype=Integer(),

145 value=self.total_score(),

146 comment=f"Total score (/{self.NQUESTIONS})",

147 ),

148 SummaryElement(

149 name="likelihood",

150 coltype=SummaryCategoryColType,

151 value=self.likelihood(req),

152 comment="Likelihood of problem",

153 ),

154 ]

155

156 def is_complete(self) -> bool:

157 return (

158 self.all_fields_not_none(self.TASK_FIELDS)

159 and self.field_contents_valid()

160 )

161

162 def get_score(self, q: int) -> int:

163 yes = "Y"

164 value = getattr(self, "q" + str(q))

165 if value is None:

166 return 0

167 if q == 1 or q == 4 or q == 5:

168 return 0 if value == yes else 1

169 else:

170 return 1 if value == yes else 0

171

172 def total_score(self) -> int:

173 total = 0

174 for q in range(1, self.NQUESTIONS + 1):

175 total += self.get_score(q)

176 return total

177

178 def likelihood(self, req: CamcopsRequest) -> str:

179 score = self.total_score()

180 if score >= 3:

181 return self.wxstring(req, "problem_probable")

182 elif score >= 2:

183 return self.wxstring(req, "problem_possible")

184 else:

185 return self.wxstring(req, "problem_unlikely")

186

187 def get_task_html(self, req: CamcopsRequest) -> str:

188 score = self.total_score()

189 likelihood = self.likelihood(req)

190 main_dict = {

191 None: None,

192 "Y": req.sstring(SS.YES),

193 "N": req.sstring(SS.NO),

194 }

195 q_a = ""

196 for q in range(1, self.NQUESTIONS + 1):

197 q_a += tr(

198 self.wxstring(req, "q" + str(q)),

199 answer(get_from_dict(main_dict, getattr(self, "q" + str(q))))

200 + " — "

201 + str(self.get_score(q)),

202 )

203 h = """

204 <div class="{CssClass.SUMMARY}">

205 <table class="{CssClass.SUMMARY}">

206 {tr_is_complete}

207 {total_score}

208 {problem_likelihood}

209 </table>

210 </div>

211 <table class="{CssClass.TASKDETAIL}">

212 <tr>

213 <th width="80%">Question</th>

214 <th width="20%">Answer</th>

215 </tr>

216 {q_a}

217 </table>

218 <div class="{CssClass.FOOTNOTES}">

219 [1] Total score ≥3 probable, ≥2 possible, 0–1 unlikely.

220 </div>

221 """.format(

222 CssClass=CssClass,

223 tr_is_complete=self.get_is_complete_tr(req),

224 total_score=tr(

225 req.sstring(SS.TOTAL_SCORE),

226 answer(score) + f" / {self.NQUESTIONS}",

227 ),

228 problem_likelihood=tr_qa(

229 self.wxstring(req, "problem_likelihood") + " <sup>[1]</sup>",

230 likelihood,

231 ),

232 q_a=q_a,

233 )

234 return h

235

236 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:

237 if not self.is_complete():

238 return []

239 return [SnomedExpression(req.snomed(SnomedLookup.SMAST_SCALE))]