Coverage for tasks/perinatalpoem.py: 56%

235 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/perinatalpoem.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30import re 

31from typing import Dict, List, Tuple, Type 

32 

33from cardinal_pythonlib.classes import classproperty 

34from pyramid.renderers import render_to_response 

35from pyramid.response import Response 

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

37from sqlalchemy.sql.schema import Column 

38from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

39 

40from camcops_server.cc_modules.cc_constants import CssClass 

41 

42from camcops_server.cc_modules.cc_html import ( 

43 get_yes_no_none, 

44 subheading_spanning_two_columns, 

45 tr_qa, 

46) 

47from camcops_server.cc_modules.cc_report import ( 

48 DateTimeFilteredReportMixin, 

49 PercentageSummaryReportMixin, 

50 Report, 

51) 

52from camcops_server.cc_modules.cc_request import CamcopsRequest 

53from camcops_server.cc_modules.cc_sqla_coltypes import ( 

54 CamcopsColumn, 

55 ZERO_TO_ONE_CHECKER, 

56 ONE_TO_TWO_CHECKER, 

57 ONE_TO_FIVE_CHECKER, 

58 ONE_TO_FOUR_CHECKER, 

59) 

60from camcops_server.cc_modules.cc_task import get_from_dict, Task 

61from camcops_server.cc_modules.cc_text import SS 

62from camcops_server.cc_modules.cc_spreadsheet import SpreadsheetPage 

63 

64 

65# ============================================================================= 

66# Perinatal-POEM 

67# ============================================================================= 

68 

69 

70class PerinatalPoem(Task): 

71 """ 

72 Server implementation of the Perinatal-POEM task. 

73 """ 

74 

75 __tablename__ = "perinatal_poem" 

76 shortname = "Perinatal-POEM" 

77 provides_trackers = False 

78 

79 # Field names 

80 FN_QA_RESPONDENT = "qa" 

81 FN_QB_SERVICE_TYPE = "qb" 

82 FN_Q1A_MH_FIRST_CONTACT = "q1a" 

83 FN_Q1B_MH_DISCHARGE = "q1b" 

84 FN_Q2A_STAFF_DID_NOT_COMMUNICATE = "q2a" 

85 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT = "q2b" 

86 FN_Q2C_HELP_NOT_QUICK_ENOUGH = "q2c" 

87 FN_Q2D_STAFF_LISTENED = "q2d" 

88 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME = "q2e" 

89 FN_Q2F_SERVICE_PROVIDED_INFO = "q2f" 

90 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME = "q2g" 

91 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND = "q2h" 

92 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY = "q2i" 

93 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE = "q2j" 

94 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY = "q2k" 

95 FN_Q2L_I_WOULD_RECOMMEND_SERVICE = "q2l" 

96 FN_Q3A_UNIT_CLEAN = "q3a" 

97 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER = "q3b" 

98 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES = "q3c" 

99 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY = "q3d" 

100 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT = "q3e" 

101 FN_Q3F_FOOD_NOT_ACCEPTABLE = "q3f" 

102 FN_GENERAL_COMMENTS = "general_comments" 

103 FN_FUTURE_PARTICIPATION = "future_participation" 

104 FN_CONTACT_DETAILS = "contact_details" 

105 

106 # Response values 

107 VAL_QA_PATIENT = 1 

108 VAL_QA_PARTNER_OTHER = 2 

109 

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

111 VAL_QB_COMMUNITY = 2 

112 

113 VAL_Q1_VERY_WELL = 1 

114 VAL_Q1_WELL = 2 

115 VAL_Q1_UNWELL = 3 

116 VAL_Q1_VERY_UNWELL = 4 

117 VAL_Q1_EXTREMELY_UNWELL = 5 

118 _MH_KEY = ( 

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

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

121 f"{VAL_Q1_EXTREMELY_UNWELL} extremely unwell)" 

122 ) 

123 

124 VAL_STRONGLY_AGREE = 1 

125 VAL_AGREE = 2 

126 VAL_DISAGREE = 3 

127 VAL_STRONGLY_DISAGREE = 4 

