Coverage for tasks/qolsg.py: 60%
67 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/qolsg.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 pendulum import DateTime as Pendulum
29from typing import List, Optional
31import cardinal_pythonlib.rnc_web as ws
32from sqlalchemy.orm import Mapped, mapped_column
33from sqlalchemy.sql.sqltypes import String
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
37from camcops_server.cc_modules.cc_html import get_yes_no_none, tr_qa
38from camcops_server.cc_modules.cc_request import CamcopsRequest
39from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
40from camcops_server.cc_modules.cc_sqla_coltypes import (
41 BIT_CHECKER,
42 mapped_camcops_column,
43 PendulumDateTimeAsIsoTextColType,
44 ZERO_TO_ONE_CHECKER,
45)
46from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
47from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
50# =============================================================================
51# QoL-SG
52# =============================================================================
54DP = 3
57class QolSG(TaskHasPatientMixin, Task): # type: ignore[misc]
58 """
59 Server implementation of the QoL-SG task.
60 """
62 __tablename__ = "qolsg"
63 shortname = "QoL-SG"
64 info_filename_stem = "qol"
65 provides_trackers = True
67 category_start_time: Mapped[Optional[Pendulum]] = mapped_column(
68 PendulumDateTimeAsIsoTextColType,
69 comment="Time categories were offered (ISO-8601)",
70 )
71 category_responded: Mapped[Optional[int]] = mapped_camcops_column(
72 permitted_value_checker=BIT_CHECKER,
73 comment="Responded to category choice? (0 no, 1 yes)",
74 )
75 category_response_time: Mapped[Optional[Pendulum]] = mapped_column(
76 PendulumDateTimeAsIsoTextColType,
77 comment="Time category was chosen (ISO-8601)",
78 )
79 category_chosen: Mapped[Optional[str]] = mapped_column(
80 String(length=len("medium")),
81 comment="Category chosen: high (QoL > 1) "
82 "medium (0 <= QoL <= 1) low (QoL < 0)",
83 )
84 gamble_fixed_option: Mapped[Optional[str]] = mapped_column(
85 String(length=len("current")),
86 comment="Fixed option in gamble (current, healthy, dead)",
87 )
88 gamble_lottery_option_p: Mapped[Optional[str]] = mapped_column(
89 String(length=len("current")),
90 comment="Gamble: option corresponding to p "
91 "(current, healthy, dead)",
92 )
93 gamble_lottery_option_q: Mapped[Optional[str]] = mapped_column(
94 String(length=len("current")),
95 comment="Gamble: option corresponding to q "
96 "(current, healthy, dead) (q = 1 - p)",
97 )
98 gamble_lottery_on_left: Mapped[Optional[int]] = mapped_camcops_column(
99 permitted_value_checker=BIT_CHECKER,
100 comment="Gamble: lottery shown on the left (0 no, 1 yes)",
101 )
102 gamble_starting_p: Mapped[Optional[float]] = mapped_camcops_column(
103 permitted_value_checker=ZERO_TO_ONE_CHECKER,
104 comment="Gamble: starting value of p",
105 )
106 gamble_start_time: Mapped[Optional[Pendulum]] = mapped_column(
107 PendulumDateTimeAsIsoTextColType,
108 comment="Time gamble was offered (ISO-8601)",
109 )
110 gamble_responded: Mapped[Optional[int]] = mapped_camcops_column(
111 permitted_value_checker=BIT_CHECKER,
112 comment="Gamble was responded to? (0 no, 1 yes)",
113 )
114 gamble_response_time: Mapped[Optional[Pendulum]] = mapped_column(
115 PendulumDateTimeAsIsoTextColType,
116 comment="Time subject responded to gamble (ISO-8601)",
117 )
118 gamble_p: Mapped[Optional[float]] = mapped_camcops_column(
119 permitted_value_checker=ZERO_TO_ONE_CHECKER,
120 comment="Final value of p",
121 )
122 utility: Mapped[Optional[float]] = mapped_column(
123 comment="Calculated utility, h"
124 )
126 @staticmethod
127 def longname(req: "CamcopsRequest") -> str:
128 _ = req.gettext
129 return _("Quality of Life: Standard Gamble")
131 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
132 return [
133 TrackerInfo(
134 value=self.utility,
135 plot_label="Quality of life: standard gamble",
136 axis_label="QoL (0-1)",
137 axis_min=0,
138 axis_max=1,
139 )
140 ]
142 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
143 if not self.is_complete():
144 return CTV_INCOMPLETE
145 return [
146 CtvInfo(
147 content=f"Quality of life: {ws.number_to_dp(self.utility, DP)}"
148 )
149 ]
151 def is_complete(self) -> bool:
152 return self.utility is not None and self.field_contents_valid()
154 def get_task_html(self, req: CamcopsRequest) -> str:
155 h = f"""
156 <div class="{CssClass.SUMMARY}">
157 <table class="{CssClass.SUMMARY}">
158 {self.get_is_complete_tr(req)}
159 {tr_qa("Utility",
160 ws.number_to_dp(self.utility, DP, default=None))}
161 </table>
162 </div>
163 <div class="{CssClass.EXPLANATION}">
164 Quality of life (QoL) has anchor values of 0 (none) and 1
165 (perfect health). The Standard Gamble offers a trade-off to
166 determine utility (QoL).
167 Values <0 and >1 are possible with some gambles.
168 </div>
169 <table class="{CssClass.TASKDETAIL}">
170 <tr><th width="50%">Measure</th><th width="50%">Value</th></tr>
171 """
172 h += tr_qa("Category choice: start time", self.category_start_time)
173 h += tr_qa(
174 "Category choice: responded?",
175 get_yes_no_none(req, self.category_responded),
176 )
177 h += tr_qa(
178 "Category choice: response time", self.category_response_time
179 )
180 h += tr_qa("Category choice: category chosen", self.category_chosen)
181 h += tr_qa("Gamble: fixed option", self.gamble_fixed_option)
182 h += tr_qa(
183 "Gamble: lottery option for <i>p</i>", self.gamble_lottery_option_p
184 )
185 h += tr_qa(
186 "Gamble: lottery option for <i>q</i> = 1 – <i>p</i>",
187 self.gamble_lottery_option_q,
188 )
189 h += tr_qa(
190 "Gamble: lottery on left?",
191 get_yes_no_none(req, self.gamble_lottery_on_left),
192 )
193 h += tr_qa("Gamble: starting <i>p</i>", self.gamble_starting_p)
194 h += tr_qa("Gamble: start time", self.gamble_start_time)
195 h += tr_qa(
196 "Gamble: responded?", get_yes_no_none(req, self.gamble_responded)
197 )
198 h += tr_qa("Gamble: response time", self.gamble_response_time)
199 h += tr_qa(
200 "Gamble: <i>p</i>",
201 ws.number_to_dp(self.gamble_p, DP, default=None),
202 )
203 h += tr_qa(
204 "Calculated utility",
205 ws.number_to_dp(self.utility, DP, default=None),
206 )
207 h += """
208 </table>
209 """
210 return h
212 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
213 if not self.is_complete():
214 return []
215 return [SnomedExpression(req.snomed(SnomedLookup.QOL_SCALE))]