Coverage for tasks/badls.py: 58%

55 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/badls.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 

28from typing import Any, List, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Integer 

32 

33from camcops_server.cc_modules.cc_constants import ( 

34 CssClass, 

35 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

36) 

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, tr 

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 Task, 

46 TaskHasPatientMixin, 

47 TaskHasRespondentMixin, 

48) 

49 

50 

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

52# BADLS 

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

54 

55 

56class Badls( # type: ignore[misc] 

57 TaskHasPatientMixin, 

58 TaskHasRespondentMixin, 

59 Task, 

60): 

61 """ 

62 Server implementation of the BADLS task. 

63 """ 

64 

65 __tablename__ = "badls" 

66 shortname = "BADLS" 

67 provides_trackers = True 

68 

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

70 NQUESTIONS = 20 

71 QUESTION_SNIPPETS = [ 

72 "food", # 1 

73 "eating", 

74 "drink", 

75 "drinking", 

76 "dressing", # 5 

77 "hygiene", 

78 "teeth", 

79 "bath/shower", 

80 "toilet/commode", 

81 "transfers", # 10 

82 "mobility", 

83 "orientation: time", 

84 "orientation: space", 

85 "communication", 

86 "telephone", # 15 

87 "hosuework/gardening", 

88 "shopping", 

89 "finances", 

90 "games/hobbies", 

91 "transport", # 20 

92 ] 

93 

94 @classmethod 

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

96 add_multiple_columns( 

97 cls, 

98 "q", 

99 1, 

100 cls.NQUESTIONS, 

101 CharColType, 

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

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

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

105 comment_strings=cls.QUESTION_SNIPPETS, 

106 ) 

107 

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

109 

110 @staticmethod 

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

112 _ = req.gettext 

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

114 

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

116 return self.standard_task_summary_fields() + [ 

117 SummaryElement( 

118 name="total_score", 

119 coltype=Integer(), 

120 value=self.total_score(), 

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

122 ) 

123 ] 

124 

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

126 if not self.is_complete(): 

127 return CTV_INCOMPLETE 

128 return [ 

129 CtvInfo( 

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

131 self.total_score() 

132 ) 

133 ) 

134 ] 

135 

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

137 text_value = getattr(self, q) 

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

139 

140 def total_score(self) -> int: 

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

142 

143 def is_complete(self) -> bool: 

144 return ( 

145 self.field_contents_valid() 

146 and self.is_respondent_complete() 

147 and self.all_fields_not_none(self.TASK_FIELDS) 

148 ) 

149 

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

151 q_a = "" 

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

153 fieldname = "q" + str(q) 

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

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

156 atext = ( 

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

158 if q is not None 

159 else None 

160 ) 

161 score = self.score(fieldname) 

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

163 return f""" 

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

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

166 {self.get_is_complete_tr(req)} 

167 <tr> 

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

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

170 </td> 

171 </table> 

172 </div> 

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

174 <tr> 

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

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

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

178 </tr> 

179 {q_a} 

180 </table> 

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

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

183 </div> 

184 {DATA_COLLECTION_UNLESS_UPGRADED_DIV} 

185 """ 

186 

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

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

189 # SNOMED-CT codes in. 

190 codes = [ 

191 SnomedExpression( 

192 req.snomed(SnomedLookup.BADLS_PROCEDURE_ASSESSMENT) 

193 ) 

194 ] 

195 if self.is_complete(): 

196 codes.append( 

197 SnomedExpression( 

198 req.snomed(SnomedLookup.BADLS_SCALE), 

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

200 ) 

201 ) 

202 return codes