Coverage for tasks/perinatalpoem.py: 56%

235 statements  

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

1""" 

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

28import re 

29from typing import Any, Dict, List, Optional, Tuple, Type 

30 

31from cardinal_pythonlib.classes import classproperty 

32from pyramid.renderers import render_to_response 

33from pyramid.response import Response 

34from sqlalchemy import Select 

35from sqlalchemy.orm import Mapped, mapped_column 

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

37from sqlalchemy.sql.sqltypes import UnicodeText 

38 

39from camcops_server.cc_modules.cc_constants import CssClass 

40 

41from camcops_server.cc_modules.cc_html import ( 

42 get_yes_no_none, 

43 subheading_spanning_two_columns, 

44 tr_qa, 

45) 

46from camcops_server.cc_modules.cc_report import ( 

47 DateTimeFilteredReportMixin, 

48 PercentageSummaryReportMixin, 

49 Report, 

50) 

51from camcops_server.cc_modules.cc_request import CamcopsRequest 

52from camcops_server.cc_modules.cc_sqla_coltypes import ( 

53 mapped_camcops_column, 

54 ZERO_TO_ONE_CHECKER, 

55 ONE_TO_TWO_CHECKER, 

56 ONE_TO_FIVE_CHECKER, 

57 ONE_TO_FOUR_CHECKER, 

58) 

59from camcops_server.cc_modules.cc_task import get_from_dict, Task 

60from camcops_server.cc_modules.cc_text import SS 

61from camcops_server.cc_modules.cc_spreadsheet import SpreadsheetPage 

62 

63 

64# ============================================================================= 

65# Perinatal-POEM 

66# ============================================================================= 

67 

68 

69class PerinatalPoem(Task): 

70 """ 

71 Server implementation of the Perinatal-POEM task. 

