Coverage for tasks/badls.py: 58%

1"""

2 camcops_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

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 (

34 CssClass,

35 DATA_COLLECTION_UNLESS_UPGRADED_DIV,

36)

37 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

38 from camcops_server.cc_modules.cc_db import add_multiple_columns

39 from camcops_server.cc_modules.cc_html import answer, tr

40 from camcops_server.cc_modules.cc_request import CamcopsRequest

41 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

42 from camcops_server.cc_modules.cc_sqla_coltypes import CharColType

43 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

44 from camcops_server.cc_modules.cc_task import (

45 Task,

46 TaskHasPatientMixin,

47 TaskHasRespondentMixin,

48)

49

50

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

52# BADLS

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

54

55

56 class 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