Coverage for tasks/badls.py: 60%

58 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/badls.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 ( 

37 CssClass, 

38 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

39) 

40from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

41from camcops_server.cc_modules.cc_db import add_multiple_columns 

42from camcops_server.cc_modules.cc_html import answer, tr 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

45from camcops_server.cc_modules.cc_sqla_coltypes import CharColType 

46from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

47from camcops_server.cc_modules.cc_task import ( 

48 Task, 

49 TaskHasPatientMixin, 

50 TaskHasRespondentMixin, 

51) 

52 

53 

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

55# BADLS 

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

57 

58 

59class BadlsMetaclass(DeclarativeMeta): 

60 # noinspection PyInitNewSignature 

61 def __init__( 

62 cls: Type["Badls"], 

63 name: str, 

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

65 classdict: Dict[str, Any], 

66 ) -> None: 

67 add_multiple_columns( 

68 cls, 

69 "q", 

70 1, 

71 cls.NQUESTIONS, 

72 CharColType, 

73 comment_fmt="Q{n}, {s} ('a' best [0] to 'd' worst [3]; " 

74 "'e'=N/A [scored 0])", 

75 pv=list(cls.SCORING.keys()), 

76 comment_strings=cls.QUESTION_SNIPPETS, 

77 ) 

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

79 

80 

81class Badls( 

82 TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=BadlsMetaclass 

83): 

84 """ 

85 Server implementation of the BADLS task. 

86 """ 

87 

88 __tablename__ = "badls" 

89 shortname = "BADLS" 

90 provides_trackers = True 

91 

92 SCORING = {"a": 0, "b": 1, "c": 2, "d": 3, "e": 0} 

93 NQUESTIONS = 20 

94 QUESTION_SNIPPETS = [ 

95 "food", # 1 

96 "eating", 

97 "drink", 

98 "drinking", 

99 "dressing", # 5 

100 "hygiene", 

101 "teeth", 

102 "bath/shower", 

103 "toilet/commode", 

104 "transfers", # 10 

105 "mobility", 

106 "orientation: time", 

107 "orientation: space", 

108 "communication", 

109 "telephone", # 15 

110 "hosuework/gardening", 

111 "shopping", 

112 "finances", 

113 "games/hobbies", 

114 "transport", # 20 

115 ] 

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

117 

118 @staticmethod 

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

120 _ = req.gettext 

121 return _("Bristol Activities of Daily Living Scale") 

122 

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

124 return self.standard_task_summary_fields() + [ 

125 SummaryElement( 

126 name="total_score", 

127 coltype=Integer(), 

128 value=self.total_score(), 

129 comment="Total score (/ 48)", 

130 ) 

131 ] 

132 

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

134 if not self.is_complete(): 

135 return CTV_INCOMPLETE 

136 return [ 

137 CtvInfo( 

138 content="BADLS total score {}/60 (lower is better)".format( 

139 self.total_score() 

140 ) 

141 ) 

142 ] 

143 

144 def score(self, q: str) -> int: 

145 text_value = getattr(self, q) 

146 return self.SCORING.get(text_value, 0) 

147 

148 def total_score(self) -> int: 

149 return sum(self.score(q) for q in self.TASK_FIELDS) 

150 

151 def is_complete(self) -> bool: 

152 return ( 

153 self.field_contents_valid() 

154 and self.is_respondent_complete() 

155 and self.all_fields_not_none(self.TASK_FIELDS) 

156 ) 

157 

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

159 q_a = "" 

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

161 fieldname = "q" + str(q) 

162 qtext = self.wxstring(req, fieldname) # happens to be the same 

163 avalue = getattr(self, "q" + str(q)) 

164 atext = ( 

165 self.wxstring(req, "q{}_{}".format(q, avalue)) 

166 if q is not None 

167 else None 

168 ) 

169 score = self.score(fieldname) 

170 q_a += tr(qtext, answer(atext), score) 

171 return f""" 

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

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

174 {self.get_is_complete_tr(req)} 

175 <tr> 

176 <td>Total score (0–60, higher worse)</td> 

177 <td>{answer(self.total_score())}</td> 

178 </td> 

179 </table> 

180 </div> 

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

182 <tr> 

183 <th width="30%">Question</th> 

184 <th width="50%">Answer <sup>[1]</sup></th> 

185 <th width="20%">Score</th> 

186 </tr> 

187 {q_a} 

188 </table> 

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

190 [1] Scored a = 0, b = 1, c = 2, d = 3, e = 0. 

191 </div> 

192 {DATA_COLLECTION_UNLESS_UPGRADED_DIV} 

193 """ 

194 

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

196 # The BADLS is ALWAYS carer-rated, so it's appropriate to put the 

197 # SNOMED-CT codes in. 

198 codes = [ 

199 SnomedExpression( 

200 req.snomed(SnomedLookup.BADLS_PROCEDURE_ASSESSMENT) 

201 ) 

202 ] 

203 if self.is_complete(): 

204 codes.append( 

205 SnomedExpression( 

206 req.snomed(SnomedLookup.BADLS_SCALE), 

207 {req.snomed(SnomedLookup.BADLS_SCORE): self.total_score()}, 

208 ) 

209 ) 

210 return codes