128 _AGREE_KEY = ( 

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

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

131 ) 

132 

133 _INPATIENT_ONLY = "[Inpatient services only]" 

134 

135 YES_INT = 1 

136 NO_INT = 0 

137 

138 # ------------------------------------------------------------------------- 

139 # Fields 

140 # ------------------------------------------------------------------------- 

141 qa = CamcopsColumn( 

142 FN_QA_RESPONDENT, 

143 Integer, 

144 permitted_value_checker=ONE_TO_TWO_CHECKER, 

145 comment=( 

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

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

148 ), 

149 ) 

150 qb = CamcopsColumn( 

151 FN_QB_SERVICE_TYPE, 

152 Integer, 

153 permitted_value_checker=ONE_TO_TWO_CHECKER, 

154 comment=( 

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

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

157 f"community ({VAL_QB_COMMUNITY})?" 

158 ), 

159 ) 

160 

161 q1a = CamcopsColumn( 

162 FN_Q1A_MH_FIRST_CONTACT, 

163 Integer, 

164 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

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

166 ) 

167 q1b = CamcopsColumn( 

168 FN_Q1B_MH_DISCHARGE, 

169 Integer, 

170 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

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

172 ) 

173 

174 q2a = CamcopsColumn( 

175 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, 

176 Integer, 

177 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

179 ) 

180 q2b = CamcopsColumn( 

181 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, 

182 Integer, 

183 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

185 ) 

186 q2c = CamcopsColumn( 

187 FN_Q2C_HELP_NOT_QUICK_ENOUGH, 

188 Integer, 

189 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

191 ) 

192 q2d = CamcopsColumn( 

193 FN_Q2D_STAFF_LISTENED, 

194 Integer, 

195 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

197 ) 

198 

199 q2e = CamcopsColumn( 

200 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, 

201 Integer, 

202 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

204 ) 

205 q2f = CamcopsColumn( 

206 FN_Q2F_SERVICE_PROVIDED_INFO, 

207 Integer, 

208 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

210 ) 

211 q2g = CamcopsColumn( 

212 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, 

213 Integer, 

214 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

216 ) 

217 q2h = CamcopsColumn( 

218 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, 

219 Integer, 

220 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

222 ) 

223 

224 q2i = CamcopsColumn( 

225 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, 

226 Integer, 

227 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

229 ) 

230 q2j = CamcopsColumn( 

231 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, 

232 Integer, 

233 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

235 ) 

236 q2k = CamcopsColumn( 

237 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, 

238 Integer, 

239 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

241 ) 

242 q2l = CamcopsColumn( 

243 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, 

244 Integer, 

245 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

247 ) 

248 

249 q3a = CamcopsColumn( 

250 FN_Q3A_UNIT_CLEAN, 

251 Integer, 

252 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

254 ) 

255 q3b = CamcopsColumn( 

256 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, 

257 Integer, 

258 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

260 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

261 ) 

262 q3c = CamcopsColumn( 

263 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, 

264 Integer, 

265 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

267 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

268 ) 

269 q3d = CamcopsColumn( 

270 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, 

271 Integer, 

272 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

274 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

275 ) 

276 q3e = CamcopsColumn( 

277 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, 

278 Integer, 

279 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

281 f"{_AGREE_KEY} {_INPATIENT_ONLY}", 

282 ) 

283 q3f = CamcopsColumn( 

284 FN_Q3F_FOOD_NOT_ACCEPTABLE, 

285 Integer, 

286 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

288 ) 

289 

290 general_comments = Column( 

291 FN_GENERAL_COMMENTS, UnicodeText, comment="General comments" 

292 ) 

293 future_participation = CamcopsColumn( 

294 FN_FUTURE_PARTICIPATION, 

295 Integer, 

296 permitted_value_checker=ZERO_TO_ONE_CHECKER, 

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

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

299 ) 

300 contact_details = Column( 

301 FN_CONTACT_DETAILS, UnicodeText, comment="Contact details" 

302 ) 

303 

304 # ------------------------------------------------------------------------- 

