Coverage for tasks/apeq_cpft_perinatal.py: 48%

130 statements  

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

1""" 

2camcops_server/tasks/apeq_cpft_perinatal.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 typing import Any, Dict, List, Optional, Tuple, Type 

29 

30from cardinal_pythonlib.classes import classproperty 

31 

32from pyramid.renderers import render_to_response 

33from pyramid.response import Response 

34from sqlalchemy.orm import Mapped, mapped_column 

35from sqlalchemy.sql.expression import and_, column, select 

36from sqlalchemy.sql.sqltypes import UnicodeText 

37 

38from camcops_server.cc_modules.cc_constants import CssClass 

39from camcops_server.cc_modules.cc_html import tr_qa 

40from camcops_server.cc_modules.cc_pyramid import ViewParam 

41from camcops_server.cc_modules.cc_report import ( 

42 DateTimeFilteredReportMixin, 

43 PercentageSummaryReportMixin, 

44 Report, 

45) 

46from camcops_server.cc_modules.cc_request import CamcopsRequest 

47from camcops_server.cc_modules.cc_sqla_coltypes import ( 

48 mapped_camcops_column, 

49 ZERO_TO_FIVE_CHECKER, 

50 ZERO_TO_TWO_CHECKER, 

51) 

52from camcops_server.cc_modules.cc_task import Task 

53from camcops_server.cc_modules.cc_spreadsheet import SpreadsheetPage 

54 

55 

56# ============================================================================= 

57# APEQCPFTPerinatal 

58# ============================================================================= 

59 

60 

61class APEQCPFTPerinatal(Task): 

62 """ 

63 Server implementation of the APEQ-CPFT-Perinatal task. 

64 """ 

65 

66 __tablename__ = "apeq_cpft_perinatal" 

67 shortname = "APEQ-CPFT-Perinatal" 

68 

69 FIRST_MAIN_Q = 1 

70 LAST_MAIN_Q = 6 

71 FN_QPREFIX = "q" 

72 MAIN_EXPLANATION = " (0 no, 1 yes to some extent, 2 yes)" 

73 

74 q1: Mapped[Optional[int]] = mapped_camcops_column( 

75 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

76 comment="Q1. Treated with respect/dignity" + MAIN_EXPLANATION, 

77 ) 

78 q2: Mapped[Optional[int]] = mapped_camcops_column( 

79 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

80 comment="Q2. Felt listened to" + MAIN_EXPLANATION, 

81 ) 

82 q3: Mapped[Optional[int]] = mapped_camcops_column( 

83 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

84 comment="Q3. Needs were understood" + MAIN_EXPLANATION, 

85 ) 

86 q4: Mapped[Optional[int]] = mapped_camcops_column( 

87 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

88 comment="Q4. Given info about team" + MAIN_EXPLANATION, 

89 ) 

90 q5: Mapped[Optional[int]] = mapped_camcops_column( 

91 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

92 comment="Q5. Family considered/included" + MAIN_EXPLANATION, 

93 ) 

94 q6: Mapped[Optional[int]] = mapped_camcops_column( 

95 permitted_value_checker=ZERO_TO_TWO_CHECKER, 

96 comment="Q6. Views on treatment taken into account" + MAIN_EXPLANATION, 

97 ) 

98 ff_rating: Mapped[Optional[int]] = mapped_camcops_column( 

99 permitted_value_checker=ZERO_TO_FIVE_CHECKER, 

100 comment="How likely to recommend service to friends and family " 

101 "(0 don't know, 1 extremely unlikely, 2 unlikely, " 

102 "3 neither likely nor unlikely, 4 likely, 5 extremely likely)", 

103 ) 

104 ff_why: Mapped[Optional[str]] = mapped_column( 

105 UnicodeText, 

106 comment="Why was friends/family rating given as it was?", 

107 ) 

108 comments: Mapped[Optional[str]] = mapped_column( 

109 UnicodeText, comment="General comments" 

110 ) 

111 

112 REQUIRED_FIELDS = ["q1", "q2", "q3", "q4", "q5", "q6", "ff_rating"] 

113 

114 @staticmethod 

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

116 _ = req.gettext 

117 return _( 

118 "Assessment Patient Experience Questionnaire for " 

119 "CPFT Perinatal Services" 

120 ) 

121 

122 def is_complete(self) -> bool: 

123 return self.all_fields_not_none(self.REQUIRED_FIELDS) 

124 

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

126 options_main = {None: "?"} # type: Dict[Optional[int], str] 

127 for o in range(0, 2 + 1): 

128 options_main[o] = self.wxstring(req, f"main_a{o}") 

129 options_ff = {None: "?"} # type: Dict[Optional[int], str] 

130 for o in range(0, 5 + 1): 

131 options_ff[o] = self.wxstring(req, f"ff_a{o}") 

132 

133 qlines = [] # type: List[str] 

134 for qnum in range(self.FIRST_MAIN_Q, self.LAST_MAIN_Q + 1): 

135 xstring_attr_name = f"q{qnum}" 

136 qlines.append( 

137 tr_qa( 

138 self.wxstring(req, xstring_attr_name), 

139 options_main.get(getattr(self, xstring_attr_name)), 

140 ) 

141 ) 

142 q_a = "".join(qlines) 

143 return f""" 

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

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

