Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/perinatalpoem.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27""" 

28 

29import re 

30from typing import Dict, List, Tuple, Type 

31 

32from cardinal_pythonlib.classes import classproperty 

33from pyramid.renderers import render_to_response 

34from pyramid.response import Response 

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

36from sqlalchemy.sql.schema import Column 

37from sqlalchemy.sql.sqltypes import Integer, 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 CamcopsColumn, 

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 ( 

60 get_from_dict, 

61 Task, 

62) 

63from camcops_server.cc_modules.cc_text import SS 

64from camcops_server.cc_modules.cc_tsv import TsvPage 

65 

66 

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

68# Perinatal-POEM 

69# ============================================================================= 

70 

71class PerinatalPoem(Task): 

72 """ 

73 Server implementation of the Perinatal-POEM task. 

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, Integer, 

143 permitted_value_checker=ONE_TO_TWO_CHECKER, 

144 comment=( 

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

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

147 ) 

148 ) 

149 qb = CamcopsColumn( 

150 FN_QB_SERVICE_TYPE, Integer, 

151 permitted_value_checker=ONE_TO_TWO_CHECKER, 

152 comment=( 

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

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

155 f"community ({VAL_QB_COMMUNITY})?" 

156 ) 

157 ) 

158 

159 q1a = CamcopsColumn( 

160 FN_Q1A_MH_FIRST_CONTACT, Integer, 

161 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

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

163 ) 

164 q1b = CamcopsColumn( 

165 FN_Q1B_MH_DISCHARGE, Integer, 

166 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

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

168 ) 

169 

170 q2a = CamcopsColumn( 

171 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, Integer, 

172 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

174 ) 

175 q2b = CamcopsColumn( 

176 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, Integer, 

177 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

179 ) 

180 q2c = CamcopsColumn( 

181 FN_Q2C_HELP_NOT_QUICK_ENOUGH, Integer, 

182 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

184 ) 

185 q2d = CamcopsColumn( 

186 FN_Q2D_STAFF_LISTENED, Integer, 

187 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

189 ) 

190 

191 q2e = CamcopsColumn( 

192 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, Integer, 

193 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

195 ) 

196 q2f = CamcopsColumn( 

197 FN_Q2F_SERVICE_PROVIDED_INFO, Integer, 

198 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

200 ) 

201 q2g = CamcopsColumn( 

202 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, Integer, 

203 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

205 ) 

206 q2h = CamcopsColumn( 

207 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, Integer, 

208 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

210 ) 

211 

212 q2i = CamcopsColumn( 

213 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, Integer, 

214 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

216 ) 

217 q2j = CamcopsColumn( 

218 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, Integer, 

219 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

221 ) 

222 q2k = CamcopsColumn( 

223 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, Integer, 

224 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

226 ) 

227 q2l = CamcopsColumn( 

228 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, Integer, 

229 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

231 ) 

232 

233 q3a = CamcopsColumn( 

234 FN_Q3A_UNIT_CLEAN, Integer, 

235 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

237 ) 

238 q3b = CamcopsColumn( 

239 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, Integer, 

240 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

242 f"{_AGREE_KEY} {_INPATIENT_ONLY}" 

243 ) 

244 q3c = CamcopsColumn( 

245 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, Integer, 

246 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

248 f"{_AGREE_KEY} {_INPATIENT_ONLY}" 

249 ) 

250 q3d = CamcopsColumn( 

251 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, Integer, 

252 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

254 f"{_AGREE_KEY} {_INPATIENT_ONLY}" 

255 ) 

256 q3e = CamcopsColumn( 

257 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, Integer, 

258 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

260 f"{_AGREE_KEY} {_INPATIENT_ONLY}" 

261 ) 

262 q3f = CamcopsColumn( 

263 FN_Q3F_FOOD_NOT_ACCEPTABLE, Integer, 

264 permitted_value_checker=ONE_TO_FOUR_CHECKER, 

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

266 ) 

267 

268 general_comments = Column( 

269 FN_GENERAL_COMMENTS, UnicodeText, 

270 comment="General comments" 

271 ) 

272 future_participation = CamcopsColumn( 

273 FN_FUTURE_PARTICIPATION, Integer, 

274 permitted_value_checker=ZERO_TO_ONE_CHECKER, 

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

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

277 ) 

