Coverage for tasks/cage.py: 59%
61 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/cage.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28"""
30from typing import Any, Dict, List, Tuple, Type
32from cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy.ext.declarative import DeclarativeMeta
34from sqlalchemy.sql.sqltypes import Integer
36from camcops_server.cc_modules.cc_constants import CssClass
37from camcops_server.cc_modules.cc_db import add_multiple_columns
38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
39from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa
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 Task, TaskHasPatientMixin
45from camcops_server.cc_modules.cc_text import SS
46from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
49# =============================================================================
50# CAGE
51# =============================================================================
54class CageMetaclass(DeclarativeMeta):
55 # noinspection PyInitNewSignature
56 def __init__(
57 cls: Type["Cage"],
58 name: str,
59 bases: Tuple[Type, ...],
60 classdict: Dict[str, Any],
61 ) -> None:
62 add_multiple_columns(
63 cls,
64 "q",
65 1,
66 cls.NQUESTIONS,
67 CharColType,
68 pv=["Y", "N"],
69 comment_fmt="Q{n}, {s} (Y, N)",
70 comment_strings=["C", "A", "G", "E"],
71 )
72 super().__init__(name, bases, classdict)
75class Cage(TaskHasPatientMixin, Task, metaclass=CageMetaclass):
76 """
77 Server implementation of the CAGE task.
78 """
80 __tablename__ = "cage"
81 shortname = "CAGE"
82 provides_trackers = True
84 NQUESTIONS = 4
85 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
87 @staticmethod
88 def longname(req: "CamcopsRequest") -> str:
89 _ = req.gettext
90 return _("CAGE Questionnaire")
92 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
93 return [
94 TrackerInfo(
95 value=self.total_score(),
96 plot_label="CAGE total score",
97 axis_label=f"Total score (out of {self.NQUESTIONS})",
98 axis_min=-0.5,
99 axis_max=self.NQUESTIONS + 0.5,
100 horizontal_lines=[1.5],
101 )
102 ]
104 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
105 if not self.is_complete():
106 return CTV_INCOMPLETE
107 return [
108 CtvInfo(
109 content=f"CAGE score {self.total_score()}/{self.NQUESTIONS}"
110 )
111 ]
113 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
114 return self.standard_task_summary_fields() + [
115 SummaryElement(
116 name="total",
117 coltype=Integer(),
118 value=self.total_score(),
119 comment=f"Total score (/{self.NQUESTIONS})",
120 )
121 ]
123 def is_complete(self) -> bool:
124 return (
125 self.all_fields_not_none(Cage.TASK_FIELDS)
126 and self.field_contents_valid()
127 )
129 def get_value(self, q: int) -> int:
130 return 1 if getattr(self, "q" + str(q)) == "Y" else 0
132 def total_score(self) -> int:
133 total = 0
134 for i in range(1, Cage.NQUESTIONS + 1):
135 total += self.get_value(i)
136 return total
138 def get_task_html(self, req: CamcopsRequest) -> str:
139 score = self.total_score()
140 exceeds_cutoff = score >= 2
141 q_a = ""
142 for q in range(1, Cage.NQUESTIONS + 1):
143 q_a += tr_qa(
144 str(q) + " — " + self.wxstring(req, "q" + str(q)),
145 getattr(self, "q" + str(q)),
146 ) # answer is itself Y/N/NULL # noqa
147 total_score = tr(
148 req.sstring(SS.TOTAL_SCORE),
149 answer(score) + f" / {self.NQUESTIONS}",
150 )
151 over_threshold = tr_qa(
152 self.wxstring(req, "over_threshold"),
153 get_yes_no(req, exceeds_cutoff),
154 )
155 return f"""
156 <div class="{CssClass.SUMMARY}">
157 <table class="{CssClass.SUMMARY}">
158 {self.get_is_complete_tr(req)}
159 {total_score}
160 {over_threshold}
161 </table>
162 </div>
163 <table class="{CssClass.TASKDETAIL}">
164 <tr>
165 <th width="70%">Question</th>
166 <th width="30%">Answer</th>
167 </tr>
168 {q_a}
169 </table>
170 """
172 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
173 codes = [
174 SnomedExpression(
175 req.snomed(SnomedLookup.CAGE_PROCEDURE_ASSESSMENT)
176 )
177 ]
178 if self.is_complete():
179 codes.append(
180 SnomedExpression(
181 req.snomed(SnomedLookup.CAGE_SCALE),
182 {req.snomed(SnomedLookup.CAGE_SCORE): self.total_score()},
183 )
184 )
185 return codes