Coverage for tasks/qolsg.py: 60%

1"""

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

28 from pendulum import DateTime as Pendulum

29 from typing import List, Optional

30

31 import cardinal_pythonlib.rnc_web as ws

32 from sqlalchemy.orm import Mapped, mapped_column

33 from sqlalchemy.sql.sqltypes import String

34

35 from camcops_server.cc_modules.cc_constants import CssClass

36 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

37 from camcops_server.cc_modules.cc_html import get_yes_no_none, tr_qa

38 from camcops_server.cc_modules.cc_request import CamcopsRequest

39 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

40 from camcops_server.cc_modules.cc_sqla_coltypes import (

41 BIT_CHECKER,

42 mapped_camcops_column,

43 PendulumDateTimeAsIsoTextColType,

44 ZERO_TO_ONE_CHECKER,

45)

46 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

47 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

48

49

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

51# QoL-SG

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

53

54 DP = 3

55

56

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