Coverage for tasks/aims.py: 59%

58 statements  

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

1""" 

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

28from typing import Any, cast, List, Optional, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Integer 

32 

33from camcops_server.cc_modules.cc_constants import CssClass, PV 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

35from camcops_server.cc_modules.cc_db import add_multiple_columns 

36from camcops_server.cc_modules.cc_html import ( 

37 answer, 

38 get_yes_no_none, 

39 tr, 

40 tr_qa, 

41) 

42from camcops_server.cc_modules.cc_request import CamcopsRequest 

43from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 get_from_dict, 

47 Task, 

48 TaskHasClinicianMixin, 

49 TaskHasPatientMixin, 

50) 

51from camcops_server.cc_modules.cc_text import SS 

52from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

53 

54 

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

56# AIMS 

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

58 

59 

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