Source code for camcops_server.tasks.perinatalpoem

#!/usr/bin/env python

"""
camcops_server/tasks/perinatalpoem.py

===============================================================================

    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

===============================================================================

"""

import re
from typing import Dict, List, Tuple, Type

from cardinal_pythonlib.classes import classproperty
from pyramid.renderers import render_to_response
from pyramid.response import Response
from sqlalchemy.sql.expression import and_, column, select
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Integer, UnicodeText

from camcops_server.cc_modules.cc_constants import CssClass

from camcops_server.cc_modules.cc_html import (
    get_yes_no_none,
    subheading_spanning_two_columns,
    tr_qa,
)
from camcops_server.cc_modules.cc_report import (
    DateTimeFilteredReportMixin,
    PercentageSummaryReportMixin,
    Report,
)
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import (
    CamcopsColumn,
    ZERO_TO_ONE_CHECKER,
    ONE_TO_TWO_CHECKER,
    ONE_TO_FIVE_CHECKER,
    ONE_TO_FOUR_CHECKER,
)
from camcops_server.cc_modules.cc_task import get_from_dict, Task
from camcops_server.cc_modules.cc_text import SS
from camcops_server.cc_modules.cc_spreadsheet import SpreadsheetPage


# =============================================================================
# Perinatal-POEM
# =============================================================================