278 contact_details = Column( 

279 FN_CONTACT_DETAILS, UnicodeText, 

280 comment="Contact details" 

281 ) 

282 

283 # ------------------------------------------------------------------------- 

284 # Fieldname collections 

285 # ------------------------------------------------------------------------- 

286 REQUIRED_ALWAYS = [ 

287 FN_QA_RESPONDENT, 

288 FN_QB_SERVICE_TYPE, 

289 FN_Q1A_MH_FIRST_CONTACT, 

290 FN_Q1B_MH_DISCHARGE, 

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 # not FN_GENERAL_COMMENTS, 

304 FN_FUTURE_PARTICIPATION, 

305 # not FN_CONTACT_DETAILS, 

306 ] 

307 REQUIRED_INPATIENT = [ 

308 FN_Q3A_UNIT_CLEAN, 

309 FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, 

310 FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, 

311 FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, 

312 FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, 

313 FN_Q3F_FOOD_NOT_ACCEPTABLE, 

314 ] 

315 Q1_FIELDS = [ 

316 FN_Q1A_MH_FIRST_CONTACT, 

317 FN_Q1B_MH_DISCHARGE, 

318 ] 

319 Q2_FIELDS = [ 

320 FN_Q2A_STAFF_DID_NOT_COMMUNICATE, 

321 FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, 

322 FN_Q2C_HELP_NOT_QUICK_ENOUGH, 

323 FN_Q2D_STAFF_LISTENED, 

324 FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, 

325 FN_Q2F_SERVICE_PROVIDED_INFO, 

326 FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, 

327 FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, 

328 FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, 

329 FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, 

330 FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, 

331 FN_Q2L_I_WOULD_RECOMMEND_SERVICE, 

332 ] 

333 Q3_FIELDS = REQUIRED_INPATIENT 

334 

335 @staticmethod 

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

337 _ = req.gettext 

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

339 

340 def was_inpatient(self) -> bool: 

341 return self.qb == self.VAL_QB_INPATIENT 

342 

343 def respondent_not_patient(self) -> bool: 

344 return self.qa == self.VAL_QA_PARTNER_OTHER 

345 

346 def offering_participation(self) -> bool: 

347 return self.future_participation == self.YES_INT 

348 

349 def is_complete(self) -> bool: 

350 if self.any_fields_none(self.REQUIRED_ALWAYS): 

351 return False 

352 if (self.was_inpatient() and 

353 self.any_fields_none(self.REQUIRED_INPATIENT)): 

354 return False 

355 if not self.field_contents_valid(): 

356 return False 

357 return True 

358 

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

360 options = [self.wxstring(req, f"qa_a{o}") for o in range( 

361 self.VAL_QA_PATIENT, 

362 self.VAL_QA_PARTNER_OTHER + 1)] 

363 

364 return options 

365 

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

367 options = [self.wxstring(req, f"qb_a{o}") for o in range( 

368 self.VAL_QB_INPATIENT, 

369 self.VAL_QB_COMMUNITY + 1)] 

370 

371 return options 

372 

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

374 options = [self.wxstring(req, f"q1_a{o}") for o in range( 

375 self.VAL_Q1_VERY_WELL, 

376 self.VAL_Q1_EXTREMELY_UNWELL + 1)] 

377 

378 return options 

379 

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

381 options = [self.wxstring(req, f"agreement_a{o}") for o in range( 

382 self.VAL_STRONGLY_AGREE, 

383 self.VAL_STRONGLY_DISAGREE + 1)] 

384 

385 return options 

386 

387 @staticmethod 

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

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

390 

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

392 def loadvalues(_dict: Dict[int, str], _first: int, _last: int, 

393 _xstringprefix: str) -> None: 

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

395 _dict[val] = ( 

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

397 ) 

398 

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

400 loadvalues(respondent_dict, self.VAL_QA_PATIENT, 

401 self.VAL_QA_PARTNER_OTHER, "qa_a") 

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

403 loadvalues(service_dict, self.VAL_QB_INPATIENT, 

404 self.VAL_QB_COMMUNITY, "qb_a") 

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

406 loadvalues(mh_dict, self.VAL_Q1_VERY_WELL, 

407 self.VAL_Q1_EXTREMELY_UNWELL, "q1_a") 

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

