Coverage for tasks/perinatalpoem.py: 56%
235 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/perinatalpoem.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28"""
30import re
31from typing import Dict, List, Tuple, Type
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
40from camcops_server.cc_modules.cc_constants import CssClass
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
65# =============================================================================
66# Perinatal-POEM
67# =============================================================================
70class PerinatalPoem(Task):
71 """
72 Server implementation of the Perinatal-POEM task.
73 """
75 __tablename__ = "perinatal_poem"
76 shortname = "Perinatal-POEM"
77 provides_trackers = False
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"
106 # Response values
107 VAL_QA_PATIENT = 1
108 VAL_QA_PARTNER_OTHER = 2
110 VAL_QB_INPATIENT = 1 # inpatient = MBU = mother and baby unit
111 VAL_QB_COMMUNITY = 2
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 )
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 )
133 _INPATIENT_ONLY = "[Inpatient services only]"
135 YES_INT = 1
136 NO_INT = 0
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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
353 @staticmethod
354 def longname(req: "CamcopsRequest") -> str:
355 _ = req.gettext
356 return _("Perinatal Patient-rated Outcome and Experience Measure")
358 def was_inpatient(self) -> bool:
359 return self.qb == self.VAL_QB_INPATIENT
361 def respondent_not_patient(self) -> bool:
362 return self.qa == self.VAL_QA_PARTNER_OTHER
364 def offering_participation(self) -> bool:
365 return self.future_participation == self.YES_INT
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
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 ]
384 return options
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 ]
392 return options
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 ]
402 return options
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 ]
412 return options
414 @staticmethod
415 def get_yn_options(req: CamcopsRequest) -> List[str]:
416 return [req.sstring(SS.NO), req.sstring(SS.YES)]
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}')}"
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 )
453 q_a_list = [] # type: List[str]
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 )
464 def subheading(_xstringname: str) -> None:
465 q_a_list.append(
466 subheading_spanning_two_columns(
467 self.wxstring(req, _xstringname)
468 )
469 )
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 )
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 """
526 # No SNOMED codes for Perinatal-POEM.
529# =============================================================================
530# Reports
531# =============================================================================
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
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
561 common_headings = [_("Question"), _("Total responses")]
562 self.column_headings = common_headings + column_headings
563 self.rows = rows
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 """
574 HTML_TAG_RE = re.compile(r"<[^>]+>")
576 def __init__(self, *args, **kwargs):
577 super().__init__(*args, **kwargs)
578 self.task = PerinatalPoem() # dummy task, never written to DB
580 @classproperty
581 def task_class(self) -> Type["Task"]:
582 return PerinatalPoem
584 # noinspection PyMethodParameters
585 @classproperty
586 def report_id(cls) -> str:
587 return "perinatal_poem"
589 @classmethod
590 def title(cls, req: "CamcopsRequest") -> str:
591 _ = req.gettext
592 return _("Perinatal-POEM — Question summaries")
594 # noinspection PyMethodParameters
595 @classproperty
596 def superuser_only(cls) -> bool:
597 return False
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 )
613 def get_spreadsheet_pages(
614 self, req: "CamcopsRequest"
615 ) -> List[SpreadsheetPage]:
616 _ = req.gettext
618 pages = []
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 )
629 pages.append(
630 self.get_spreadsheet_page(
631 name=_("Comments"),
632 column_names=[_("Comment")],
633 rows=self._get_comment_rows(req),
634 )
635 )
637 return pages
639 def _get_html_tables(
640 self, req: "CamcopsRequest"
641 ) -> List["PerinatalPoemReportTable"]:
643 return [
644 self._get_html_table(req, config)
645 for config in self._get_table_configs(req)
646 ]
648 def _get_spreadsheet_tables(
649 self, req: "CamcopsRequest"
650 ) -> List["PerinatalPoemReportTable"]:
652 return [
653 self._get_spreadsheet_table(req, config)
654 for config in self._get_table_configs(req)
655 ]
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 ]
699 def _get_html_table(
700 self, req: "CamcopsRequest", config: PerinatalPoemReportTableConfig
701 ) -> PerinatalPoemReportTable:
702 column_dict = {}
704 for fieldname in config.fieldnames:
705 column_dict[fieldname] = self.task.xstring(
706 req, config.xstring_format.format(fieldname)
707 )
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 )
717 return PerinatalPoemReportTable(
718 req,
719 heading=config.heading,
720 column_headings=config.column_headings,
721 rows=rows,
722 )
724 def _get_spreadsheet_table(
725 self, req: "CamcopsRequest", config: PerinatalPoemReportTableConfig
726 ) -> PerinatalPoemReportTable:
727 column_dict = {}
729 for fieldname in config.fieldnames:
730 column_dict[fieldname] = self._strip_tags(
731 self.task.xstring(req, config.xstring_format.format(fieldname))
732 )
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 )
741 return PerinatalPoemReportTable(
742 req,
743 heading=config.heading,
744 column_headings=config.column_headings,
745 rows=rows,
746 )
748 def _strip_tags(self, text: str) -> str:
749 return self.HTML_TAG_RE.sub("", text)
751 def _get_comment_rows(self, req: "CamcopsRequest") -> List[Tuple[str]]:
752 """
753 A list of all the additional comments
754 """
756 wheres = [column("general_comments").isnot(None)]
758 self.add_task_report_filters(wheres)
760 # noinspection PyUnresolvedReferences
761 query = (
762 select([column("general_comments")])
763 .select_from(self.task.__table__)
764 .where(and_(*wheres))
765 )
767 comment_rows = []
769 for result in req.dbsession.execute(query).fetchall():
770 comment_rows.append(result)
772 return comment_rows
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)]