Coverage for tasks/perinatalpoem.py: 56%

1"""

2 camcops_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

28 import re

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

30

31 from cardinal_pythonlib.classes import classproperty

32 from pyramid.renderers import render_to_response

33 from pyramid.response import Response

34 from sqlalchemy import Select

35 from sqlalchemy.orm import Mapped, mapped_column

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

37 from sqlalchemy.sql.sqltypes import UnicodeText

38

39 from camcops_server.cc_modules.cc_constants import CssClass

40

41 from camcops_server.cc_modules.cc_html import (

42 get_yes_no_none,

43 subheading_spanning_two_columns,

44 tr_qa,

45)

46 from camcops_server.cc_modules.cc_report import (

47 DateTimeFilteredReportMixin,

48 PercentageSummaryReportMixin,

49 Report,

50)

51 from camcops_server.cc_modules.cc_request import CamcopsRequest

52 from 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)

59 from camcops_server.cc_modules.cc_task import get_from_dict, Task

60 from camcops_server.cc_modules.cc_text import SS

61 from camcops_server.cc_modules.cc_spreadsheet import SpreadsheetPage

62

63

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

65# Perinatal-POEM

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

67

68

69 class 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

487 class 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

503 class 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

519 class 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)]