Coverage for tasks/qolsg.py: 60%

67 statements  

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

1""" 

2camcops_server/tasks/qolsg.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 pendulum import DateTime as Pendulum 

29from typing import List, Optional 

30 

31import cardinal_pythonlib.rnc_web as ws 

32from sqlalchemy.orm import Mapped, mapped_column 

33from sqlalchemy.sql.sqltypes import String 

34 

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 

48 

49 

50# ============================================================================= 

51# QoL-SG 

52# ============================================================================= 

53 

54DP = 3 

55 

56 

57class QolSG(TaskHasPatientMixin, Task): # type: ignore[misc] 

58 """ 

59 Server implementation of the QoL-SG task. 

60 """ 

61 

62 __tablename__ = "qolsg" 

63 shortname = "QoL-SG" 

64 info_filename_stem = "qol" 

65 provides_trackers = True 

66 

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 ) 

125 

126 @staticmethod 

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

128 _ = req.gettext 

129 return _("Quality of Life: Standard Gamble") 

130 

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 ] 

141 

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 ] 

150 

151 def is_complete(self) -> bool: 

152 return self.utility is not None and self.field_contents_valid() 

153 

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 &lt;0 and &gt;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 

211 

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))]