305 # Fieldname collections 

306 # ------------------------------------------------------------------------- 

307 REQUIRED_ALWAYS = [ 

308 FN_QA_RESPONDENT, 

309 FN_QB_SERVICE_TYPE, 

310 FN_Q1A_MH_FIRST_CONTACT, 

311 FN_Q1B_MH_DISCHARGE, 

312 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, 

313 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, 

314 FN_Q2C_HELP_NOT_QUICK_ENOUGH, 

315 FN_Q2D_STAFF_LISTENED, 

316 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, 

317 FN_Q2F_SERVICE_PROVIDED_INFO, 

318 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, 

319 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, 

320 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, 

321 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, 

322 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, 

323 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, 

324 # not FN_GENERAL_COMMENTS, 

325 FN_FUTURE_PARTICIPATION, 

326 # not FN_CONTACT_DETAILS, 

327 ] 

328 REQUIRED_INPATIENT = [ 

329 FN_Q3A_UNIT_CLEAN, 

330 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, 

331 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, 

332 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, 

333 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, 

334 FN_Q3F_FOOD_NOT_ACCEPTABLE, 

335 ] 

336 Q1_FIELDS = [FN_Q1A_MH_FIRST_CONTACT, FN_Q1B_MH_DISCHARGE] 

337 Q2_FIELDS = [ 

338 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, 

339 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, 

340 FN_Q2C_HELP_NOT_QUICK_ENOUGH, 

341 FN_Q2D_STAFF_LISTENED, 

342 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, 

343 FN_Q2F_SERVICE_PROVIDED_INFO, 

344 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, 

345 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, 

346 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, 

347 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, 

348 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, 

349 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, 

350 ] 

351 Q3_FIELDS = REQUIRED_INPATIENT 

352 

353 @staticmethod 

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

355 _ = req.gettext 

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

357 

358 def was_inpatient(self) -> bool: 

359 return self.qb == self.VAL_QB_INPATIENT 

360 

361 def respondent_not_patient(self) -> bool: 

362 return self.qa == self.VAL_QA_PARTNER_OTHER 

363 

364 def offering_participation(self) -> bool: 

365 return self.future_participation == self.YES_INT 

366 

367 def is_complete(self) -> bool: 

368 if self.any_fields_none(self.REQUIRED_ALWAYS): 

369 return False 

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

371 self.REQUIRED_INPATIENT 

372 ): 

373 return False 

374 if not self.field_contents_valid(): 

375 return False 

376 return True 

377 

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

379 options = [ 

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

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

382 ] 

383 

384 return options 

385 

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

387 options = [ 

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

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

390 ] 

391 

392 return options 

393 

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

395 options = [ 

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

397 for o in range( 

398 self.VAL_Q1_VERY_WELL, self.VAL_Q1_EXTREMELY_UNWELL + 1 

399 ) 

400 ] 

401 

402 return options 

403 

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

405 options = [ 

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

407 for o in range( 

408 self.VAL_STRONGLY_AGREE, self.VAL_STRONGLY_DISAGREE + 1 

409 ) 

410 ] 

411 

412 return options 

413 

414 @staticmethod 

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

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

417 

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

419 def loadvalues( 

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

421 ) -> None: 

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

423 _dict[ 

424 val 

425 ] = f"{val} — {self.wxstring(req, f'{_xstringprefix}{val}')}" 

426 

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

428 loadvalues( 

429 respondent_dict, 

430 self.VAL_QA_PATIENT, 

431 self.VAL_QA_PARTNER_OTHER, 

432 "qa_a", 

433 ) 

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

435 loadvalues( 

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

437 ) 

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

439 loadvalues( 

440 mh_dict, 

441 self.VAL_Q1_VERY_WELL, 

442 self.VAL_Q1_EXTREMELY_UNWELL, 

443 "q1_a", 

444 ) 

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

446 loadvalues( 

447 agree_dict, 

448 self.VAL_STRONGLY_AGREE, 

449 self.VAL_STRONGLY_DISAGREE, 

450 "agreement_a", 

451 ) 

452 

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

454 

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