409 loadvalues(agree_dict, self.VAL_STRONGLY_AGREE, 

410 self.VAL_STRONGLY_DISAGREE, "agreement_a") 

411 

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

413 

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

415 xstringname = _fieldname + "_q" 

416 q_a_list.append( 

417 tr_qa(self.xstring(req, xstringname), # not wxstring 

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

419 ) 

420 

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

422 q_a_list.append(subheading_spanning_two_columns( 

423 self.wxstring(req, _xstringname))) 

424 

425 # Preamble 

426 addqa(self.FN_QA_RESPONDENT, respondent_dict) 

427 addqa(self.FN_QB_SERVICE_TYPE, service_dict) 

428 # The bulk 

429 subheading("q1_stem") 

430 for fieldname in self.Q1_FIELDS: 

431 addqa(fieldname, mh_dict) 

432 subheading("q2_stem") 

433 for fieldname in self.Q2_FIELDS: 

434 addqa(fieldname, agree_dict) 

435 if self.was_inpatient(): 

436 subheading("q3_stem") 

437 for fieldname in self.Q3_FIELDS: 

438 addqa(fieldname, agree_dict) 

439 # General 

440 q_a_list.append(subheading_spanning_two_columns( 

441 req.sstring(SS.GENERAL))) 

442 q_a_list.append(tr_qa( 

443 self.wxstring(req, "general_comments_q"), 

444 self.general_comments 

445 )) 

446 q_a_list.append(tr_qa( 

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

448 get_yes_no_none(req, self.future_participation) 

449 )) 

450 if self.offering_participation(): 

451 q_a_list.append(tr_qa( 

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

453 self.contact_details 

454 )) 

455 

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

457 return f""" 

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

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

460 {self.get_is_complete_tr(req)} 

461 </table> 

462 </div> 

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

464 <tr> 

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

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

467 </tr> 

468 {q_a} 

469 </table> 

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

471 </div> 

472 """ 

473 

474 # No SNOMED codes for Perinatal-POEM. 

475 

476 

477# ============================================================================= 

478# Reports 

479# ============================================================================= 

480 

481class PerinatalPoemReportTableConfig(object): 

482 def __init__(self, 

483 heading: str, 

484 column_headings: List[str], 

485 fieldnames: List[str], 

486 min_answer: int = 0, 

487 xstring_format: str = "{}_q") -> None: 

488 self.heading = heading 

489 self.column_headings = column_headings 

490 self.fieldnames = fieldnames 

491 self.min_answer = min_answer 

492 self.xstring_format = xstring_format 

493 

494 

495class PerinatalPoemReportTable(object): 

496 def __init__(self, req: "CamcopsRequest", 

497 heading: str, 

498 column_headings: List[str], 

499 rows: List[List[str]]) -> None: 

500 _ = req.gettext 

501 self.heading = heading 

502 

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

504 self.column_headings = common_headings + column_headings 

505 self.rows = rows 

506 

507 

508class PerinatalPoemReport(DateTimeFilteredReportMixin, Report, 

509 PercentageSummaryReportMixin): 

510 """ 

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

512 Then a summary of the comments. 

513 """ 

514 HTML_TAG_RE = re.compile(r'<[^>]+>') 

515 

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

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

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

519 

520 @classproperty 

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

522 return PerinatalPoem 

523 

524 # noinspection PyMethodParameters 

525 @classproperty 

526 def report_id(cls) -> str: 

527 return "perinatal_poem" 

528 

529 @classmethod 

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

531 _ = req.gettext 

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

533 

534 # noinspection PyMethodParameters 

535 @classproperty 

536 def superuser_only(cls) -> bool: 

537 return False 

538 

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

540 return render_to_response( 

541 "perinatal_poem_report.mako", 

542 dict( 

543 title=self.title(req), 

544 report_id=self.report_id, 

545 start_datetime=self.start_datetime, 

546 end_datetime=self.end_datetime, 

547 tables=self._get_html_tables(req), 

548 comments=self._get_comments(req) 

549 ), 

550 request=req 

551 ) 

552 

553 def get_tsv_pages(self, req: "CamcopsRequest") -> List[TsvPage]: 

554 _ = req.gettext 

555 

556 pages = [] 

557 

558 for table in self._get_tsv_tables(req): 

559 pages.append( 

560 self.get_tsv_page( 

561 name=table.heading, 

562 column_names=table.column_headings, 

563 rows=table.rows 

564 ) 

565 ) 

566 

567 pages.append( 

568 self.get_tsv_page( 

569 name=_("Comments"), 

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

571 rows=self._get_comment_rows(req) 

572 ) 

573 ) 

574 

575 return pages 

576 

577 def _get_html_tables( 

578 self, req: "CamcopsRequest") -> List["PerinatalPoemReportTable"]: 

579 

580 return [ 

581 self._get_html_table(req, config) 

582 for config in self._get_table_configs(req) 

583 ] 

584 

585 def _get_tsv_tables( 

586 self, req: "CamcopsRequest") -> List["PerinatalPoemReportTable"]: 

587 

588 return [ 

589 self._get_tsv_table(req, config) 

590 for config in self._get_table_configs(req) 

591 ] 

592 

593 def _get_table_configs( 

594 self, 

595 req: "CamcopsRequest") -> List["PerinatalPoemReportTableConfig"]: 

596 return [ 

597 PerinatalPoemReportTableConfig( 

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

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

600 fieldnames=["qa"], 

601 min_answer=1 

602 ), 

603 PerinatalPoemReportTableConfig( 

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

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

606 fieldnames=["qb"], 

607 min_answer=1 

608 ), 

609 PerinatalPoemReportTableConfig( 

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

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

612 fieldnames=PerinatalPoem.Q1_FIELDS, 

613 min_answer=1 

614 ), 

615 PerinatalPoemReportTableConfig( 

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

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

618 fieldnames=PerinatalPoem.Q2_FIELDS, 

619 min_answer=1 

620 ), 

621 PerinatalPoemReportTableConfig( 

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

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

624 fieldnames=PerinatalPoem.Q3_FIELDS, 

625 min_answer=1 

626 ), 

627 PerinatalPoemReportTableConfig( 

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

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

630 fieldnames=["future_participation"], 

631 xstring_format="participation_q" 

632 ), 

633 ] 

634 

635 def _get_html_table( 

636 self, req: "CamcopsRequest", 

637 config: PerinatalPoemReportTableConfig 

638 ) -> PerinatalPoemReportTable: 

639 column_dict = {} 

640 

641 for fieldname in config.fieldnames: 

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

643 req, config.xstring_format.format(fieldname) 

644 ) 