72 """ 

73 

74 __tablename__ = "perinatal_poem" 

75 shortname = "Perinatal-POEM" 

76 provides_trackers = False 

77 

78 # Field names 

79 FN_QA_RESPONDENT = "qa" 

80 FN_QB_SERVICE_TYPE = "qb" 

81 FN_Q1A_MH_FIRST_CONTACT = "q1a" 

82 FN_Q1B_MH_DISCHARGE = "q1b" 

83 FN_Q2A_STAFF_DID_NOT_COMMUNICATE = "q2a" 

84 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT = "q2b" 

85 FN_Q2C_HELP_NOT_QUICK_ENOUGH = "q2c" 

86 FN_Q2D_STAFF_LISTENED = "q2d" 

87 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME = "q2e" 

88 FN_Q2F_SERVICE_PROVIDED_INFO = "q2f" 

89 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME = "q2g" 

90 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND = "q2h" 

91 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY = "q2i" 

92 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE = "q2j" 

93 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY = "q2k" 

94 FN_Q2L_I_WOULD_RECOMMEND_SERVICE = "q2l" 

95 FN_Q3A_UNIT_CLEAN = "q3a" 

96 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER = "q3b" 

97 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES = "q3c" 

98 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY = "q3d" 

99 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT = "q3e" 

100 FN_Q3F_FOOD_NOT_ACCEPTABLE = "q3f" 

101 FN_GENERAL_COMMENTS = "general_comments" 

102 FN_FUTURE_PARTICIPATION = "future_participation" 

103 FN_CONTACT_DETAILS = "contact_details" 

104 

105 # Response values 

106 VAL_QA_PATIENT = 1 

107 VAL_QA_PARTNER_OTHER = 2 

108 

109 VAL_QB_INPATIENT = 1 # inpatient = MBU = mother and baby unit 

110 VAL_QB_COMMUNITY = 2 

111 

112 VAL_Q1_VERY_WELL = 1 

113 VAL_Q1_WELL = 2 

114 VAL_Q1_UNWELL = 3 

115 VAL_Q1_VERY_UNWELL = 4 

116 VAL_Q1_EXTREMELY_UNWELL = 5 

117 _MH_KEY = ( 

118 f"({VAL_Q1_VERY_WELL} very well, {VAL_Q1_WELL} well, " 

119 f"{VAL_Q1_UNWELL} unwell, {VAL_Q1_VERY_UNWELL} very unwell, " 

120 f"{VAL_Q1_EXTREMELY_UNWELL} extremely unwell)" 

121 ) 

122 

123 VAL_STRONGLY_AGREE = 1 

124 VAL_AGREE = 2 

125 VAL_DISAGREE = 3 

126 VAL_STRONGLY_DISAGREE = 4 

127 _AGREE_KEY = ( 

128 f"({VAL_STRONGLY_AGREE} strongly agree, {VAL_AGREE} agree, " 

129 f"{VAL_DISAGREE} disagree, {VAL_STRONGLY_DISAGREE} strongly disagree)" 

130 ) 

131 

132 _INPATIENT_ONLY = "[Inpatient services only]" 

133 

134 YES_INT = 1 

135 NO_INT = 0 

136 

137 # ------------------------------------------------------------------------- 

138 # Fields 

139 # ------------------------------------------------------------------------- 

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

141 permitted_value_checker=ONE_TO_TWO_CHECKER, 

142 comment=( 

143 f"Question A: Is the respondent the patient ({VAL_QA_PATIENT}) " 

144 f"or other ({VAL_QA_PARTNER_OTHER})?" 

145 ), 

146 ) 

147 qb: Mapped[Optional[int]] = mapped_camcops_column( 

148 permitted_value_checker=ONE_TO_TWO_CHECKER, 

149 comment=( 

150 f"Question B: Was the service type inpatient [mother-and-baby " 

151 f"unit, MBU] ({VAL_QB_INPATIENT}) or " 

152 f"community ({VAL_QB_COMMUNITY})?" 

153 ), 

154 ) 

155 

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

157 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

158 comment=f"Q1A: mental health at first contact {_MH_KEY}", 

159 ) 

160 q1b: Mapped[Optional[int]] = mapped_camcops_column( 

161 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

162 comment=f"Q1B: mental health at discharge {_MH_KEY}", 

163 ) 

164 

165 q2a: Mapped[Optional[int]] = mapped_camcops_column( 

166 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

167 comment=f"Q2a: staff didn't communicate with others {_AGREE_KEY}", 

168 ) 

169 q2b: Mapped[Optional[int]] = mapped_camcops_column( 

170 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

171 comment=f"Q2b: Staff gave right amount of support {_AGREE_KEY}", 

172 ) 

173 q2c: Mapped[Optional[int]] = mapped_camcops_column( 

174 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

175 comment=f"Q2c: Help not quick enough after referral {_AGREE_KEY}", 

176 ) 

177 q2d: Mapped[Optional[int]] = mapped_camcops_column( 

178 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

179 comment=f"Q2d: Staff listened/understood {_AGREE_KEY}", 

180 ) 

181 

182 q2e: Mapped[Optional[int]] = mapped_camcops_column( 

183 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

184 comment=f"Q2e: Staff didn't involve pt enough {_AGREE_KEY}", 

185 ) 

186 q2f: Mapped[Optional[int]] = mapped_camcops_column( 

187 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

188 comment=f"Q2f: Service provided information {_AGREE_KEY}", 

189 ) 

190 q2g: Mapped[Optional[int]] = mapped_camcops_column( 

191 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

192 comment=f"Q2g: Staff not very sensitive to pt {_AGREE_KEY}", 

193 ) 

194 q2h: Mapped[Optional[int]] = mapped_camcops_column( 

195 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

196 comment=f"Q2h: Staff helped understanding of illness {_AGREE_KEY}", 

197 ) 

198 

199 q2i: Mapped[Optional[int]] = mapped_camcops_column( 

200 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

201 comment=f"Q2i: Staff not very sensitive to baby {_AGREE_KEY}", 

202 ) 

203 q2j: Mapped[Optional[int]] = mapped_camcops_column( 

204 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

205 comment=f"Q2j: Staff helped confidence re baby {_AGREE_KEY}", 

206 ) 

207 q2k: Mapped[Optional[int]] = mapped_camcops_column( 

208 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

209 comment=f"Q2k: Service involved others helpfully {_AGREE_KEY}", 

210 ) 

211 q2l: Mapped[Optional[int]] = mapped_camcops_column( 

212 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

213 comment=f"Q2l: Would recommend service {_AGREE_KEY}", 

214 ) 

215 

216 q3a: Mapped[Optional[int]] = mapped_camcops_column( 

217 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

218 comment=f"Q3a: MBU clean {_AGREE_KEY} {_INPATIENT_ONLY}", 

219 ) 

220 q3b: Mapped[Optional[int]] = mapped_camcops_column( 

221 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

222 comment=f"Q3b: MBU not a good place to recover " 

223 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

224 ) 

225 q3c: Mapped[Optional[int]] = mapped_camcops_column( 

226 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

227 comment=f"Q3c: MBU did not provide helpful activities " 

228 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

229 ) 

230 q3d: Mapped[Optional[int]] = mapped_camcops_column( 

231 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

232 comment=f"Q3d: MBU a good place for baby to be with pt " 

233 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

234 ) 

235 q3e: Mapped[Optional[int]] = mapped_camcops_column( 

236 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

237 comment=f"Q3e: MBU supported contact with family/friends " 

238 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

239 ) 

240 q3f: Mapped[Optional[int]] = mapped_camcops_column( 

241 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

242 comment=f"Q3f: Food not acceptable {_AGREE_KEY} {_INPATIENT_ONLY}", 

243 ) 

244 

245 general_comments: Mapped[Optional[str]] = mapped_column( 

246 FN_GENERAL_COMMENTS, UnicodeText, comment="General comments" 

247 ) 

248 future_participation: Mapped[Optional[int]] = mapped_camcops_column( 

249 permitted_value_checker=ZERO_TO_ONE_CHECKER, 

250 comment=f"Willing to participate in future studies " 

251 f"({YES_INT} yes, {NO_INT} no)", 

252 ) 

253 contact_details: Mapped[Optional[str]] = mapped_column( 

254 FN_CONTACT_DETAILS, UnicodeText, comment="Contact details" 

255 ) 

256 

257 # ------------------------------------------------------------------------- 

258 # Fieldname collections 

259 # ------------------------------------------------------------------------- 

260 REQUIRED_ALWAYS = [ 

261 FN_QA_RESPONDENT, 

262 FN_QB_SERVICE_TYPE, 

263 FN_Q1A_MH_FIRST_CONTACT, 

264 FN_Q1B_MH_DISCHARGE, 

265 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, 

266 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, 

267 FN_Q2C_HELP_NOT_QUICK_ENOUGH, 

268 FN_Q2D_STAFF_LISTENED, 

269 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, 

270 FN_Q2F_SERVICE_PROVIDED_INFO, 

271 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, 

272 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, 

273 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, 

274 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, 

275 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, 

276 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, 

277 # not FN_GENERAL_COMMENTS, 

278 FN_FUTURE_PARTICIPATION, 

279 # not FN_CONTACT_DETAILS, 

280 ] 

281 REQUIRED_INPATIENT = [ 

282 FN_Q3A_UNIT_CLEAN, 

283 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, 

284 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, 

285 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, 

286 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, 

287 FN_Q3F_FOOD_NOT_ACCEPTABLE, 

288 ] 

289 Q1_FIELDS = [FN_Q1A_MH_FIRST_CONTACT, FN_Q1B_MH_DISCHARGE] 

290 Q2_FIELDS = [ 

291 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, 

292 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, 

293 FN_Q2C_HELP_NOT_QUICK_ENOUGH, 

294 FN_Q2D_STAFF_LISTENED, 

295 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, 

296 FN_Q2F_SERVICE_PROVIDED_INFO, 

297 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, 

298 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, 

299 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, 

300 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, 

301 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, 

302 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, 

303 ] 

304 Q3_FIELDS = REQUIRED_INPATIENT 

305 

306 @staticmethod 

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

308 _ = req.gettext 

309 return _("Perinatal Patient-rated Outcome and Experience Measure") 

310 

311 def was_inpatient(self) -> bool: 

312 return self.qb == self.VAL_QB_INPATIENT 

313 

314 def respondent_not_patient(self) -> bool: 

315 return self.qa == self.VAL_QA_PARTNER_OTHER 

316 

317 def offering_participation(self) -> bool: 

318 return self.future_participation == self.YES_INT 

319 

320 def is_complete(self) -> bool: 

321 if self.any_fields_none(self.REQUIRED_ALWAYS): 

322 return False 

323 if self.was_inpatient() and self.any_fields_none( 

324 self.REQUIRED_INPATIENT 

325 ): 

326 return False 

327 if not self.field_contents_valid(): 

328 return False 

329 return True 

330 

331 def get_qa_options(self, req: CamcopsRequest) -> List[str]: 

332 options = [ 

333 self.wxstring(req, f"qa_a{o}") 

334 for o in range(self.VAL_QA_PATIENT, self.VAL_QA_PARTNER_OTHER + 1) 

335 ] 

336 

337 return options 

338 

339 def get_qb_options(self, req: CamcopsRequest) -> List[str]: 

340 options = [ 

341 self.wxstring(req, f"qb_a{o}") 

342 for o in range(self.VAL_QB_INPATIENT, self.VAL_QB_COMMUNITY + 1) 

343 ] 

344 

345 return options 

346 

347 def get_q1_options(self, req: CamcopsRequest) -> List[str]: 

348 options = [ 

349 self.wxstring(req, f"q1_a{o}") 

350 for o in range( 

351 self.VAL_Q1_VERY_WELL, self.VAL_Q1_EXTREMELY_UNWELL + 1 

352 ) 

353 ] 

354 

355 return options 

356 

357 def get_agree_options(self, req: CamcopsRequest) -> List[str]: 

358 options = [ 

359 self.wxstring(req, f"agreement_a{o}") 

360 for o in range( 

361 self.VAL_STRONGLY_AGREE, self.VAL_STRONGLY_DISAGREE + 1 

362 ) 

363 ] 

364 

365 return options 

366 

367 @staticmethod 

368 def get_yn_options(req: CamcopsRequest) -> List[str]: 

369 return [req.sstring(SS.NO), req.sstring(SS.YES)] 

370 

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

372 def loadvalues( 

373 _dict: Dict[int, str], _first: int, _last: int, _xstringprefix: str 

374 ) -> None: 

375 for val in range(_first, _last + 1): 

376 _dict[val] = ( 

377 f"{val} — {self.wxstring(req, f'{_xstringprefix}{val}')}" 

378 ) 

379 

380 respondent_dict = {} # type: Dict[int, str] 

381 loadvalues( 

382 respondent_dict, 

383 self.VAL_QA_PATIENT, 

384 self.VAL_QA_PARTNER_OTHER, 

385 "qa_a", 

386 ) 

387 service_dict = {} # type: Dict[int, str] 

388 loadvalues( 

389 service_dict, self.VAL_QB_INPATIENT, self.VAL_QB_COMMUNITY, "qb_a" 

390 ) 

391 mh_dict = {} # type: Dict[int, str] 

392 loadvalues( 

393 mh_dict, 

394 self.VAL_Q1_VERY_WELL, 

395 self.VAL_Q1_EXTREMELY_UNWELL, 

396 "q1_a", 

397 ) 

398 agree_dict = {} # type: Dict[int, str] 

399 loadvalues( 

400 agree_dict, 

401 self.VAL_STRONGLY_AGREE, 

402 self.VAL_STRONGLY_DISAGREE, 

403 "agreement_a", 

404 ) 

405 

406 q_a_list = [] # type: List[str] 

407 

408 def addqa(_fieldname: str, _valuedict: Dict[int, str]) -> None: 

409 xstringname = _fieldname + "_q" 

410 q_a_list.append( 

411 tr_qa( 

412 self.xstring(req, xstringname), # not wxstring 

413 get_from_dict(_valuedict, getattr(self, _fieldname)), 

414 ) 

415 ) 

416 

417 def subheading(_xstringname: str) -> None: 

418 q_a_list.append( 

419 subheading_spanning_two_columns( 

420 self.wxstring(req, _xstringname) 

421 ) 

422 ) 

423 

424 # Preamble 

425 addqa(self.FN_QA_RESPONDENT, respondent_dict) 

426 addqa(self.FN_QB_SERVICE_TYPE, service_dict) 

427 # The bulk 

428 subheading("q1_stem") 

429 for fieldname in self.Q1_FIELDS: 

430 addqa(fieldname, mh_dict) 

431 subheading("q2_stem") 

432 for fieldname in self.Q2_FIELDS: 

433 addqa(fieldname, agree_dict) 

434 if self.was_inpatient(): 

435 subheading("q3_stem") 

436 for fieldname in self.Q3_FIELDS: 

437 addqa(fieldname, agree_dict) 

438 # General 

439 q_a_list.append( 

440 subheading_spanning_two_columns(req.sstring(SS.GENERAL)) 

441 ) 

442 q_a_list.append( 

443 tr_qa( 

444 self.wxstring(req, "general_comments_q"), self.general_comments 

445 ) 

446 ) 

447 q_a_list.append( 

448 tr_qa( 

449 self.wxstring(req, "participation_q"), 

450 get_yes_no_none(req, self.future_participation), 

451 ) 

452 ) 

453 if self.offering_participation(): 

454 q_a_list.append( 

455 tr_qa( 

456 self.wxstring(req, "contact_details_q"), 

457 self.contact_details, 

458 ) 

459 ) 

460 

461 q_a = "\n".join(q_a_list) 

462 return f""" 

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

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

