Coverage for tasks/paradise24.py: 47%

55 statements  

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

1""" 

2camcops_server/tasks/paradise24.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**PARADISE 24 task.** 

27 

28""" 

29 

30from typing import Any, cast, Optional, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.sql.sqltypes import Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_db import add_multiple_columns 

37from camcops_server.cc_modules.cc_html import tr_qa, tr, answer 

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

40 

41 

42class Paradise24( # type: ignore[misc] 

43 TaskHasPatientMixin, 

44 Task, 

45): 

46 __tablename__ = "paradise24" 

47 shortname = "PARADISE 24" 

48 

49 Q_PREFIX = "q" 

50 FIRST_Q = 1 

51 LAST_Q = 24 

52 

53 @classmethod 

54 def extend_columns(cls: Type["Paradise24"], **kwargs: Any) -> None: 

55 

56 add_multiple_columns( 

57 cls, 

58 cls.Q_PREFIX, 

59 cls.FIRST_Q, 

60 cls.LAST_Q, 

61 coltype=Integer, 

62 minimum=0, 

63 maximum=2, 

64 comment_fmt="Q{n} - {s}", 

65 comment_strings=[ 

66 "rested 0-2 (none - a lot)", 

67 "loss interest 0-2 (none - a lot)", 

68 "appetite 0-2 (none - a lot)", 

69 "sleeping 0-2 (none - a lot)", 

70 "irritable 0-2 (none - a lot)", 

71 "slowed down 0-2 (none - a lot)", 

72 "sad 0-2 (none - a lot)", 

73 "worry 0-2 (none - a lot)", 

74 "cope 0-2 (none - a lot)", 

75 "pain 0-2 (none - a lot)", 

76 "concentrating 0-2 (none - a lot)", 

77 "remembering 0-2 (none - a lot)", 

78 "decisions 0-2 (none - a lot)", 

79 "conversation 0-2 (none - a lot)", 

80 "walking 0-2 (none - a lot)", 

81 "grooming 0-2 (none - a lot)", 

82 "sexual 0-2 (none - a lot)", 

83 "staying by yourself 0-2 (none - a lot)", 

84 "health 0-2 (none - a lot)", 

85 "friendship 0-2 (none - a lot)", 

86 "getting along 0-2 (none - a lot)", 

87 "work or school 0-2 (none - a lot)", 

88 "money 0-2 (none - a lot)", 

89 "community 0-2 (none - a lot)", 

90 ], 

91 ) 

92 

93 ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q) 

94 

95 @staticmethod 

96 def longname(req: CamcopsRequest) -> str: 

97 _ = req.gettext 

98 return _( 

99 "Psychosocial fActors Relevant to BrAin DISorders in Europe–24" 

100 ) 

101 

102 def is_complete(self) -> bool: 

103 if self.any_fields_none(self.ALL_FIELD_NAMES): 

104 return False 

105 

106 return True 

107 

108 def total_score(self) -> Optional[int]: 

109 if not self.is_complete(): 

110 return None 

111 

112 return cast(int, self.sum_fields(self.ALL_FIELD_NAMES)) 

113 

114 def metric_score(self) -> Optional[int]: 

115 total_score = self.total_score() 

116 

117 if total_score is None: 

118 return None 

119 

120 # Table 3 of Cieza et al. (2015); see help. 

121 # - doi:10.1371/journal.pone.0132410.t003 

122 # - Indexes are raw scores. 

123 # - Values are transformed scores. 

124 score_lookup = [ 

125 0, # 0 

126 10, 

127 19, 

128 25, 

129 29, 

130 33, 

131 36, 

132 38, 

133 41, 

134 43, 

135 45, # 10 

136 46, 

137 48, 

138 50, 

139 51, 

140 53, 

141 54, 

142 55, 

143 57, 

144 58, 

145 59, # 20 

146 60, 

147 61, 

148 63, 

149 64, 

150 65, 

151 66, 

152 67, 

153 68, 

154 69, 

155 71, # 30 

156 72, 

157 73, 

158 74, 

159 76, 

160 77, 

161 78, 

162 80, 

163 81, 

164 83, 

165 85, # 40 

166 87, 

167 89, 

168 91, 

169 92, 

170 94, 

171 96, 

172 98, 

173 100, # 48 

174 ] 

175 

176 try: 

177 return score_lookup[total_score] 

178 except (IndexError, TypeError): 

179 return None 

180 

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

182 rows = "" 

183 for q_num in range(self.FIRST_Q, self.LAST_Q + 1): 

184 field = self.Q_PREFIX + str(q_num) 

185 question_cell = f"{q_num}. {self.xstring(req, field)}" 

186 

187 rows += tr_qa(question_cell, self.get_answer_cell(req, q_num)) 

188 

189 html = """ 

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

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

192 {tr_is_complete} 

193 {total_score} 

194 {metric_score} 

195 </table> 

196 </div> 

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

198 <tr> 

199 <th width="60%">Question</th> 

200 <th width="40%">Score</th> 

201 </tr> 

202 {rows} 

203 </table> 

204 <div class="{CssClass.FOOTNOTES}"> 

205 [1] Sum of all questions, range 0–48. 

206 [2] Transformed metric scale, range 0–100. 

207 </div> 

208 """.format( 

209 CssClass=CssClass, 

210 tr_is_complete=self.get_is_complete_tr(req), 

211 total_score=tr( 

212 self.wxstring(req, "raw_score") + " <sup>[1]</sup>", 

213 f"{answer(self.total_score())}", 

214 ), 

215 metric_score=tr( 

216 self.wxstring(req, "metric_score") + " <sup>[2]</sup>", 

217 f"{answer(self.metric_score())}", 

218 ), 

219 rows=rows, 

220 ) 

221 return html 

222 

223 def get_answer_cell(self, req: CamcopsRequest, q_num: int) -> str: 

224 q_field = self.Q_PREFIX + str(q_num) 

225 

226 score = getattr(self, q_field) 

227 meaning = self.get_score_meaning(req, score) 

228 answer_cell = f"{score} [{meaning}]" 

229 

230 return answer_cell 

231 

232 def get_score_meaning(self, req: CamcopsRequest, score: int) -> str: 

233 return self.wxstring(req, f"option_{score}")