146 {self.get_is_complete_tr(req)} 

147 </table> 

148 </div> 

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

150 <tr> 

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

152 <th width="40%">Answer</th> 

153 </tr> 

154 {q_a} 

155 {tr_qa(self.wxstring(req, "q_ff_rating"), 

156 options_ff.get(self.ff_rating))} 

157 {tr_qa(self.wxstring(req, "q_ff_why"), 

158 self.ff_why or "")} 

159 {tr_qa(self.wxstring(req, "q_comments"), 

160 self.comments or "")} 

161 </table> 

162 """ 

163 

164 def get_main_options(self, req: "CamcopsRequest") -> List[str]: 

165 options = [] 

166 

167 for n in range(0, 2 + 1): 

168 options.append(self.wxstring(req, f"main_a{n}")) 

169 

170 return options 

171 

172 def get_ff_options(self, req: "CamcopsRequest") -> List[str]: 

173 options = [] 

174 

175 for n in range(0, 5 + 1): 

176 options.append(self.wxstring(req, f"ff_a{n}")) 

177 

178 return options 

179 

180 

181# ============================================================================= 

182# Reports 

183# ============================================================================= 

184 

185 

186class APEQCPFTPerinatalReport( 

187 DateTimeFilteredReportMixin, Report, PercentageSummaryReportMixin 

188): 

189 """ 

190 Provides a summary of each question, x% of people said each response etc. 

191 Then a summary of the comments. 

192 """ 

193 

194 COL_Q = 0 

195 COL_TOTAL = 1 

196 COL_RESPONSE_START = 2 

197 

198 COL_FF_WHY = 1 

199 

200 def __init__(self, *args: Any, **kwargs: Any) -> None: 

201 super().__init__(*args, **kwargs) 

202 self.task = APEQCPFTPerinatal() # dummy task, never written to DB 

203 

204 @classproperty 

205 def task_class(self) -> Type["Task"]: 

206 return APEQCPFTPerinatal 

207 

208 # noinspection PyMethodParameters 

209 @classproperty 

210 def report_id(cls) -> str: 

211 return "apeq_cpft_perinatal" 

212 

213 @classmethod 

214 def title(cls, req: "CamcopsRequest") -> str: 

215 _ = req.gettext 

216 return _("APEQ CPFT Perinatal — Question summaries") 

217 

218 # noinspection PyMethodParameters 

219 @classproperty 

220 def superuser_only(cls) -> bool: 

221 return False 

222 

223 @classmethod 

224 def get_specific_http_query_keys(cls) -> List[str]: 

225 return [ViewParam.START_DATETIME, ViewParam.END_DATETIME] 

226 

227 def render_html(self, req: "CamcopsRequest") -> Response: 

228 cell_format = "{0:.1f}%" 

229 

230 return render_to_response( 

231 "apeq_cpft_perinatal_report.mako", 

232 dict( 

233 title=self.title(req), 

234 report_id=self.report_id, 

235 start_datetime=self.start_datetime, 

236 end_datetime=self.end_datetime, 

237 main_column_headings=self._get_main_column_headings(req), 

238 main_rows=self._get_main_rows(req, cell_format=cell_format), 

239 ff_column_headings=self._get_ff_column_headings(req), 

240 ff_rows=self._get_ff_rows(req, cell_format=cell_format), 

241 ff_why_rows=self._get_ff_why_rows(req), 

242 comments=self._get_comments(req), 

243 ), 

244 request=req, 

245 ) 

246 

247 def get_spreadsheet_pages( 

248 self, req: "CamcopsRequest" 

249 ) -> List[SpreadsheetPage]: 

250 _ = req.gettext 

251 

252 main_page = self.get_spreadsheet_page( 

253 name=_("Main questions"), 

254 column_names=self._get_main_column_headings(req), 

255 rows=self._get_main_rows(req), 

256 ) 

257 ff_page = self.get_spreadsheet_page( 

258 name=_("Friends and family question"), 

259 column_names=self._get_ff_column_headings(req), 

260 rows=self._get_ff_rows(req), 

261 ) 

262 ff_why_page = self.get_spreadsheet_page( 

263 name=_("Reasons given for the above responses"), 

264 column_names=[_("Response"), _("Reason")], 

265 rows=self._get_ff_why_rows(req), 

266 ) 

267 comments_page = self.get_spreadsheet_page( 

268 name=_("Comments"), 

269 column_names=[_("Comment")], 

270 rows=self._get_comment_rows(req), 

271 ) 

272 

273 return [main_page, ff_page, ff_why_page, comments_page] 

274 

275 def _get_main_column_headings(self, req: "CamcopsRequest") -> List[str]: 

276 _ = req.gettext 

277 names = [ 

278 _("Question"), 

279 _("Total responses"), 

280 ] + self.task.get_main_options(req) 

281 

282 return names 

283 

284 def _get_main_rows( 

285 self, req: "CamcopsRequest", cell_format: str = "{}" 

286 ) -> List[List[str]]: 

287 """ 