465 {self.get_is_complete_tr(req)} 

466 </table> 

467 </div> 

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

469 <tr> 

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

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

472 </tr> 

473 {q_a} 

474 </table> 

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

476 </div> 

477 """ 

478 

479 # No SNOMED codes for Perinatal-POEM. 

480 

481 

482# ============================================================================= 

483# Reports 

484# ============================================================================= 

485 

486 

487class PerinatalPoemReportTableConfig(object): 

488 def __init__( 

489 self, 

490 heading: str, 

491 column_headings: List[str], 

492 fieldnames: List[str], 

493 min_answer: int = 0, 

494 xstring_format: str = "{}_q", 

495 ) -> None: 

496 self.heading = heading 

497 self.column_headings = column_headings 

498 self.fieldnames = fieldnames 

499 self.min_answer = min_answer 

500 self.xstring_format = xstring_format 

501 

502 

503class PerinatalPoemReportTable(object): 

504 def __init__( 

505 self, 

506 req: "CamcopsRequest", 

507 heading: str, 

508 column_headings: List[str], 

509 rows: List[List[str]], 

510 ) -> None: 

511 _ = req.gettext 

512 self.heading = heading 

513 

514 common_headings = [_("Question"), _("Total responses")] 

515 self.column_headings = common_headings + column_headings 

516 self.rows = rows 

517 

518 

519class PerinatalPoemReport( 

520 DateTimeFilteredReportMixin, Report, PercentageSummaryReportMixin 

521): 

522 """ 

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

