Coverage for tasks/empsa.py: 73%

95 statements  

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

1""" 

2camcops_server/tasks/empsa.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 

26Eating and Meal Preparation Skills Assessment (EMPSA) 

27 

28""" 

29 

30from typing import Optional, TYPE_CHECKING 

31 

32import cardinal_pythonlib.rnc_web as ws 

33from cardinal_pythonlib.stringfunc import strseq 

34from sqlalchemy import UnicodeText 

35from sqlalchemy.orm import Mapped 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_html import answer, tr 

39from camcops_server.cc_modules.cc_sqla_coltypes import ( 

40 mapped_camcops_column, 

41 ZERO_TO_10_CHECKER, 

42) 

43from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

44 

45if TYPE_CHECKING: 

46 from camcops_server.cc_modules.cc_request import CamcopsRequest 

47 

48DP = 2 

49RANGE_SUFFIX = " (0 none - 10 total)" 

50 

51 

52class Empsa(TaskHasPatientMixin, Task): # type: ignore[misc] 

53 __tablename__ = "empsa" 

54 shortname = "EMPSA" 

55 

56 q1_ability: Mapped[Optional[int]] = mapped_camcops_column( 

57 permitted_value_checker=ZERO_TO_10_CHECKER, 

58 comment="Q1 ability (planning)" + RANGE_SUFFIX, 

59 ) 

60 q1_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

61 permitted_value_checker=ZERO_TO_10_CHECKER, 

62 comment="Q1 motivation (planning)" + RANGE_SUFFIX, 

63 ) 

64 q1_comments: Mapped[Optional[str]] = mapped_camcops_column( 

65 UnicodeText, comment="Q1 comments (planning)" 

66 ) 

67 

68 q2_ability: Mapped[Optional[int]] = mapped_camcops_column( 

69 permitted_value_checker=ZERO_TO_10_CHECKER, 

70 comment="Q2 ability (budget)" + RANGE_SUFFIX, 

71 ) 

72 q2_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

73 permitted_value_checker=ZERO_TO_10_CHECKER, 

74 comment="Q2 motivation (budget)" + RANGE_SUFFIX, 

75 ) 

76 q2_comments: Mapped[Optional[str]] = mapped_camcops_column( 

77 UnicodeText, comment="Q2 comments (budget)" 

78 ) 

79 

80 q3_ability: Mapped[Optional[int]] = mapped_camcops_column( 

81 permitted_value_checker=ZERO_TO_10_CHECKER, 

82 comment="Q3 ability (shopping)" + RANGE_SUFFIX, 

83 ) 

84 q3_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

85 permitted_value_checker=ZERO_TO_10_CHECKER, 

86 comment="Q3 motivation (shopping)" + RANGE_SUFFIX, 

87 ) 

88 q3_comments: Mapped[Optional[str]] = mapped_camcops_column( 

89 UnicodeText, comment="Q3 comments (shopping)" 

90 ) 

91 

92 q4_ability: Mapped[Optional[int]] = mapped_camcops_column( 

93 permitted_value_checker=ZERO_TO_10_CHECKER, 

94 comment="Q4 ability (cooking)" + RANGE_SUFFIX, 

95 ) 

96 q4_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

97 permitted_value_checker=ZERO_TO_10_CHECKER, 

98 comment="Q4 motivation (cooking)" + RANGE_SUFFIX, 

99 ) 

100 q4_comments: Mapped[Optional[str]] = mapped_camcops_column( 

101 UnicodeText, comment="Q4 comments (cooking)" 

102 ) 

103 

104 q5_ability: Mapped[Optional[int]] = mapped_camcops_column( 

105 permitted_value_checker=ZERO_TO_10_CHECKER, 

106 comment="Q5 ability (preparing)" + RANGE_SUFFIX, 

107 ) 

108 q5_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

109 permitted_value_checker=ZERO_TO_10_CHECKER, 

110 comment="Q5 motivation (preparing)" + RANGE_SUFFIX, 

111 ) 

112 q5_comments: Mapped[Optional[str]] = mapped_camcops_column( 

113 UnicodeText, comment="Q5 comments (preparing)" 

114 ) 

115 

