Coverage for tasks/qolbasic.py: 61%

1"""

2 camcops_server/tasks/qolbasic.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 typing import List, Optional

29

30 from cardinal_pythonlib.maths_py import mean

31 import cardinal_pythonlib.rnc_web as ws

32 from sqlalchemy.orm import Mapped

33 from sqlalchemy.sql.sqltypes import Float

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 answer, identity, tr

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 mapped_camcops_column,

42 PermittedValueChecker,

43)

44 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

45 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

46 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

47

48

49# =============================================================================

50# QoL-Basic

51# =============================================================================

52

53 DP = 3

54

55

56 class QolBasic(TaskHasPatientMixin, Task): # type: ignore[misc]

57 """

58 Server implementation of the QoL-Basic task.

59 """

60

61 __tablename__ = "qolbasic"

62 shortname = "QoL-Basic"

63 info_filename_stem = "qol"

64 provides_trackers = True

65

66 tto: Mapped[Optional[float]] = mapped_camcops_column(

67 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=10),

68 comment="Time trade-off (QoL * 10). Prompt: ... Indicate... the "

69 "number of years in full health [0-10] that you think is "

70 "of equal value to 10 years in your current health state.",

71 )

72 rs: Mapped[Optional[float]] = mapped_camcops_column(

73 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100),

74 comment="Rating scale (QoL * 100). Prompt: Mark the point on the "

75 "scale [0-100] that you feel best illustrates your current "

76 "quality of life.",

77 )

78

79 TASK_FIELDS = ["tto", "rs"]

80

81 @staticmethod

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

83 _ = req.gettext

84 return _("Quality of Life: basic assessment")

85

86 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:

87 return [

88 TrackerInfo(

89 value=self.get_tto_qol(),

90 plot_label="Quality of life: time trade-off",

91 axis_label="TTO QoL (0-1)",

92 axis_min=0,

93 axis_max=1,

94 ),

95 TrackerInfo(

96 value=self.get_rs_qol(),

97 plot_label="Quality of life: rating scale",

98 axis_label="RS QoL (0-1)",

99 axis_min=0,

100 axis_max=1,

101 ),

102 ]

103

104 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:

105 if not self.is_complete():

106 return CTV_INCOMPLETE

107 tto_qol = self.get_tto_qol()

108 rs_qol = self.get_rs_qol()

109 mean_qol = mean([tto_qol, rs_qol])

110 return [

111 CtvInfo(

112 content=(

113 f"Quality of life: time trade-off "

114 f"{ws.number_to_dp(tto_qol, DP)}, "

115 f"rating scale {ws.number_to_dp(rs_qol, DP)}, "

116 f"mean {ws.number_to_dp(mean_qol, DP)}."

117 )

118 )

119 ]

120

121 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:

122 return self.standard_task_summary_fields() + [

123 SummaryElement(

124 name="tto_qol",

125 coltype=Float(),

126 value=self.get_tto_qol(),

127 comment="Quality of life (0-1), from time trade-off method",

128 ),

129 SummaryElement(

130 name="rs_qol",

131 coltype=Float(),

132 value=self.get_rs_qol(),

133 comment="Quality of life (0-1), from rating scale method",

134 ),

135 ]

136

137 def is_complete(self) -> bool:

138 return (

139 self.all_fields_not_none(QolBasic.TASK_FIELDS)

140 and self.field_contents_valid()

141 )

142

143 def get_tto_qol(self) -> Optional[float]:

144 return self.tto / 10 if self.tto is not None else None

145

146 def get_rs_qol(self) -> Optional[float]:

147 return self.rs / 100 if self.rs is not None else None

148

149 def get_task_html(self, req: CamcopsRequest) -> str:

150 tto_qol = self.get_tto_qol()

151 rs_qol = self.get_rs_qol()

152 mean_qol = mean([tto_qol, rs_qol])

153 h = """

154 <div class="{CssClass.SUMMARY}">

155 <table class="{CssClass.SUMMARY}">

156 {tr_is_complete}

157 {mean_qol}

158 </table>

159 </div>

160 <div class="{CssClass.EXPLANATION}">

161 Quality of life (QoL) has anchor values of 0 (none) and 1

162 (perfect health), and can be asked about in several ways.

163 </div>

164 <table class="{CssClass.TASKDETAIL}">

165 <tr>

166 <th width="33%">Scale</th>

167 <th width="33%">Answer</th>

168 <td width="33%">Implied QoL</th>

169 </tr>

170 {tto}

171 {rs}

172 </table>

173 """.format(

174 CssClass=CssClass,

175 tr_is_complete=self.get_is_complete_tr(req),

176 mean_qol=tr(

177 "Mean QoL",

178 answer(

179 ws.number_to_dp(mean_qol, DP, default=None),

180 formatter_answer=identity,

181 ),

182 ),

183 tto=tr(

184 self.wxstring(req, "tto_q_s"),

185 answer(ws.number_to_dp(self.tto, DP, default=None)),

186 answer(

187 ws.number_to_dp(tto_qol, DP, default=None),

188 formatter_answer=identity,

189 ),

190 ),

191 rs=tr(

192 self.wxstring(req, "rs_q_s"),

193 answer(ws.number_to_dp(self.rs, DP, default=None)),

194 answer(

195 ws.number_to_dp(rs_qol, DP, default=None),

196 formatter_answer=identity,

197 ),

198 ),

199 )

200 return h

201

202 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:

203 if not self.is_complete():

204 return []

205 return [SnomedExpression(req.snomed(SnomedLookup.QOL_SCALE))]