288 Percentage of people who answered x for each question 

289 """ 

290 column_dict = {} 

291 

292 qnums = range(self.task.FIRST_MAIN_Q, self.task.LAST_MAIN_Q + 1) 

293 

294 for qnum in qnums: 

295 column_name = f"{self.task.FN_QPREFIX}{qnum}" 

296 

297 column_dict[column_name] = self.task.wxstring(req, column_name) 

298 

299 return self.get_percentage_summaries( 

300 req, 

301 column_dict=column_dict, 

302 num_answers=3, 

303 cell_format=cell_format, 

304 ) 

305 

306 def _get_ff_column_headings(self, req: "CamcopsRequest") -> List[str]: 

307 _ = req.gettext 

308 return [ 

309 _("Question"), 

310 _("Total responses"), 

311 ] + self.task.get_ff_options(req) 

312 

313 def _get_ff_rows( 

314 self, req: "CamcopsRequest", cell_format: str = "{}" 

315 ) -> List[List[str]]: 

316 """ 

317 Percentage of people who answered x for the friends/family question 

318 """ 

319 return self.get_percentage_summaries( 

320 req, 

321 column_dict={ 

322 "ff_rating": self.task.wxstring( 

323 req, f"{self.task.FN_QPREFIX}_ff_rating" 

324 ) 

325 }, 

326 num_answers=6, 

327 cell_format=cell_format, 

328 ) 

329 

330 def _get_ff_why_rows(self, req: "CamcopsRequest") -> List[List[str]]: 

331 """ 

332 Reasons for giving a particular answer to the friends/family question 

333 """ 

334 

335 options = self.task.get_ff_options(req) 

336 

337 wheres = [ 

338 column("ff_rating").isnot(None), 

339 column("ff_why").isnot(None), 

340 ] 

341 

342 self.add_task_report_filters(wheres) # type: ignore[arg-type] 

343 

344 # noinspection PyUnresolvedReferences 

345 query = ( 

346 select(column("ff_rating"), column("ff_why")) # type: ignore[var-annotated] # noqa: E501 

347 .select_from(self.task.__table__) 

348 .where(and_(*wheres)) 

349 .order_by("ff_why") 

350 ) 

351 

352 rows = [] 

353 

354 for result in req.dbsession.execute(query).fetchall(): 

355 rows.append([options[result[0]], result[1]]) 

356 

357 return rows 

358 

359 def _get_comment_rows(self, req: "CamcopsRequest") -> List[Tuple[str]]: 

360 """ 

361 A list of all the additional comments, as rows. 

362 """ 

363 

364 wheres = [column("comments").isnot(None)] 

365 

366 self.add_task_report_filters(wheres) # type: ignore[arg-type] 

367 

368 # noinspection PyUnresolvedReferences 

369 query = ( 

370 select(column("comments")) # type: ignore[var-annotated] 

371 .select_from(self.task.__table__) 

372 .where(and_(*wheres)) 

373 ) 

374 

375 comment_rows = [] 

376 

377 for result in req.dbsession.execute(query).fetchall(): 

378 comment_rows.append(result) 

379 

380 return comment_rows 

381 

382 def _get_comments(self, req: "CamcopsRequest") -> List[str]: 

383 """ 

384 A list of all the additional comments. 

385 """ 

386 return [x[0] for x in self._get_comment_rows(req)]