Coverage for tasks/aims.py: 59%

1"""

2 camcops_server/tasks/aims.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, cast, List, Optional, 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 CssClass, PV

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 (

37 answer,

38 get_yes_no_none,

39 tr,

40 tr_qa,

41)

42 from camcops_server.cc_modules.cc_request import CamcopsRequest

43 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

44 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

45 from camcops_server.cc_modules.cc_task import (

46 get_from_dict,

47 Task,

48 TaskHasClinicianMixin,

49 TaskHasPatientMixin,

50)

51 from camcops_server.cc_modules.cc_text import SS

52 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

53

54

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

56# AIMS

57# =============================================================================

58

59

60 class Aims( # type: ignore[misc]

61 TaskHasPatientMixin,

62 TaskHasClinicianMixin,

63 Task,

64):

65 """

66 Server implementation of the AIMS task.

67 """

68

69 __tablename__ = "aims"

70 shortname = "AIMS"

71 provides_trackers = True

72

73 NQUESTIONS = 12

74 NSCOREDQUESTIONS = 10

75

76 @classmethod

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

78 add_multiple_columns(

79 cls,

80 "q",

81 1,

82 cls.NSCOREDQUESTIONS,

83 minimum=0,

84 maximum=4,

85 comment_fmt="Q{n}, {s} (0 none - 4 severe)",

86 comment_strings=[

87 "facial_expression",

88 "lips",

89 "jaw",

90 "tongue",

91 "upper_limbs",

92 "lower_limbs",

93 "trunk",

94 "global",

95 "incapacitation",

96 "awareness",

97 ],

98 )

99 add_multiple_columns(

100 cls,

101 "q",

102 cls.NSCOREDQUESTIONS + 1,

103 cls.NQUESTIONS,

104 pv=PV.BIT,

105 comment_fmt="Q{n}, {s} (not scored) (0 no, 1 yes)",

106 comment_strings=[

107 "problems_teeth_dentures",

108 "usually_wears_dentures",

109 ],

110 )

111

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

113 SCORED_FIELDS = strseq("q", 1, NSCOREDQUESTIONS)

114

115 @staticmethod

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

117 _ = req.gettext

118 return _("Abnormal Involuntary Movement Scale")

119

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

121 return [

122 TrackerInfo(

123 value=self.total_score(),

124 plot_label="AIMS total score",

125 axis_label="Total score (out of 40)",

126 axis_min=-0.5,

127 axis_max=40.5,

128 )

129 ]

130

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

132 if not self.is_complete():

133 return CTV_INCOMPLETE

134 return [CtvInfo(content=f"AIMS total score {self.total_score()}/40")]

135

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

137 return self.standard_task_summary_fields() + [

138 SummaryElement(

139 name="total",

140 coltype=Integer(),

141 value=self.total_score(),

142 comment="Total score (/40)",

143 )

144 ]

145

146 def is_complete(self) -> bool:

147 return (

148 self.all_fields_not_none(Aims.TASK_FIELDS)

149 and self.field_contents_valid()

150 )

151

152 def total_score(self) -> int:

153 return cast(int, self.sum_fields(self.SCORED_FIELDS))

154

155 # noinspection PyUnresolvedReferences

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

157 score = self.total_score()

158 main_dict: dict[Optional[int], Optional[str]] = {None: None}

159 q10_dict: dict[Optional[int], Optional[str]] = {None: None}

160 for option in range(0, 5):

161 main_dict[option] = (

162 str(option)

163 + " — "

164 + self.wxstring(req, "main_option" + str(option))

165 )

166 q10_dict[option] = (

167 str(option)

168 + " — "

169 + self.wxstring(req, "q10_option" + str(option))

170 )

171

172 q_a = ""

173 for q in range(1, 10):

174 q_a += tr_qa(

175 self.wxstring(req, "q" + str(q) + "_s"),

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

177 )

178 q_a += (

179 tr_qa(

180 self.wxstring(req, "q10_s"), get_from_dict(q10_dict, self.q10) # type: ignore[attr-defined] # noqa: E501

181 )

182 + tr_qa(

183 self.wxstring(req, "q11_s"), get_yes_no_none(req, self.q11) # type: ignore[attr-defined] # noqa: E501

184 )

185 + tr_qa(

186 self.wxstring(req, "q12_s"), get_yes_no_none(req, self.q12) # type: ignore[attr-defined] # noqa: E501

187 )

188 )

189

190 return f"""

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

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

193 {self.get_is_complete_tr(req)}

194 {tr(req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>",

195 answer(score) + " / 40")}

196 </table>

197 </div>

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

199 <tr>

200 <th width="50%">Question</th>

201 <th width="50%">Answer</th>

202 </tr>

203 {q_a}

204 </table>

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

206 [1] Only Q1–10 are scored.

207 </div>

208 """

209

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

211 codes = [

212 SnomedExpression(

213 req.snomed(SnomedLookup.AIMS_PROCEDURE_ASSESSMENT)

214 )

215 ]

216 if self.is_complete():

217 codes.append(

218 SnomedExpression(

219 req.snomed(SnomedLookup.AIMS_SCALE),

220 {

221 req.snomed(

222 SnomedLookup.AIMS_TOTAL_SCORE

223 ): self.total_score()

224 },

225 )

226 )

227 return codes