Coverage for tasks/dast.py: 54%

68 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/dast.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

42from camcops_server.cc_modules.cc_sqla_coltypes import CharColType 

43from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

44from camcops_server.cc_modules.cc_task import ( 

45 get_from_dict, 

46 Task, 

47 TaskHasPatientMixin, 

48) 

49from camcops_server.cc_modules.cc_text import SS 

50from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

51 

52 

53# ============================================================================= 

54# DAST 

55# ============================================================================= 

56 

57 

58class DastMetaclass(DeclarativeMeta): 

59 # noinspection PyInitNewSignature 

60 def __init__( 

61 cls: Type["Dast"], 

62 name: str, 

63 bases: Tuple[Type, ...], 

64 classdict: Dict[str, Any], 

65 ) -> None: 

66 add_multiple_columns( 

67 cls, 

68 "q", 

69 1, 

70 cls.NQUESTIONS, 

71 CharColType, 

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

73 comment_fmt='Q{n}. {s} ("+" = Y scores 1, "-" = N scores 1)', 

74 comment_strings=[ 

75 "non-medical drug use (+)", 

76 "abused prescription drugs (+)", 

77 "abused >1 drug at a time (+)", 

78 "get through week without drugs (-)", 

79 "stop when want to (-)", 

80 "abuse drugs continuously (+)", 

81 "try to limit to certain situations (-)", 

82 "blackouts/flashbacks (+)", 

83 "feel bad about drug abuse (-)", 

84 "spouse/parents complain (+)", 

85 "friends/relative know/suspect (+)", 

86 "caused problems with spouse (+)", 

87 "family sought help (+)", 

88 "lost friends (+)", 

89 "neglected family/missed work (+)", 

90 "trouble at work (+)", 

91 "lost job (+)", 

92 "fights under influence (+)", 

93 "arrested for unusual behaviour under influence (+)", 

94 "arrested for driving under influence (+)", 

95 "illegal activities to obtain (+)", 

96 "arrested for possession (+)", 

97 "withdrawal symptoms (+)", 

98 "medical problems (+)", 

99 "sought help (+)", 

100 "hospital for medical problems (+)", 

101 "drug treatment program (+)", 

102 "outpatient treatment for drug abuse (+)", 

103 ], 

104 ) 

105 super().__init__(name, bases, classdict) 

106 

107 

108class Dast(TaskHasPatientMixin, Task, metaclass=DastMetaclass): 

109 """ 

110 Server implementation of the DAST task. 

111 """ 

112 

113 __tablename__ = "dast" 

114 shortname = "DAST" 

115 provides_trackers = True 

116 

117 prohibits_commercial = True 

118 

119 NQUESTIONS = 28 

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

121 

122 @staticmethod 

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

124 _ = req.gettext 

125 return _("Drug Abuse Screening Test") 

126 

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

128 return [ 

129 TrackerInfo( 

130 value=self.total_score(), 

131 plot_label="DAST total score", 

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

133 axis_min=-0.5, 

134 axis_max=self.NQUESTIONS + 0.5, 

135 horizontal_lines=[10.5, 5.5], 

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"DAST total score " 

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

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="Total score", 

156 ) 

157 ] 

158 

159 def is_complete(self) -> bool: 

160 return ( 

161 self.all_fields_not_none(Dast.TASK_FIELDS) 

162 and self.field_contents_valid() 

163 ) 

164 

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

166 yes = "Y" 

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

168 if value is None: 

169 return 0 

170 if q == 4 or q == 5 or q == 7: 

171 return 0 if value == yes else 1 

172 else: 

173 return 1 if value == yes else 0 

174 

175 def total_score(self) -> int: 

176 total = 0 

177 for q in range(1, Dast.NQUESTIONS + 1): 

178 total += self.get_score(q) 

179 return total 

180 

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

182 score = self.total_score() 

183 exceeds_cutoff_1 = score >= 6 

184 exceeds_cutoff_2 = score >= 11 

185 main_dict = { 

186 None: None, 

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

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

189 } 

190 q_a = "" 

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

192 q_a += tr( 

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

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

195 + " — " 

196 + answer(str(self.get_score(q))), 

197 ) 

198 

199 h = """ 

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

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

202 {tr_is_complete} 

203 {total_score} 

204 {exceeds_standard_cutoff_1} 

205 {exceeds_standard_cutoff_2} 

206 </table> 

207 </div> 

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

209 <tr> 

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

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

212 </tr> 

213 {q_a} 

214 </table> 

215 <div class="{CssClass.COPYRIGHT}"> 

216 DAST: Copyright © Harvey A. Skinner and the Centre for 

217 Addiction and Mental Health, Toronto, Canada. 

218 Reproduced here under the permissions granted for 

219 NON-COMMERCIAL use only. You must obtain permission from the 

220 copyright holder for any other use. 

221 </div> 

222 """.format( 

223 CssClass=CssClass, 

224 tr_is_complete=self.get_is_complete_tr(req), 

225 total_score=tr( 

226 req.sstring(SS.TOTAL_SCORE), 

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

228 ), 

229 exceeds_standard_cutoff_1=tr_qa( 

230 self.wxstring(req, "exceeds_standard_cutoff_1"), 

231 get_yes_no(req, exceeds_cutoff_1), 

232 ), 

233 exceeds_standard_cutoff_2=tr_qa( 

234 self.wxstring(req, "exceeds_standard_cutoff_2"), 

235 get_yes_no(req, exceeds_cutoff_2), 

236 ), 

237 q_a=q_a, 

238 ) 

239 return h 

240 

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

242 if not self.is_complete(): 

243 return [] 

244 return [SnomedExpression(req.snomed(SnomedLookup.DAST_SCALE))]