524 Then a summary of the comments. 

525 """ 

526 

527 HTML_TAG_RE = re.compile(r"<[^>]+>") 

528 

529 def __init__(self, *args: Any, **kwargs: Any): 

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

531 self.task = PerinatalPoem() # dummy task, never written to DB 

532 

533 @classproperty 

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

535 return PerinatalPoem 

536 

537 # noinspection PyMethodParameters 

538 @classproperty 

539 def report_id(cls) -> str: 

540 return "perinatal_poem" 

541 

542 @classmethod 

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

544 _ = req.gettext 

545 return _("Perinatal-POEM — Question summaries") 

546 

547 # noinspection PyMethodParameters 

548 @classproperty 

549 def superuser_only(cls) -> bool: 

550 return False 

551 

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

553 return render_to_response( 

554 "perinatal_poem_report.mako", 

555 dict( 

556 title=self.title(req), 

557 report_id=self.report_id, 

558 start_datetime=self.start_datetime, 

559 end_datetime=self.end_datetime, 

560 tables=self._get_html_tables(req), 

561 comments=self._get_comments(req), 

562 ), 

563 request=req, 

564 ) 

565 

566 def get_spreadsheet_pages( 

567 self, req: "CamcopsRequest" 

568 ) -> List[SpreadsheetPage]: 

569 _ = req.gettext 

570 

571 pages = [] 

572 

573 for table in self._get_spreadsheet_tables(req): 

574 pages.append( 

575 self.get_spreadsheet_page( 

576 name=table.heading, 

577 column_names=table.column_headings, 

578 rows=table.rows, 

579 ) 

580 ) 

581 

582 pages.append( 

583 self.get_spreadsheet_page( 

584 name=_("Comments"), 

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

586 rows=self._get_comment_rows(req), 

587 ) 

588 ) 

589 

590 return pages 

591 

592 def _get_html_tables( 

593 self, req: "CamcopsRequest" 

594 ) -> List["PerinatalPoemReportTable"]: 

595 

596 return [ 

597 self._get_html_table(req, config) 

598 for config in self._get_table_configs(req) 

599 ] 

600 

601 def _get_spreadsheet_tables( 

602 self, req: "CamcopsRequest" 

603 ) -> List["PerinatalPoemReportTable"]: 

604 

605 return [ 

606 self._get_spreadsheet_table(req, config) 

607 for config in self._get_table_configs(req) 

608 ] 

609 

610 def _get_table_configs( 

611 self, req: "CamcopsRequest" 

612 ) -> List["PerinatalPoemReportTableConfig"]: 

613 return [ 

614 PerinatalPoemReportTableConfig( 

615 heading=self.task.xstring(req, "qa_q"), 

616 column_headings=self.task.get_qa_options(req), 

617 fieldnames=["qa"], 

618 min_answer=1, 

619 ), 

620 PerinatalPoemReportTableConfig( 

621 heading=self.task.xstring(req, "qb_q"), 

622 column_headings=self.task.get_qb_options(req), 

623 fieldnames=["qb"], 

624 min_answer=1, 

625 ), 

626 PerinatalPoemReportTableConfig( 

627 heading=self.task.xstring(req, "q1_stem"), 

628 column_headings=self.task.get_q1_options(req), 

629 fieldnames=PerinatalPoem.Q1_FIELDS, 

630 min_answer=1, 

631 ), 

632 PerinatalPoemReportTableConfig( 

633 heading=self.task.xstring(req, "q2_stem"), 

634 column_headings=self.task.get_agree_options(req), 

635 fieldnames=PerinatalPoem.Q2_FIELDS, 

636 min_answer=1, 

637 ), 

638 PerinatalPoemReportTableConfig( 

639 heading=self.task.xstring(req, "q3_stem"), 

640 column_headings=self.task.get_agree_options(req), 

641 fieldnames=PerinatalPoem.Q3_FIELDS, 

642 min_answer=1, 

643 ), 

644 PerinatalPoemReportTableConfig( 

645 heading=self.task.xstring(req, "participation_q"), 

646 column_headings=self.task.get_yn_options(req), 

647 fieldnames=["future_participation"], 

648 xstring_format="participation_q", 

649 ), 

650 ] 

651 

652 def _get_html_table( 

653 self, req: "CamcopsRequest", config: PerinatalPoemReportTableConfig 

654 ) -> PerinatalPoemReportTable: 

655 column_dict = {} 

656 

657 for fieldname in config.fieldnames: 

658 column_dict[fieldname] = self.task.xstring( 

659 req, config.xstring_format.format(fieldname) 

660 ) 

661 

662 rows = self.get_percentage_summaries( 

663 req, 

664 column_dict=column_dict, 

665 num_answers=len(config.column_headings), 

666 cell_format="{0:.1f}%", 

667 min_answer=config.min_answer, 

668 ) 

669 

670 return PerinatalPoemReportTable( 

671 req, 

672 heading=config.heading, 

673 column_headings=config.column_headings, 

674 rows=rows, 

675 ) 

676 

677 def _get_spreadsheet_table( 

678 self, req: "CamcopsRequest", config: PerinatalPoemReportTableConfig 

679 ) -> PerinatalPoemReportTable: 

680 column_dict = {} 

681 

682 for fieldname in config.fieldnames: 

683 column_dict[fieldname] = self._strip_tags( 

684 self.task.xstring(req, config.xstring_format.format(fieldname)) 

685 ) 

686 

687 rows = self.get_percentage_summaries( 

688 req, 

689 column_dict=column_dict, 

690 num_answers=len(config.column_headings), 

691 min_answer=config.min_answer, 

692 ) 

693 

694 return PerinatalPoemReportTable( 

695 req, 

696 heading=config.heading, 

697 column_headings=config.column_headings, 

698 rows=rows, 

699 ) 

700 

701 def _strip_tags(self, text: str) -> str: 

702 return self.HTML_TAG_RE.sub("", text) 

703 

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

705 """ 

706 A list of all the additional comments 

707 """ 

708 

709 wheres = [column("general_comments").isnot(None)] 

710 

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

712 

713 # noinspection PyUnresolvedReferences 

714 query: Select[Any] = ( 

715 select(column("general_comments")) 

716 .select_from(self.task.__table__) 

717 .where(and_(*wheres)) 

718 ) 

719 

720 comment_rows = [] 

721 

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

723 comment_rows.append(result) 

724 

725 return comment_rows 

726 

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

728 """ 

729 A list of all the additional comments. 

730 """ 

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