Coverage for tasks/mast.py: 49%

1"""

2 camcops_server/tasks/mast.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 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_sqla_coltypes import CharColType

40 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

41 from camcops_server.cc_modules.cc_task import (

42 get_from_dict,

43 Task,

44 TaskHasPatientMixin,

45)

46 from camcops_server.cc_modules.cc_text import SS

47 from camcops_server.cc_modules.cc_trackerhelpers import (

48 LabelAlignment,

49 TrackerInfo,

50 TrackerLabel,

51)

52

53

54# =============================================================================

55# MAST

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

57

58

59 class Mast( # type: ignore[misc]

60 TaskHasPatientMixin,

61 Task,

62):

63 """

64 Server implementation of the MAST task.

65 """

66

67 __tablename__ = "mast"

68 shortname = "MAST"

69 provides_trackers = True

70

71 NQUESTIONS = 24

72

73 @classmethod

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

75 add_multiple_columns(

76 cls,

77 "q",

78 1,

79 cls.NQUESTIONS,

80 CharColType,

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

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

83 comment_strings=[

84 "feel you are a normal drinker",

85 "couldn't remember evening before",

86 "relative worries/complains",

87 "stop drinking after 1-2 drinks",

88 "feel guilty",

89 "friends/relatives think you are a normal drinker",

90 "can stop drinking when you want",

91 "attended Alcoholics Anonymous",

92 "physical fights",

93 "drinking caused problems with relatives",

94 "family have sought help",

95 "lost friends",

96 "trouble at work/school",

97 "lost job",

98 "neglected obligations for >=2 days",

99 "drink before noon often",

100 "liver trouble",

101 "delirium tremens",

102 "sought help",

103 "hospitalized",

104 "psychiatry admission",

105 "clinic visit or professional help",

106 "arrested for drink-driving",

107 "arrested for other drunk behaviour",

108 ],

109 )

110

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

112 MAX_SCORE = 53

113 ROSS_THRESHOLD = 13

114

115 @staticmethod

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

117 _ = req.gettext

118 return _("Michigan Alcohol Screening Test")

119

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

121 return [

122 TrackerInfo(

123 value=self.total_score(),

124 plot_label="MAST total score",

125 axis_label=f"Total score (out of {self.MAX_SCORE})",

126 axis_min=-0.5,

127 axis_max=self.MAX_SCORE + 0.5,

128 horizontal_lines=[self.ROSS_THRESHOLD - 0.5],

129 horizontal_labels=[

130 TrackerLabel(

131 self.ROSS_THRESHOLD,

132 "Ross threshold",

133 LabelAlignment.bottom,

134 )

135 ],

136 )

137 ]

138

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

140 if not self.is_complete():

141 return CTV_INCOMPLETE

142 return [

143 CtvInfo(

144 content=f"MAST total score "

145 f"{self.total_score()}/{self.MAX_SCORE}"

146 )

147 ]

148

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

150 return self.standard_task_summary_fields() + [

151 SummaryElement(

152 name="total",

153 coltype=Integer(),

154 value=self.total_score(),

155 comment=f"Total score (/{self.MAX_SCORE})",

156 ),

157 SummaryElement(

158 name="exceeds_threshold",

159 coltype=Boolean(),

160 value=self.exceeds_ross_threshold(),

161 comment=f"Exceeds Ross threshold "

162 f"(total score >= {self.ROSS_THRESHOLD})",

163 ),

164 ]

165

166 def is_complete(self) -> bool:

167 return (

168 self.all_fields_not_none(self.TASK_FIELDS)

169 and self.field_contents_valid()

170 )

171

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

173 yes = "Y"

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

175 if value is None:

176 return 0

177 if q == 1 or q == 4 or q == 6 or q == 7:

178 presence = 0 if value == yes else 1

179 else:

180 presence = 1 if value == yes else 0

181 if q == 3 or q == 5 or q == 9 or q == 16:

182 points = 1

183 elif q == 8 or q == 19 or q == 20:

184 points = 5

185 else:

186 points = 2

187 return points * presence

188

189 def total_score(self) -> int:

190 total = 0

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

192 total += self.get_score(q)

193 return total

194

195 def exceeds_ross_threshold(self) -> bool:

196 score = self.total_score()

197 return score >= self.ROSS_THRESHOLD

198

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

200 score = self.total_score()

201 exceeds_threshold = self.exceeds_ross_threshold()

202 main_dict = {

203 None: None,

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

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

206 }

207 q_a = ""

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

209 q_a += tr(

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

211 (

212 answer(

213 get_from_dict(main_dict, getattr(self, "q" + str(q)))

214 )

215 + answer(" — " + str(self.get_score(q)))

216 ),

217 )

218 return f"""

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

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

221 {self.get_is_complete_tr(req)}

222 {tr(req.sstring(SS.TOTAL_SCORE),

223 answer(score) + " / {}".format(self.MAX_SCORE))}

224 {tr_qa(self.wxstring(req, "exceeds_threshold"),

225 get_yes_no(req, exceeds_threshold))}

226 </table>

227 </div>

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

229 <tr>

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

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

232 </tr>

233 {q_a}

234 </table>

235 """

236

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

238 codes = [

239 SnomedExpression(

240 req.snomed(SnomedLookup.MAST_PROCEDURE_ASSESSMENT)

241 )

242 ]

243 if self.is_complete():

244 codes.append(

245 SnomedExpression(

246 req.snomed(SnomedLookup.MAST_SCALE),

247 {req.snomed(SnomedLookup.MAST_SCORE): self.total_score()},

248 )

249 )

250 return codes