456 xstringname = _fieldname + "_q" 

457 q_a_list.append( 

458 tr_qa( 

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

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

461 ) 

462 ) 

463 

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

465 q_a_list.append( 

466 subheading_spanning_two_columns( 

467 self.wxstring(req, _xstringname) 

468 ) 

469 ) 

470 

471 # Preamble 

472 addqa(self.FN_QA_RESPONDENT, respondent_dict) 

473 addqa(self.FN_QB_SERVICE_TYPE, service_dict) 

474 # The bulk 

475 subheading("q1_stem") 

476 for fieldname in self.Q1_FIELDS: 

477 addqa(fieldname, mh_dict) 

478 subheading("q2_stem") 

479 for fieldname in self.Q2_FIELDS: 

480 addqa(fieldname, agree_dict) 

481 if self.was_inpatient(): 

482 subheading("q3_stem") 

483 for fieldname in self.Q3_FIELDS: 

484 addqa(fieldname, agree_dict) 

485 # General 

486 q_a_list.append( 

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

488 ) 

489 q_a_list.append( 

490 tr_qa( 

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

492 ) 

493 ) 

494 q_a_list.append( 

495 tr_qa( 

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

497 get_yes_no_none(req, self.future_participation), 

498 ) 

499 ) 

500 if self.offering_participation(): 

501 q_a_list.append( 

502 tr_qa( 

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

504 self.contact_details, 

505 ) 

506 ) 

507 

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

509 return f""" 

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

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

512 {self.get_is_complete_tr(req)} 

513 </table> 

514 </div> 

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

516 <tr> 

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

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

519 </tr> 

520 {q_a} 

521 </table> 

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

523 </div> 

524 """ 

525 

526 # No SNOMED codes for Perinatal-POEM. 

527 

528 

529# ============================================================================= 

530# Reports 

531# ============================================================================= 

532 

533 

534class PerinatalPoemReportTableConfig(object): 

535 def __init__( 

536 self, 

537 heading: str, 

538 column_headings: List[str], 

539 fieldnames: List[str], 

540 min_answer: int = 0, 

541 xstring_format: str = "{}_q", 

542 ) -> None: 

543 self.heading = heading 

544 self.column_headings = column_headings 

545 self.fieldnames = fieldnames 

546 self.min_answer = min_answer 

547 self.xstring_format = xstring_format 

548 

549 

550class PerinatalPoemReportTable(object): 

551 def __init__( 

552 self, 

553 req: "CamcopsRequest", 

554 heading: str, 

555 column_headings: List[str], 

556 rows: List[List[str]], 

557 ) -> None: 

558 _ = req.gettext 

559 self.heading = heading 

560 

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

562 self.column_headings = common_headings + column_headings 

563 self.rows = rows 

564 

565 

566class PerinatalPoemReport( 

567 DateTimeFilteredReportMixin, Report, PercentageSummaryReportMixin 

568): 

569 """ 

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

571 Then a summary of the comments. 

572 """ 

573 

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

575 

576 def __init__(self, *args, **kwargs): 

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

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

579 

580 @classproperty 

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

582 return PerinatalPoem 

583 

584 # noinspection PyMethodParameters 

585 @classproperty 

586 def report_id(cls) -> str: 

587 return "perinatal_poem" 

588 

589 @classmethod 

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

591 _ = req.gettext 

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

593 

594 # noinspection PyMethodParameters 

595 @classproperty 

596 def superuser_only(cls) -> bool: 

597 return False 

598 

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

600 return render_to_response( 

601 "perinatal_poem_report.mako", 

602 dict( 

603 title=self.title(req), 

604 report_id=self.report_id, 

605 start_datetime=self.start_datetime, 

606 end_datetime=self.end_datetime, 

607 tables=self._get_html_tables(req), 

608 comments=self._get_comments(req), 

609 ), 

610 request=req, 

611 ) 

612 

613 def get_spreadsheet_pages( 

614 self, req: "CamcopsRequest" 

615 ) -> List[SpreadsheetPage]: 

616 _ = req.gettext 

617 

618 pages = [] 

