Coverage for tasks/aims.py: 59%
58 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/aims.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28from typing import Any, cast, List, Optional, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.sql.sqltypes import Integer
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
55# =============================================================================
56# AIMS
57# =============================================================================
60class Aims( # type: ignore[misc]
61 TaskHasPatientMixin,
62 TaskHasClinicianMixin,
63 Task,
64):
65 """
66 Server implementation of the AIMS task.
67 """
69 __tablename__ = "aims"
70 shortname = "AIMS"
71 provides_trackers = True
73 NQUESTIONS = 12
74 NSCOREDQUESTIONS = 10
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 )
112 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
113 SCORED_FIELDS = strseq("q", 1, NSCOREDQUESTIONS)
115 @staticmethod
116 def longname(req: "CamcopsRequest") -> str:
117 _ = req.gettext
118 return _("Abnormal Involuntary Movement Scale")
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 ]
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")]
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 ]
146 def is_complete(self) -> bool:
147 return (
148 self.all_fields_not_none(Aims.TASK_FIELDS)
149 and self.field_contents_valid()
150 )
152 def total_score(self) -> int:
153 return cast(int, self.sum_fields(self.SCORED_FIELDS))
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 )
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 )
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 """
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