Coverage for tasks/apeq_cpft_perinatal.py: 48%
130 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/apeq_cpft_perinatal.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28from typing import Any, Dict, List, Optional, Tuple, Type
30from cardinal_pythonlib.classes import classproperty
32from pyramid.renderers import render_to_response
33from pyramid.response import Response
34from sqlalchemy.orm import Mapped, mapped_column
35from sqlalchemy.sql.expression import and_, column, select
36from sqlalchemy.sql.sqltypes import UnicodeText
38from camcops_server.cc_modules.cc_constants import CssClass
39from camcops_server.cc_modules.cc_html import tr_qa
40from camcops_server.cc_modules.cc_pyramid import ViewParam
41from camcops_server.cc_modules.cc_report import (
42 DateTimeFilteredReportMixin,
43 PercentageSummaryReportMixin,
44 Report,
45)
46from camcops_server.cc_modules.cc_request import CamcopsRequest
47from camcops_server.cc_modules.cc_sqla_coltypes import (
48 mapped_camcops_column,
49 ZERO_TO_FIVE_CHECKER,
50 ZERO_TO_TWO_CHECKER,
51)
52from camcops_server.cc_modules.cc_task import Task
53from camcops_server.cc_modules.cc_spreadsheet import SpreadsheetPage
56# =============================================================================
57# APEQCPFTPerinatal
58# =============================================================================
61class APEQCPFTPerinatal(Task):
62 """
63 Server implementation of the APEQ-CPFT-Perinatal task.
64 """
66 __tablename__ = "apeq_cpft_perinatal"
67 shortname = "APEQ-CPFT-Perinatal"
69 FIRST_MAIN_Q = 1
70 LAST_MAIN_Q = 6
71 FN_QPREFIX = "q"
72 MAIN_EXPLANATION = " (0 no, 1 yes to some extent, 2 yes)"
74 q1: Mapped[Optional[int]] = mapped_camcops_column(
75 permitted_value_checker=ZERO_TO_TWO_CHECKER,
76 comment="Q1. Treated with respect/dignity" + MAIN_EXPLANATION,
77 )
78 q2: Mapped[Optional[int]] = mapped_camcops_column(
79 permitted_value_checker=ZERO_TO_TWO_CHECKER,
80 comment="Q2. Felt listened to" + MAIN_EXPLANATION,
81 )
82 q3: Mapped[Optional[int]] = mapped_camcops_column(
83 permitted_value_checker=ZERO_TO_TWO_CHECKER,
84 comment="Q3. Needs were understood" + MAIN_EXPLANATION,
85 )
86 q4: Mapped[Optional[int]] = mapped_camcops_column(
87 permitted_value_checker=ZERO_TO_TWO_CHECKER,
88 comment="Q4. Given info about team" + MAIN_EXPLANATION,
89 )
90 q5: Mapped[Optional[int]] = mapped_camcops_column(
91 permitted_value_checker=ZERO_TO_TWO_CHECKER,
92 comment="Q5. Family considered/included" + MAIN_EXPLANATION,
93 )
94 q6: Mapped[Optional[int]] = mapped_camcops_column(
95 permitted_value_checker=ZERO_TO_TWO_CHECKER,
96 comment="Q6. Views on treatment taken into account" + MAIN_EXPLANATION,
97 )
98 ff_rating: Mapped[Optional[int]] = mapped_camcops_column(
99 permitted_value_checker=ZERO_TO_FIVE_CHECKER,
100 comment="How likely to recommend service to friends and family "
101 "(0 don't know, 1 extremely unlikely, 2 unlikely, "
102 "3 neither likely nor unlikely, 4 likely, 5 extremely likely)",
103 )
104 ff_why: Mapped[Optional[str]] = mapped_column(
105 UnicodeText,
106 comment="Why was friends/family rating given as it was?",
107 )
108 comments: Mapped[Optional[str]] = mapped_column(
109 UnicodeText, comment="General comments"
110 )
112 REQUIRED_FIELDS = ["q1", "q2", "q3", "q4", "q5", "q6", "ff_rating"]
114 @staticmethod
115 def longname(req: "CamcopsRequest") -> str:
116 _ = req.gettext
117 return _(
118 "Assessment Patient Experience Questionnaire for "
119 "CPFT Perinatal Services"
120 )
122 def is_complete(self) -> bool:
123 return self.all_fields_not_none(self.REQUIRED_FIELDS)
125 def get_task_html(self, req: CamcopsRequest) -> str:
126 options_main = {None: "?"} # type: Dict[Optional[int], str]
127 for o in range(0, 2 + 1):
128 options_main[o] = self.wxstring(req, f"main_a{o}")
129 options_ff = {None: "?"} # type: Dict[Optional[int], str]
130 for o in range(0, 5 + 1):
131 options_ff[o] = self.wxstring(req, f"ff_a{o}")
133 qlines = [] # type: List[str]
134 for qnum in range(self.FIRST_MAIN_Q, self.LAST_MAIN_Q + 1):
135 xstring_attr_name = f"q{qnum}"
136 qlines.append(
137 tr_qa(
138 self.wxstring(req, xstring_attr_name),
139 options_main.get(getattr(self, xstring_attr_name)),
140 )
141 )
142 q_a = "".join(qlines)
143 return f"""
144 <div class="{CssClass.SUMMARY}">
145 <table class="{CssClass.SUMMARY}">
146 {self.get_is_complete_tr(req)}
147 </table>
148 </div>
149 <table class="{CssClass.TASKDETAIL}">
150 <tr>
151 <th width="60%">Question</th>
152 <th width="40%">Answer</th>
153 </tr>
154 {q_a}
155 {tr_qa(self.wxstring(req, "q_ff_rating"),
156 options_ff.get(self.ff_rating))}
157 {tr_qa(self.wxstring(req, "q_ff_why"),
158 self.ff_why or "")}
159 {tr_qa(self.wxstring(req, "q_comments"),
160 self.comments or "")}
161 </table>
162 """
164 def get_main_options(self, req: "CamcopsRequest") -> List[str]:
165 options = []
167 for n in range(0, 2 + 1):
168 options.append(self.wxstring(req, f"main_a{n}"))
170 return options
172 def get_ff_options(self, req: "CamcopsRequest") -> List[str]:
173 options = []
175 for n in range(0, 5 + 1):
176 options.append(self.wxstring(req, f"ff_a{n}"))
178 return options
181# =============================================================================
182# Reports
183# =============================================================================
186class APEQCPFTPerinatalReport(
187 DateTimeFilteredReportMixin, Report, PercentageSummaryReportMixin
188):
189 """
190 Provides a summary of each question, x% of people said each response etc.
191 Then a summary of the comments.
192 """
194 COL_Q = 0
195 COL_TOTAL = 1
196 COL_RESPONSE_START = 2
198 COL_FF_WHY = 1
200 def __init__(self, *args: Any, **kwargs: Any) -> None:
201 super().__init__(*args, **kwargs)
202 self.task = APEQCPFTPerinatal() # dummy task, never written to DB
204 @classproperty
205 def task_class(self) -> Type["Task"]:
206 return APEQCPFTPerinatal
208 # noinspection PyMethodParameters
209 @classproperty
210 def report_id(cls) -> str:
211 return "apeq_cpft_perinatal"
213 @classmethod
214 def title(cls, req: "CamcopsRequest") -> str:
215 _ = req.gettext
216 return _("APEQ CPFT Perinatal — Question summaries")
218 # noinspection PyMethodParameters
219 @classproperty
220 def superuser_only(cls) -> bool:
221 return False
223 @classmethod
224 def get_specific_http_query_keys(cls) -> List[str]:
225 return [ViewParam.START_DATETIME, ViewParam.END_DATETIME]
227 def render_html(self, req: "CamcopsRequest") -> Response:
228 cell_format = "{0:.1f}%"
230 return render_to_response(
231 "apeq_cpft_perinatal_report.mako",
232 dict(
233 title=self.title(req),
234 report_id=self.report_id,
235 start_datetime=self.start_datetime,
236 end_datetime=self.end_datetime,
237 main_column_headings=self._get_main_column_headings(req),
238 main_rows=self._get_main_rows(req, cell_format=cell_format),
239 ff_column_headings=self._get_ff_column_headings(req),
240 ff_rows=self._get_ff_rows(req, cell_format=cell_format),
241 ff_why_rows=self._get_ff_why_rows(req),
242 comments=self._get_comments(req),
243 ),
244 request=req,
245 )
247 def get_spreadsheet_pages(
248 self, req: "CamcopsRequest"
249 ) -> List[SpreadsheetPage]:
250 _ = req.gettext
252 main_page = self.get_spreadsheet_page(
253 name=_("Main questions"),
254 column_names=self._get_main_column_headings(req),
255 rows=self._get_main_rows(req),
256 )
257 ff_page = self.get_spreadsheet_page(
258 name=_("Friends and family question"),
259 column_names=self._get_ff_column_headings(req),
260 rows=self._get_ff_rows(req),
261 )
262 ff_why_page = self.get_spreadsheet_page(
263 name=_("Reasons given for the above responses"),
264 column_names=[_("Response"), _("Reason")],
265 rows=self._get_ff_why_rows(req),
266 )
267 comments_page = self.get_spreadsheet_page(
268 name=_("Comments"),
269 column_names=[_("Comment")],
270 rows=self._get_comment_rows(req),
271 )
273 return [main_page, ff_page, ff_why_page, comments_page]
275 def _get_main_column_headings(self, req: "CamcopsRequest") -> List[str]:
276 _ = req.gettext
277 names = [
278 _("Question"),
279 _("Total responses"),
280 ] + self.task.get_main_options(req)
282 return names
284 def _get_main_rows(
285 self, req: "CamcopsRequest", cell_format: str = "{}"
286 ) -> List[List[str]]:
287 """
288 Percentage of people who answered x for each question
289 """
290 column_dict = {}
292 qnums = range(self.task.FIRST_MAIN_Q, self.task.LAST_MAIN_Q + 1)
294 for qnum in qnums:
295 column_name = f"{self.task.FN_QPREFIX}{qnum}"
297 column_dict[column_name] = self.task.wxstring(req, column_name)
299 return self.get_percentage_summaries(
300 req,
301 column_dict=column_dict,
302 num_answers=3,
303 cell_format=cell_format,
304 )
306 def _get_ff_column_headings(self, req: "CamcopsRequest") -> List[str]:
307 _ = req.gettext
308 return [
309 _("Question"),
310 _("Total responses"),
311 ] + self.task.get_ff_options(req)
313 def _get_ff_rows(
314 self, req: "CamcopsRequest", cell_format: str = "{}"
315 ) -> List[List[str]]:
316 """
317 Percentage of people who answered x for the friends/family question
318 """
319 return self.get_percentage_summaries(
320 req,
321 column_dict={
322 "ff_rating": self.task.wxstring(
323 req, f"{self.task.FN_QPREFIX}_ff_rating"
324 )
325 },
326 num_answers=6,
327 cell_format=cell_format,
328 )
330 def _get_ff_why_rows(self, req: "CamcopsRequest") -> List[List[str]]:
331 """
332 Reasons for giving a particular answer to the friends/family question
333 """
335 options = self.task.get_ff_options(req)
337 wheres = [
338 column("ff_rating").isnot(None),
339 column("ff_why").isnot(None),
340 ]
342 self.add_task_report_filters(wheres) # type: ignore[arg-type]
344 # noinspection PyUnresolvedReferences
345 query = (
346 select(column("ff_rating"), column("ff_why")) # type: ignore[var-annotated] # noqa: E501
347 .select_from(self.task.__table__)
348 .where(and_(*wheres))
349 .order_by("ff_why")
350 )
352 rows = []
354 for result in req.dbsession.execute(query).fetchall():
355 rows.append([options[result[0]], result[1]])
357 return rows
359 def _get_comment_rows(self, req: "CamcopsRequest") -> List[Tuple[str]]:
360 """
361 A list of all the additional comments, as rows.
362 """
364 wheres = [column("comments").isnot(None)]
366 self.add_task_report_filters(wheres) # type: ignore[arg-type]
368 # noinspection PyUnresolvedReferences
369 query = (
370 select(column("comments")) # type: ignore[var-annotated]
371 .select_from(self.task.__table__)
372 .where(and_(*wheres))
373 )
375 comment_rows = []
377 for result in req.dbsession.execute(query).fetchall():
378 comment_rows.append(result)
380 return comment_rows
382 def _get_comments(self, req: "CamcopsRequest") -> List[str]:
383 """
384 A list of all the additional comments.
385 """
386 return [x[0] for x in self._get_comment_rows(req)]