645 

646 rows = self.get_percentage_summaries( 

647 req, 

648 column_dict=column_dict, 

649 num_answers=len(config.column_headings), 

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

651 min_answer=config.min_answer 

652 ) 

653 

654 return PerinatalPoemReportTable( 

655 req, 

656 heading=config.heading, 

657 column_headings=config.column_headings, 

658 rows=rows 

659 ) 

660 

661 def _get_tsv_table( 

662 self, req: "CamcopsRequest", 

663 config: PerinatalPoemReportTableConfig 

664 ) -> PerinatalPoemReportTable: 

665 column_dict = {} 

666 

667 for fieldname in config.fieldnames: 

668 column_dict[fieldname] = self._strip_tags( 

669 self.task.xstring( 

670 req, config.xstring_format.format(fieldname) 

671 ) 

672 ) 

673 

674 rows = self.get_percentage_summaries( 

675 req, 

676 column_dict=column_dict, 

677 num_answers=len(config.column_headings), 

678 min_answer=config.min_answer 

679 ) 

680 

681 return PerinatalPoemReportTable( 

682 req, 

683 heading=config.heading, 

684 column_headings=config.column_headings, 

685 rows=rows 

686 ) 

687 

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

689 return self.HTML_TAG_RE.sub('', text) 

690 

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

692 """ 

693 A list of all the additional comments 

694 """ 

695 

696 wheres = [ 

697 column("general_comments").isnot(None) 

698 ] 

699 

700 self.add_task_report_filters(wheres) 

701 

702 # noinspection PyUnresolvedReferences 

703 query = ( 

704 select([ 

705 column("general_comments"), 

706 ]) 

707 .select_from(self.task.__table__) 

708 .where(and_(*wheres)) 

709 ) 

710 

711 comment_rows = [] 

712 

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

714 comment_rows.append(result) 

715 

716 return comment_rows 

717 

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

719 """ 

720 A list of all the additional comments. 

721 """ 

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