116 q6_ability: Mapped[Optional[int]] = mapped_camcops_column( 

117 permitted_value_checker=ZERO_TO_10_CHECKER, 

118 comment="Q6 ability (portions)" + RANGE_SUFFIX, 

119 ) 

120 q6_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

121 permitted_value_checker=ZERO_TO_10_CHECKER, 

122 comment="Q6 motivation (portions)" + RANGE_SUFFIX, 

123 ) 

124 q6_comments: Mapped[Optional[str]] = mapped_camcops_column( 

125 UnicodeText, comment="Q6 comments (portions)" 

126 ) 

127 

128 q7_ability: Mapped[Optional[int]] = mapped_camcops_column( 

129 permitted_value_checker=ZERO_TO_10_CHECKER, 

130 comment="Q7 ability (throwing away)" + RANGE_SUFFIX, 

131 ) 

132 q7_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

133 permitted_value_checker=ZERO_TO_10_CHECKER, 

134 comment="Q7 motivation (throwing away)" + RANGE_SUFFIX, 

135 ) 

136 q7_comments: Mapped[Optional[str]] = mapped_camcops_column( 

137 UnicodeText, comment="Q7 comments (throwing away)" 

138 ) 

139 

140 q8_ability: Mapped[Optional[int]] = mapped_camcops_column( 

141 permitted_value_checker=ZERO_TO_10_CHECKER, 

142 comment="Q8 ability (difficult food)" + RANGE_SUFFIX, 

143 ) 

144 q8_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

145 permitted_value_checker=ZERO_TO_10_CHECKER, 

146 comment="Q8 motivation (difficult food)" + RANGE_SUFFIX, 

147 ) 

148 q8_comments: Mapped[Optional[str]] = mapped_camcops_column( 

149 UnicodeText, comment="Q8 comments (difficult food)" 

150 ) 

151 

152 q9_ability: Mapped[Optional[int]] = mapped_camcops_column( 

153 permitted_value_checker=ZERO_TO_10_CHECKER, 

154 comment="Q9 ability (normal pace)" + RANGE_SUFFIX, 

155 ) 

156 q9_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

157 permitted_value_checker=ZERO_TO_10_CHECKER, 

158 comment="Q9 motivation (normal pace)" + RANGE_SUFFIX, 

159 ) 

160 q9_comments: Mapped[Optional[str]] = mapped_camcops_column( 

161 UnicodeText, comment="Q9 comments (normal pace)" 

162 ) 

163 

164 q10_ability: Mapped[Optional[int]] = mapped_camcops_column( 

165 permitted_value_checker=ZERO_TO_10_CHECKER, 

166 comment="Q10 ability (others)" + RANGE_SUFFIX, 

167 ) 

168 q10_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

169 permitted_value_checker=ZERO_TO_10_CHECKER, 

170 comment="Q10 motivation (others)" + RANGE_SUFFIX, 

171 ) 

172 q10_comments: Mapped[Optional[str]] = mapped_camcops_column( 

173 UnicodeText, comment="Q10 comments (others)" 

174 ) 

175 

176 q11_ability: Mapped[Optional[int]] = mapped_camcops_column( 

177 permitted_value_checker=ZERO_TO_10_CHECKER, 

178 comment="Q11 ability (public)" + RANGE_SUFFIX, 

179 ) 

180 q11_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

181 permitted_value_checker=ZERO_TO_10_CHECKER, 

182 comment="Q11 motivation (public)" + RANGE_SUFFIX, 

183 ) 

184 q11_comments: Mapped[Optional[str]] = mapped_camcops_column( 

185 UnicodeText, comment="Q11 comments (public)" 

186 ) 

187 

188 q12_ability: Mapped[Optional[int]] = mapped_camcops_column( 

189 permitted_value_checker=ZERO_TO_10_CHECKER, 

190 comment="Q12 ability (distress)" + RANGE_SUFFIX, 

191 ) 

192 q12_motivation: Mapped[Optional[int]] = mapped_camcops_column( 

193 permitted_value_checker=ZERO_TO_10_CHECKER, 

194 comment="Q12 motivation (distress)" + RANGE_SUFFIX, 

195 ) 

196 q12_comments: Mapped[Optional[str]] = mapped_camcops_column( 

197 UnicodeText, comment="Q12 comments (distress)" 

198 ) 

199 

200 PREFIX = "q" 