[docs]class PerinatalPoem(Task): """ Server implementation of the Perinatal-POEM task. """ __tablename__ = "perinatal_poem" shortname = "Perinatal-POEM" provides_trackers = False # Field names FN_QA_RESPONDENT = "qa" FN_QB_SERVICE_TYPE = "qb" FN_Q1A_MH_FIRST_CONTACT = "q1a" FN_Q1B_MH_DISCHARGE = "q1b" FN_Q2A_STAFF_DID_NOT_COMMUNICATE = "q2a" FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT = "q2b" FN_Q2C_HELP_NOT_QUICK_ENOUGH = "q2c" FN_Q2D_STAFF_LISTENED = "q2d" FN_Q2E_STAFF_DID_NOT_INVOLVE_ME = "q2e" FN_Q2F_SERVICE_PROVIDED_INFO = "q2f" FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME = "q2g" FN_Q2H_STAFF_HELPED_ME_UNDERSTAND = "q2h" FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY = "q2i" FN_Q2J_STAFF_HELPED_MY_CONFIDENCE = "q2j" FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY = "q2k" FN_Q2L_I_WOULD_RECOMMEND_SERVICE = "q2l" FN_Q3A_UNIT_CLEAN = "q3a" FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER = "q3b" FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES = "q3c" FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY = "q3d" FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT = "q3e" FN_Q3F_FOOD_NOT_ACCEPTABLE = "q3f" FN_GENERAL_COMMENTS = "general_comments" FN_FUTURE_PARTICIPATION = "future_participation" FN_CONTACT_DETAILS = "contact_details" # Response values VAL_QA_PATIENT = 1 VAL_QA_PARTNER_OTHER = 2 VAL_QB_INPATIENT = 1 # inpatient = MBU = mother and baby unit VAL_QB_COMMUNITY = 2 VAL_Q1_VERY_WELL = 1 VAL_Q1_WELL = 2 VAL_Q1_UNWELL = 3 VAL_Q1_VERY_UNWELL = 4 VAL_Q1_EXTREMELY_UNWELL = 5 _MH_KEY = ( f"({VAL_Q1_VERY_WELL} very well, {VAL_Q1_WELL} well, " f"{VAL_Q1_UNWELL} unwell, {VAL_Q1_VERY_UNWELL} very unwell, " f"{VAL_Q1_EXTREMELY_UNWELL} extremely unwell)" ) VAL_STRONGLY_AGREE = 1 VAL_AGREE = 2 VAL_DISAGREE = 3 VAL_STRONGLY_DISAGREE = 4 _AGREE_KEY = ( f"({VAL_STRONGLY_AGREE} strongly agree, {VAL_AGREE} agree, " f"{VAL_DISAGREE} disagree, {VAL_STRONGLY_DISAGREE} strongly disagree)" ) _INPATIENT_ONLY = "[Inpatient services only]" YES_INT = 1 NO_INT = 0 # ------------------------------------------------------------------------- # Fields # ------------------------------------------------------------------------- qa = CamcopsColumn( FN_QA_RESPONDENT, Integer, permitted_value_checker=ONE_TO_TWO_CHECKER, comment=( f"Question A: Is the respondent the patient ({VAL_QA_PATIENT}) " f"or other ({VAL_QA_PARTNER_OTHER})?" ), ) qb = CamcopsColumn( FN_QB_SERVICE_TYPE, Integer, permitted_value_checker=ONE_TO_TWO_CHECKER, comment=( f"Question B: Was the service type inpatient [mother-and-baby " f"unit, MBU] ({VAL_QB_INPATIENT}) or " f"community ({VAL_QB_COMMUNITY})?" ), ) q1a = CamcopsColumn( FN_Q1A_MH_FIRST_CONTACT, Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment=f"Q1A: mental health at first contact {_MH_KEY}", ) q1b = CamcopsColumn( FN_Q1B_MH_DISCHARGE, Integer, permitted_value_checker=ONE_TO_FIVE_CHECKER, comment=f"Q1B: mental health at discharge {_MH_KEY}", ) q2a = CamcopsColumn( FN_Q2A_STAFF_DID_NOT_COMMUNICATE, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2a: staff didn't communicate with others {_AGREE_KEY}", ) q2b = CamcopsColumn( FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2b: Staff gave right amount of support {_AGREE_KEY}", ) q2c = CamcopsColumn( FN_Q2C_HELP_NOT_QUICK_ENOUGH, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2c: Help not quick enough after referral {_AGREE_KEY}", ) q2d = CamcopsColumn( FN_Q2D_STAFF_LISTENED, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2d: Staff listened/understood {_AGREE_KEY}", ) q2e = CamcopsColumn( FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2e: Staff didn't involve pt enough {_AGREE_KEY}", ) q2f = CamcopsColumn( FN_Q2F_SERVICE_PROVIDED_INFO, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2f: Service provided information {_AGREE_KEY}", ) q2g = CamcopsColumn( FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2g: Staff not very sensitive to pt {_AGREE_KEY}", ) q2h = CamcopsColumn( FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2h: Staff helped understanding of illness {_AGREE_KEY}", ) q2i = CamcopsColumn( FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2i: Staff not very sensitive to baby {_AGREE_KEY}", ) q2j = CamcopsColumn( FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2j: Staff helped confidence re baby {_AGREE_KEY}", ) q2k = CamcopsColumn( FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2k: Service involved others helpfully {_AGREE_KEY}", ) q2l = CamcopsColumn( FN_Q2L_I_WOULD_RECOMMEND_SERVICE, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q2l: Would recommend service {_AGREE_KEY}", ) q3a = CamcopsColumn( FN_Q3A_UNIT_CLEAN, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q3a: MBU clean {_AGREE_KEY} {_INPATIENT_ONLY}", ) q3b = CamcopsColumn( FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q3b: MBU not a good place to recover " f"{_AGREE_KEY} {_INPATIENT_ONLY}", ) q3c = CamcopsColumn( FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q3c: MBU did not provide helpful activities " f"{_AGREE_KEY} {_INPATIENT_ONLY}", ) q3d = CamcopsColumn( FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q3d: MBU a good place for baby to be with pt " f"{_AGREE_KEY} {_INPATIENT_ONLY}", ) q3e = CamcopsColumn( FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q3e: MBU supported contact with family/friends " f"{_AGREE_KEY} {_INPATIENT_ONLY}", ) q3f = CamcopsColumn( FN_Q3F_FOOD_NOT_ACCEPTABLE, Integer, permitted_value_checker=ONE_TO_FOUR_CHECKER, comment=f"Q3f: Food not acceptable {_AGREE_KEY} {_INPATIENT_ONLY}", ) general_comments = Column( FN_GENERAL_COMMENTS, UnicodeText, comment="General comments" ) future_participation = CamcopsColumn( FN_FUTURE_PARTICIPATION, Integer, permitted_value_checker=ZERO_TO_ONE_CHECKER, comment=f"Willing to participate in future studies " f"({YES_INT} yes, {NO_INT} no)", ) contact_details = Column( FN_CONTACT_DETAILS, UnicodeText, comment="Contact details" ) # ------------------------------------------------------------------------- # Fieldname collections # ------------------------------------------------------------------------- REQUIRED_ALWAYS = [ FN_QA_RESPONDENT, FN_QB_SERVICE_TYPE, FN_Q1A_MH_FIRST_CONTACT, FN_Q1B_MH_DISCHARGE, FN_Q2A_STAFF_DID_NOT_COMMUNICATE, FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, FN_Q2C_HELP_NOT_QUICK_ENOUGH, FN_Q2D_STAFF_LISTENED, FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, FN_Q2F_SERVICE_PROVIDED_INFO, FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, FN_Q2L_I_WOULD_RECOMMEND_SERVICE, # not FN_GENERAL_COMMENTS, FN_FUTURE_PARTICIPATION, # not FN_CONTACT_DETAILS, ] REQUIRED_INPATIENT = [ FN_Q3A_UNIT_CLEAN, FN_Q3B_UNIT_NOT_GOOD_PLACE_TO_RECOVER, FN_Q3C_UNIT_DID_NOT_PROVIDE_ACTIVITIES, FN_Q3D_UNIT_GOOD_PLACE_FOR_BABY, FN_Q3E_UNIT_SUPPORTED_FAMILY_FRIENDS_CONTACT, FN_Q3F_FOOD_NOT_ACCEPTABLE, ] Q1_FIELDS = [FN_Q1A_MH_FIRST_CONTACT, FN_Q1B_MH_DISCHARGE] Q2_FIELDS = [ FN_Q2A_STAFF_DID_NOT_COMMUNICATE, FN_Q2B_STAFF_GAVE_RIGHT_SUPPORT, FN_Q2C_HELP_NOT_QUICK_ENOUGH, FN_Q2D_STAFF_LISTENED, FN_Q2E_STAFF_DID_NOT_INVOLVE_ME, FN_Q2F_SERVICE_PROVIDED_INFO, FN_Q2G_STAFF_NOT_SENSITIVE_TO_ME, FN_Q2H_STAFF_HELPED_ME_UNDERSTAND, FN_Q2I_STAFF_NOT_SENSITIVE_TO_BABY, FN_Q2J_STAFF_HELPED_MY_CONFIDENCE, FN_Q2K_SERVICE_INVOLVED_OTHERS_HELPFULLY, FN_Q2L_I_WOULD_RECOMMEND_SERVICE, ] Q3_FIELDS = REQUIRED_INPATIENT
[docs] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Perinatal Patient-rated Outcome and Experience Measure")
def was_inpatient(self) -> bool: return self.qb == self.VAL_QB_INPATIENT def respondent_not_patient(self) -> bool: return self.qa == self.VAL_QA_PARTNER_OTHER def offering_participation(self) -> bool: return self.future_participation == self.YES_INT
[docs] def is_complete(self) -> bool: if self.any_fields_none(self.REQUIRED_ALWAYS): return False if self.was_inpatient() and self.any_fields_none( self.REQUIRED_INPATIENT ): return False if not self.field_contents_valid(): return False return True
def get_qa_options(self, req: CamcopsRequest) -> List[str]: options = [ self.wxstring(req, f"qa_a{o}") for o in range(self.VAL_QA_PATIENT, self.VAL_QA_PARTNER_OTHER + 1) ] return options def get_qb_options(self, req: CamcopsRequest) -> List[str]: options = [ self.wxstring(req, f"qb_a{o}") for o in range(self.VAL_QB_INPATIENT, self.VAL_QB_COMMUNITY + 1) ] return options def get_q1_options(self, req: CamcopsRequest) -> List[str]: options = [ self.wxstring(req, f"q1_a{o}") for o in range( self.VAL_Q1_VERY_WELL, self.VAL_Q1_EXTREMELY_UNWELL + 1 ) ] return options def get_agree_options(self, req: CamcopsRequest) -> List[str]: options = [ self.wxstring(req, f"agreement_a{o}") for o in range( self.VAL_STRONGLY_AGREE, self.VAL_STRONGLY_DISAGREE + 1 ) ] return options @staticmethod def get_yn_options(req: CamcopsRequest) -> List[str]: return [req.sstring(SS.NO), req.sstring(SS.YES)]
[docs] def get_task_html(self, req: CamcopsRequest) -> str: def loadvalues( _dict: Dict[int, str], _first: int, _last: int, _xstringprefix: str ) -> None: for val in range(_first, _last + 1): _dict[ val ] = f"{val}{self.wxstring(req, f'{_xstringprefix}{val}')}" respondent_dict = {} # type: Dict[int, str] loadvalues( respondent_dict, self.VAL_QA_PATIENT, self.VAL_QA_PARTNER_OTHER, "qa_a", ) service_dict = {} # type: Dict[int, str] loadvalues( service_dict, self.VAL_QB_INPATIENT, self.VAL_QB_COMMUNITY, "qb_a" ) mh_dict = {} # type: Dict[int, str] loadvalues( mh_dict, self.VAL_Q1_VERY_WELL, self.VAL_Q1_EXTREMELY_UNWELL, "q1_a", ) agree_dict = {} # type: Dict[int, str] loadvalues( agree_dict, self.VAL_STRONGLY_AGREE, self.VAL_STRONGLY_DISAGREE, "agreement_a", ) q_a_list = [] # type: List[str] def addqa(_fieldname: str, _valuedict: Dict[int, str]) -> None: xstringname = _fieldname + "_q" q_a_list.append( tr_qa( self.xstring(req, xstringname), # not wxstring get_from_dict(_valuedict, getattr(self, _fieldname)), ) ) def subheading(_xstringname: str) -> None: q_a_list.append( subheading_spanning_two_columns( self.wxstring(req, _xstringname) ) ) # Preamble addqa(self.FN_QA_RESPONDENT, respondent_dict) addqa(self.FN_QB_SERVICE_TYPE, service_dict) # The bulk subheading("q1_stem") for fieldname in self.Q1_FIELDS: addqa(fieldname, mh_dict) subheading("q2_stem") for fieldname in self.Q2_FIELDS: addqa(fieldname, agree_dict) if self.was_inpatient(): subheading("q3_stem") for fieldname in self.Q3_FIELDS: addqa(fieldname, agree_dict) # General q_a_list.append( subheading_spanning_two_columns(req.sstring(SS.GENERAL)) ) q_a_list.append( tr_qa( self.wxstring(req, "general_comments_q"), self.general_comments ) ) q_a_list.append( tr_qa( self.wxstring(req, "participation_q"), get_yes_no_none(req, self.future_participation), ) ) if self.offering_participation(): q_a_list.append( tr_qa( self.wxstring(req, "contact_details_q"), self.contact_details, ) ) q_a = "\n".join(q_a_list) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer</th> </tr> {q_a} </table> <div class="{CssClass.FOOTNOTES}"> </div> """
# No SNOMED codes for Perinatal-POEM. # ============================================================================= # Reports # ============================================================================= class PerinatalPoemReportTableConfig(object): def __init__( self, heading: str, column_headings: List[str], fieldnames: List[str], min_answer: int = 0, xstring_format: str = "{}_q", ) -> None: self.heading = heading self.column_headings = column_headings self.fieldnames = fieldnames self.min_answer = min_answer self.xstring_format = xstring_format class PerinatalPoemReportTable(object): def __init__( self, req: "CamcopsRequest", heading: str, column_headings: List[str], rows: List[List[str]], ) -> None: _ = req.gettext self.heading = heading common_headings = [_("Question"), _("Total responses")] self.column_headings = common_headings + column_headings self.rows = rows
[docs]class PerinatalPoemReport( DateTimeFilteredReportMixin, Report, PercentageSummaryReportMixin ): """ Provides a summary of each question, x% of people said each response etc. Then a summary of the comments. """ HTML_TAG_RE = re.compile(r"<[^>]+>")
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.task = PerinatalPoem() # dummy task, never written to DB
[docs] @classproperty def task_class(self) -> Type["Task"]: return PerinatalPoem
# noinspection PyMethodParameters @classproperty def report_id(cls) -> str: return "perinatal_poem" @classmethod def title(cls, req: "CamcopsRequest") -> str: _ = req.gettext return _("Perinatal-POEM — Question summaries") # noinspection PyMethodParameters @classproperty def superuser_only(cls) -> bool: return False def render_html(self, req: "CamcopsRequest") -> Response: return render_to_response( "perinatal_poem_report.mako", dict( title=self.title(req), report_id=self.report_id, start_datetime=self.start_datetime, end_datetime=self.end_datetime, tables=self._get_html_tables(req), comments=self._get_comments(req), ), request=req, ) def get_spreadsheet_pages( self, req: "CamcopsRequest" ) -> List[SpreadsheetPage]: _ = req.gettext pages = [] for table in self._get_spreadsheet_tables(req): pages.append( self.get_spreadsheet_page( name=table.heading, column_names=table.column_headings, rows=table.rows, ) ) pages.append( self.get_spreadsheet_page( name=_("Comments"), column_names=[_("Comment")], rows=self._get_comment_rows(req), ) ) return pages def _get_html_tables( self, req: "CamcopsRequest" ) -> List["PerinatalPoemReportTable"]: return [ self._get_html_table(req, config) for config in self._get_table_configs(req) ] def _get_spreadsheet_tables( self, req: "CamcopsRequest" ) -> List["PerinatalPoemReportTable"]: return [ self._get_spreadsheet_table(req, config) for config in self._get_table_configs(req) ] def _get_table_configs( self, req: "CamcopsRequest" ) -> List["PerinatalPoemReportTableConfig"]: return [ PerinatalPoemReportTableConfig( heading=self.task.xstring(req, "qa_q"), column_headings=self.task.get_qa_options(req), fieldnames=["qa"], min_answer=1, ), PerinatalPoemReportTableConfig( heading=self.task.xstring(req, "qb_q"), column_headings=self.task.get_qb_options(req), fieldnames=["qb"], min_answer=1, ), PerinatalPoemReportTableConfig( heading=self.task.xstring(req, "q1_stem"), column_headings=self.task.get_q1_options(req), fieldnames=PerinatalPoem.Q1_FIELDS, min_answer=1, ), PerinatalPoemReportTableConfig( heading=self.task.xstring(req, "q2_stem"), column_headings=self.task.get_agree_options(req), fieldnames=PerinatalPoem.Q2_FIELDS, min_answer=1, ), PerinatalPoemReportTableConfig( heading=self.task.xstring(req, "q3_stem"), column_headings=self.task.get_agree_options(req), fieldnames=PerinatalPoem.Q3_FIELDS, min_answer=1, ), PerinatalPoemReportTableConfig( heading=self.task.xstring(req, "participation_q"), column_headings=self.task.get_yn_options(req), fieldnames=["future_participation"], xstring_format="participation_q", ), ] def _get_html_table( self, req: "CamcopsRequest", config: PerinatalPoemReportTableConfig ) -> PerinatalPoemReportTable: column_dict = {} for fieldname in config.fieldnames: column_dict[fieldname] = self.task.xstring( req, config.xstring_format.format(fieldname) ) rows = self.get_percentage_summaries( req, column_dict=column_dict, num_answers=len(config.column_headings), cell_format="{0:.1f}%", min_answer=config.min_answer, ) return PerinatalPoemReportTable( req, heading=config.heading, column_headings=config.column_headings, rows=rows, ) def _get_spreadsheet_table( self, req: "CamcopsRequest", config: PerinatalPoemReportTableConfig ) -> PerinatalPoemReportTable: column_dict = {} for fieldname in config.fieldnames: column_dict[fieldname] = self._strip_tags( self.task.xstring(req, config.xstring_format.format(fieldname)) ) rows = self.get_percentage_summaries( req, column_dict=column_dict, num_answers=len(config.column_headings), min_answer=config.min_answer, ) return PerinatalPoemReportTable( req, heading=config.heading, column_headings=config.column_headings, rows=rows, ) def _strip_tags(self, text: str) -> str: return self.HTML_TAG_RE.sub("", text) def _get_comment_rows(self, req: "CamcopsRequest") -> List[Tuple[str]]: """ A list of all the additional comments """ wheres = [column("general_comments").isnot(None)] self.add_task_report_filters(wheres) # noinspection PyUnresolvedReferences query = ( select([column("general_comments")]) .select_from(self.task.__table__) .where(and_(*wheres)) ) comment_rows = [] for result in req.dbsession.execute(query).fetchall(): comment_rows.append(result) return comment_rows def _get_comments(self, req: "CamcopsRequest") -> List[str]: """ A list of all the additional comments. """ return [x[0] for x in self._get_comment_rows(req)]