619 

620 for table in self._get_spreadsheet_tables(req): 

621 pages.append( 

622 self.get_spreadsheet_page( 

623 name=table.heading, 

624 column_names=table.column_headings, 

625 rows=table.rows, 

626 ) 

627 ) 

628 

629 pages.append( 

630 self.get_spreadsheet_page( 

631 name=_("Comments"), 

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

633 rows=self._get_comment_rows(req), 

634 ) 

635 ) 

636 

637 return pages 

638 

639 def _get_html_tables( 

640 self, req: "CamcopsRequest" 

641 ) -> List["PerinatalPoemReportTable"]: 

642 

643 return [ 

644 self._get_html_table(req, config) 

645 for config in self._get_table_configs(req) 

646 ] 

647 

648 def _get_spreadsheet_tables( 

649 self, req: "CamcopsRequest" 

650 ) -> List["PerinatalPoemReportTable"]: 

651 

652 return [ 

653 self._get_spreadsheet_table(req, config) 

654 for config in self._get_table_configs(req) 

655 ] 

656 

657 def _get_table_configs( 

658 self, req: "CamcopsRequest" 

659 ) -> List["PerinatalPoemReportTableConfig"]: 

660 return [ 

661 PerinatalPoemReportTableConfig( 

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

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

664 fieldnames=["qa"], 

665 min_answer=1, 

666 ), 

667 PerinatalPoemReportTableConfig( 

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

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

670 fieldnames=["qb"], 

671 min_answer=1, 

672 ), 

673 PerinatalPoemReportTableConfig( 

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

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

676 fieldnames=PerinatalPoem.Q1_FIELDS, 

677 min_answer=1, 

678 ), 

679 PerinatalPoemReportTableConfig( 

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

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

682 fieldnames=PerinatalPoem.Q2_FIELDS, 

683 min_answer=1, 

684 ), 

685 PerinatalPoemReportTableConfig( 

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

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

688 fieldnames=PerinatalPoem.Q3_FIELDS, 

689 min_answer=1, 

690 ), 

691 PerinatalPoemReportTableConfig( 

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

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

694 fieldnames=["future_participation"], 

695 xstring_format="participation_q", 

696 ), 

697 ] 

698 

699 def _get_html_table( 

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

701 ) -> PerinatalPoemReportTable: 

702 column_dict = {} 

703 

704 for fieldname in config.fieldnames: 

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

706 req, config.xstring_format.format(fieldname) 

707 ) 

708 

709 rows = self.get_percentage_summaries( 

710 req, 

711 column_dict=column_dict, 

712 num_answers=len(config.column_headings), 

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

714 min_answer=config.min_answer, 

715 ) 

716 

717 return PerinatalPoemReportTable( 

718 req, 

719 heading=config.heading, 

720 column_headings=config.column_headings, 

721 rows=rows, 

722 ) 

723 

724 def _get_spreadsheet_table( 

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

726 ) -> PerinatalPoemReportTable: 

727 column_dict = {} 

728 

729 for fieldname in config.fieldnames: 

730 column_dict[fieldname] = self._strip_tags( 

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

732 ) 

733 

734 rows = self.get_percentage_summaries( 

735 req, 

736 column_dict=column_dict, 

737 num_answers=len(config.column_headings), 

738 min_answer=config.min_answer, 

739 ) 

740 

741 return PerinatalPoemReportTable( 

742 req, 

743 heading=config.heading, 

744 column_headings=config.column_headings, 

745 rows=rows, 

746 ) 

747 

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

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

750 

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

752 """ 

753 A list of all the additional comments 

754 """ 

755 

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

757 

758 self.add_task_report_filters(wheres) 

759 

760 # noinspection PyUnresolvedReferences 

761 query = ( 

762 select([column("general_comments")]) 

763 .select_from(self.task.__table__) 

764 .where(and_(*wheres)) 

765 ) 

766 

767 comment_rows = [] 

768 

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

770 comment_rows.append(result) 

771 

772 return comment_rows 

773 

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

775 """ 

776 A list of all the additional comments. 

777 """ 

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