201 ABILITY_SUFFIX = "_ability" 

202 MOTIVATION_SUFFIX = "_motivation" 

203 COMMENTS_SUFFIX = "_comments" 

204 ALL_ABILITY_FIELD_NAMES = strseq(PREFIX, 1, 12, ABILITY_SUFFIX) 

205 ALL_MOTIVATION_FIELD_NAMES = strseq(PREFIX, 1, 12, MOTIVATION_SUFFIX) 

206 ALL_MANDATORY_FIELD_NAMES = ( 

207 ALL_ABILITY_FIELD_NAMES + ALL_MOTIVATION_FIELD_NAMES 

208 ) 

209 FIRST_Q = 1 

210 LAST_Q = 12 

211 MAX_SCORE = 10 # per question, or for subscales that are means 

212 

213 @staticmethod 

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

215 _ = req.gettext 

216 return _("Eating and Meal Preparation Skills Assessment") 

217 

218 def is_complete(self) -> bool: 

219 if self.any_fields_none(self.ALL_MANDATORY_FIELD_NAMES): 

220 return False 

221 

222 return True 

223 

224 def ability_subscale(self) -> Optional[float]: 

225 return self.mean_fields(self.ALL_ABILITY_FIELD_NAMES, ignorevalues=[]) 

226 

227 def motivation_subscale(self) -> Optional[float]: 

228 return self.mean_fields( 

229 self.ALL_MOTIVATION_FIELD_NAMES, ignorevalues=[] 

230 ) 

231 

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

233 rows = self.get_task_html_rows(req) 

234 

235 html = """ 

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

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

238 {tr_is_complete} 

239 {ability_subscale} 

240 {motivation_subscale} 

241 </table> 

242 </div> 

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

244 {rows} 

245 </table> 

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

247 [1] {ability_footnote} 

248 [2] {motivation_footnote} 

249 </div> 

250 """.format( 

251 CssClass=CssClass, 

252 tr_is_complete=self.get_is_complete_tr(req), 

253 ability_subscale=tr( 

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

255 answer( 

256 ws.number_to_dp(self.ability_subscale(), DP, default=None) 

257 ) 

258 + f" / {self.MAX_SCORE}", 

259 ), 

260 motivation_subscale=tr( 

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

262 answer( 

263 ws.number_to_dp( 

264 self.motivation_subscale(), DP, default=None 

265 ), 

266 ) 

267 + f" / {self.MAX_SCORE}", 

268 ), 

269 rows=rows, 

270 ability_footnote=self.wxstring(req, "ability_footnote"), 

271 motivation_footnote=self.wxstring(req, "motivation_footnote"), 

272 ) 

273 return html 

274 

275 def get_task_html_rows(self, req: "CamcopsRequest") -> str: 

276 task_text = self.xstring(req, "task") 

277 ability_text = self.xstring(req, "ability") 

278 motivation_text = self.xstring(req, "motivation") 

279 comments_text = self.xstring(req, "comments") 

280 header = f""" 

281 <tr> 

282 <th width="2%"></th> 

283 <th width="41%">{task_text}</th> 

284 <th width="8%">{ability_text}</th> 

285 <th width="8%">{motivation_text}</th> 

286 <th width="41%">{comments_text}</th> 

287 </tr> 

288 """ 

289 return header + self.get_task_html_rows_for_range( 

290 req, self.FIRST_Q, self.LAST_Q 

291 ) 

292 

293 def get_task_html_rows_for_range( 

294 self, req: "CamcopsRequest", first_q: int, last_q: int 

295 ) -> str: 

296 rows = "" 

297 for q_num in range(first_q, last_q + 1): 

298 q_str = f"{self.PREFIX}{q_num}" 

299 ability_field = f"{q_str}{self.ABILITY_SUFFIX}" 

300 motivation_field = f"{q_str}{self.MOTIVATION_SUFFIX}" 

301 comments_field = f"{q_str}{self.COMMENTS_SUFFIX}" 

302 question_cell = self.xstring(req, q_str) 

303 

304 rows += tr( 

305 str(q_num), 

306 question_cell, 

307 answer(getattr(self, ability_field)), 

308 answer(getattr(self, motivation_field)), 

309 answer(getattr(self, comments_field), default=""), 

310 ) 

311 

312 return rows