def export_task(req: "CamcopsRequest", recipient: ExportRecipient, task: Task) -> None: """ Exports a single task, checking that it remains valid to do so. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` recipient: an :class:`camcops_server.cc_modules.cc_exportmodels.ExportRecipient` task: a :class:`camcops_server.cc_modules.cc_task.Task` """ # noqa # Double-check it's OK! Just in case, for example, an old backend task has # persisted, or someone's managed to get an iffy back-end request in some # other way. if not recipient.is_task_suitable(task): # Warning will already have been emitted. return cfg = req.config lockfilename = cfg.get_export_lockfilename_task( recipient_name=recipient.recipient_name, basetable=task.tablename, pk=task.get_pk(), ) dbsession = req.dbsession try: with lockfile.FileLock(lockfilename, timeout=0): # doesn't wait # We recheck the export status once we hold the lock, in case # multiple jobs are competing to export it. if ExportedTask.task_already_exported( dbsession=dbsession, recipient_name=recipient.recipient_name, basetable=task.tablename, task_pk=task.get_pk()): log.info( "Task {!r} already exported to recipient {!r}; " "ignoring", task, recipient) # Not a warning; it's normal to see these because it allows the # client API to skip some checks for speed. return # OK; safe to export now. et = ExportedTask(recipient, task) dbsession.add(et) et.export(req) dbsession.commit() # so the ExportedTask is visible to others ASAP except lockfile.AlreadyLocked: log.warning( "Export logfile {!r} already locked by another process; " "aborting", lockfilename)
def task_classes(self) -> List[Type[Task]]: """ Return a list of task classes permitted by the filter. Uses caching, since the filter will be called repeatedly. """ if self._task_classes is None: self._task_classes = [] # type: List[Type[Task]] if self.task_types: starting_classes = task_classes_from_table_names( self.task_types) else: starting_classes = Task.all_subclasses_by_shortname() skip_anonymous_tasks = self.skip_anonymous_tasks() for cls in starting_classes: if (self.tasks_offering_trackers_only and not cls.provides_trackers): # Class doesn't provide trackers; skip continue if skip_anonymous_tasks and not cls.has_patient: # Anonymous task; skip continue if self.text_contents and not cls.get_text_filter_columns(): # Text filter and task has no text columns; skip continue self._task_classes.append(cls) sort_task_classes_in_place(self._task_classes, self.sort_method) return self._task_classes
def rebuild_entire_task_index( cls, session: SqlASession, indexed_at_utc: Pendulum, skip_tasks_with_missing_tables: bool = False, ) -> None: """ Rebuilds the entire index. Args: session: an SQLAlchemy Session indexed_at_utc: current time in UTC skip_tasks_with_missing_tables: should we skip over tasks if their tables are not in the database? (This is so we can rebuild an index from a database upgrade, but not crash because newer tasks haven't had their tables created yet.) """ log.info("Rebuilding entire task index") # noinspection PyUnresolvedReferences idxtable = cls.__table__ # type: Table # Delete all entries with if_sqlserver_disable_constraints_triggers(session, idxtable.name): session.execute(idxtable.delete()) # Now rebuild: for taskclass in Task.all_subclasses_by_tablename(): if skip_tasks_with_missing_tables: basetable = taskclass.tablename engine = get_engine_from_session(session) if not table_exists(engine, basetable): continue cls.rebuild_index_for_task_type( session, taskclass, indexed_at_utc, delete_first=False )
def apply_standard_task_fields(self, task: Task) -> None: """ Writes some default values to an SQLAlchemy ORM object representing a task. """ self.apply_standard_db_fields(task) task.when_created = self.era_time
def all_tracker_task_classes() -> List[Type[Task]]: """ Returns a list of all task classes that provide tracker information. """ return [ cls for cls in Task.all_subclasses_by_shortname() if cls.provides_trackers ]
def add_tasks(self, patient_id: int): for cls in Task.all_subclasses_by_tablename(): task = cls() task.id = self.next_id(cls.id) self.apply_standard_task_fields(task) if task.has_patient: task.patient_id = patient_id self.fill_in_task_fields(task) self.dbsession.add(task) self.dbsession.commit()
def offers_all_non_anonymous_task_types(self) -> bool: """ Does this filter offer every single non-anonymous task class? Used for efficiency when using indexes. """ offered_task_classes = self.task_classes for taskclass in Task.all_subclasses_by_shortname(): if taskclass.is_anonymous: continue if taskclass not in offered_task_classes: return False return True
def _task_matches_python_parts_of_filter(self, task: Task) -> bool: """ Does the task pass the Python parts of the filter? Only applicable to the direct (not "via index") route. """ assert not self._via_index # "Is task complete" filter if self._filter.complete_only: if not task.is_complete(): return False return True
def make_from_task( cls, task: Task, indexed_at_utc: Pendulum ) -> "TaskIndexEntry": """ Returns a task index entry for the specified :class:`camcops_server.cc_modules.cc_task.Task`. The returned index requires inserting into a database session. Args: task: a :class:`camcops_server.cc_modules.cc_task.Task` indexed_at_utc: current time in UTC """ assert indexed_at_utc is not None, "Missing indexed_at_utc" index = cls() index.indexed_at_utc = indexed_at_utc index.task_table_name = task.tablename index.task_pk = task.pk patient = task.patient index.patient_pk = patient.pk if patient else None index.device_id = task.device_id index.era = task.era index.when_created_utc = task.get_creation_datetime_utc() index.when_created_iso = task.when_created # noinspection PyProtectedMember index.when_added_batch_utc = task._when_added_batch_utc index.adding_user_id = task.get_adding_user_id() index.group_id = task.group_id index.task_is_complete = task.is_complete() return index
def rebuild_entire_task_index(cls, session: SqlASession, indexed_at_utc: Pendulum) -> None: """ Rebuilds the entire index. Args: session: an SQLAlchemy Session indexed_at_utc: current time in UTC """ log.info("Rebuilding entire task index") # noinspection PyUnresolvedReferences idxtable = cls.__table__ # type: Table # Delete all entries session.execute( idxtable.delete() ) # Now rebuild: for taskclass in Task.all_subclasses_by_tablename(): cls.rebuild_index_for_task_type(session, taskclass, indexed_at_utc, delete_first=False)
def create_tasks(self) -> None: from camcops_server.cc_modules.cc_blob import Blob from camcops_server.tasks.photo import Photo from camcops_server.cc_modules.cc_task import Task patient_with_two_idnums = self.create_patient_with_two_idnums() patient_with_one_idnum = self.create_patient_with_one_idnum() for cls in Task.all_subclasses_by_tablename(): t1 = cls() t1.id = 1 self.apply_standard_task_fields(t1) if t1.has_patient: t1.patient_id = patient_with_two_idnums.id if isinstance(t1, Photo): b = Blob() b.id = 1 self.apply_standard_db_fields(b) b.tablename = t1.tablename b.tablepk = t1.id b.fieldname = "photo_blobid" b.filename = "some_picture.png" b.mimetype = MimeType.PNG b.image_rotation_deg_cw = 0 b.theblob = DEMO_PNG_BYTES self.dbsession.add(b) t1.photo_blobid = b.id self.dbsession.add(t1) t2 = cls() t2.id = 2 self.apply_standard_task_fields(t2) if t2.has_patient: t2.patient_id = patient_with_one_idnum.id self.dbsession.add(t2) self.dbsession.commit()
DemoDatabaseTestCase, DemoRequestTestCase, ExtendedTestCase, ) # nopep8 from camcops_server.cc_modules.cc_user import ( SecurityLoginFailure, set_password_directly, User, ) # nopep8 from camcops_server.cc_modules.celery import ( CELERY_APP_NAME, CELERY_SOFT_TIME_LIMIT_SEC, ) # nopep8 log.info("Imports complete") log.info("Using {} tasks", len(Task.all_subclasses_by_tablename())) if TYPE_CHECKING: from pyramid.router import Router # nopep8 # ============================================================================= # Other constants # ============================================================================= WINDOWS = platform.system() == "Windows" # ============================================================================= # Helper functions for web server launcher # =============================================================================
def setUp(self) -> None: super().setUp() from cardinal_pythonlib.datetimefunc import ( convert_datetime_to_utc, format_datetime, ) from camcops_server.cc_modules.cc_blob import Blob from camcops_server.cc_modules.cc_constants import DateFormat from camcops_server.cc_modules.cc_device import Device from camcops_server.cc_modules.cc_group import Group from camcops_server.cc_modules.cc_patient import Patient from camcops_server.cc_modules.cc_patientidnum import PatientIdNum from camcops_server.cc_modules.cc_task import Task from camcops_server.cc_modules.cc_user import User from camcops_server.tasks.photo import Photo Base.metadata.create_all(self.engine) self.era_time = pendulum.parse("2010-07-07T13:40+0100") self.era_time_utc = convert_datetime_to_utc(self.era_time) self.era = format_datetime(self.era_time, DateFormat.ISO8601) # Set up groups, users, etc. # ... ID number definitions iddef1 = IdNumDefinition(which_idnum=1, description="NHS number", short_description="NHS#", hl7_assigning_authority="NHS", hl7_id_type="NHSN") self.dbsession.add(iddef1) iddef2 = IdNumDefinition(which_idnum=2, description="RiO number", short_description="RiO", hl7_assigning_authority="CPFT", hl7_id_type="CPFT_RiO") self.dbsession.add(iddef2) # ... group self.group = Group() self.group.name = "testgroup" self.group.description = "Test group" self.group.upload_policy = "sex AND anyidnum" self.group.finalize_policy = "sex AND idnum1" self.dbsession.add(self.group) self.dbsession.flush() # sets PK fields # ... users self.user = User.get_system_user(self.dbsession) self.user.upload_group_id = self.group.id self.req._debugging_user = self.user # improve our debugging user # ... devices self.server_device = Device.get_server_device(self.dbsession) self.other_device = Device() self.other_device.name = "other_device" self.other_device.friendly_name = "Test device that may upload" self.other_device.registered_by_user = self.user self.other_device.when_registered_utc = self.era_time_utc self.other_device.camcops_version = CAMCOPS_SERVER_VERSION self.dbsession.add(self.other_device) self.dbsession.flush() # sets PK fields # Populate database with two of everything p1 = Patient() p1.id = 1 self._apply_standard_db_fields(p1) p1.forename = "Forename1" p1.surname = "Surname1" p1.dob = pendulum.parse("1950-01-01") self.dbsession.add(p1) p1_idnum1 = PatientIdNum() p1_idnum1.id = 1 self._apply_standard_db_fields(p1_idnum1) p1_idnum1.patient_id = p1.id p1_idnum1.which_idnum = iddef1.which_idnum p1_idnum1.idnum_value = 333 self.dbsession.add(p1_idnum1) p1_idnum2 = PatientIdNum() p1_idnum2.id = 2 self._apply_standard_db_fields(p1_idnum2) p1_idnum2.patient_id = p1.id p1_idnum2.which_idnum = iddef2.which_idnum p1_idnum2.idnum_value = 444 self.dbsession.add(p1_idnum2) p2 = Patient() p2.id = 2 self._apply_standard_db_fields(p2) p2.forename = "Forename2" p2.surname = "Surname2" p2.dob = pendulum.parse("1975-12-12") self.dbsession.add(p2) p2_idnum1 = PatientIdNum() p2_idnum1.id = 3 self._apply_standard_db_fields(p2_idnum1) p2_idnum1.patient_id = p2.id p2_idnum1.which_idnum = iddef1.which_idnum p2_idnum1.idnum_value = 555 self.dbsession.add(p2_idnum1) self.dbsession.flush() for cls in Task.all_subclasses_by_tablename(): t1 = cls() t1.id = 1 self._apply_standard_task_fields(t1) if t1.has_patient: t1.patient_id = p1.id if isinstance(t1, Photo): b = Blob() b.id = 1 self._apply_standard_db_fields(b) b.tablename = t1.tablename b.tablepk = t1.id b.fieldname = 'photo_blobid' b.filename = "some_picture.png" b.mimetype = MimeType.PNG b.image_rotation_deg_cw = 0 b.theblob = DEMO_PNG_BYTES self.dbsession.add(b) t1.photo_blobid = b.id self.dbsession.add(t1) t2 = cls() t2.id = 2 self._apply_standard_task_fields(t2) if t2.has_patient: t2.patient_id = p2.id self.dbsession.add(t2) self.dbsession.commit()
class Mfi20(TaskHasPatientMixin, Task, metaclass=Mfi20Metaclass): __tablename__ = "mfi20" shortname = "MFI-20" prohibits_clinical = True prohibits_commercial = True N_QUESTIONS = 20 MIN_SCORE_PER_Q = 1 MAX_SCORE_PER_Q = 5 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS N_Q_PER_SUBSCALE = 4 # always MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) REVERSE_QUESTIONS = Task.fieldnames_from_list( "q", {2, 5, 9, 10, 13, 14, 16, 17, 18, 19} ) GENERAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {1, 5, 12, 16}) PHYSICAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {2, 8, 14, 20}) REDUCED_ACTIVITY_QUESTIONS = Task.fieldnames_from_list("q", {3, 6, 10, 17}) REDUCED_MOTIVATION_QUESTIONS = Task.fieldnames_from_list( "q", {4, 9, 15, 18} ) MENTAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {7, 11, 13, 19}) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Multidimensional Fatigue Inventory") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score [{self.MIN_SCORE}–{self.MAX_SCORE}]", ), SummaryElement( name="general_fatigue", coltype=Integer(), value=self.general_fatigue_score(), comment=f"General fatigue {subscale_range}", ), SummaryElement( name="physical_fatigue", coltype=Integer(), value=self.physical_fatigue_score(), comment=f"Physical fatigue {subscale_range}", ), SummaryElement( name="reduced_activity", coltype=Integer(), value=self.reduced_activity_score(), comment=f"Reduced activity {subscale_range}", ), SummaryElement( name="reduced_motivation", coltype=Integer(), value=self.reduced_motivation_score(), comment=f"Reduced motivation {subscale_range}", ), SummaryElement( name="mental_fatigue", coltype=Integer(), value=self.mental_fatigue_score(), comment=f"Mental fatigue {subscale_range}", ), ] def is_complete(self) -> bool: if self.any_fields_none(self.ALL_QUESTIONS): return False if not self.field_contents_valid(): return False return True def score_fields(self, fields: List[str]) -> int: total = 0 for f in fields: value = getattr(self, f) if value is not None: if f in self.REVERSE_QUESTIONS: value = self.MAX_SCORE_PER_Q + 1 - value total += value if value is not None else 0 return total def total_score(self) -> int: return self.score_fields(self.ALL_QUESTIONS) def general_fatigue_score(self) -> int: return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS) def physical_fatigue_score(self) -> int: return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS) def reduced_activity_score(self) -> int: return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS) def reduced_motivation_score(self) -> int: return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS) def mental_fatigue_score(self) -> int: return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS) def get_task_html(self, req: CamcopsRequest) -> str: fullscale_range = f"[{self.MIN_SCORE}–{self.MAX_SCORE}]" subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" rows = "" for q_num in range(1, self.N_QUESTIONS + 1): q_field = "q" + str(q_num) question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) score = getattr(self, q_field) rows += tr_qa(question_cell, score) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {general_fatigue_score} {physical_fatigue_score} {reduced_activity_score} {reduced_motivation_score} {mental_fatigue_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer <sup>[8]</sup></th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Questions 2, 5, 9, 10, 13, 14, 16, 17, 18, 19 reverse-scored when summing. [2] Sum for questions 1–20. [3] General fatigue: Sum for questions 1, 5, 12, 16. [4] Physical fatigue: Sum for questions 2, 8, 14, 20. [5] Reduced activity: Sum for questions 3, 6, 10, 17. [6] Reduced motivation: Sum for questions 4, 9, 15, 18. [7] Mental fatigue: Sum for questions 7, 11, 13, 19. [8] All questions are rated from “1 – yes, that is true” to “5 – no, that is not true”. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE) + " <sup>[1][2]</sup>", f"{answer(self.total_score())} {fullscale_range}", ), general_fatigue_score=tr( self.wxstring(req, "general_fatigue") + " <sup>[1][3]</sup>", f"{answer(self.general_fatigue_score())} {subscale_range}", ), physical_fatigue_score=tr( self.wxstring(req, "physical_fatigue") + " <sup>[1][4]</sup>", f"{answer(self.physical_fatigue_score())} {subscale_range}", ), reduced_activity_score=tr( self.wxstring(req, "reduced_activity") + " <sup>[1][5]</sup>", f"{answer(self.reduced_activity_score())} {subscale_range}", ), reduced_motivation_score=tr( self.wxstring(req, "reduced_motivation") + " <sup>[1][6]</sup>", f"{answer(self.reduced_motivation_score())} {subscale_range}", ), mental_fatigue_score=tr( self.wxstring(req, "mental_fatigue") + " <sup>[1][7]</sup>", f"{answer(self.mental_fatigue_score())} {subscale_range}", ), rows=rows, ) return html
SCALE_BDI_I = "BDI-I" # must match client SCALE_BDI_IA = "BDI-IA" # must match client SCALE_BDI_II = "BDI-II" # must match client TOPICS_BY_SCALE = { SCALE_BDI_I: BDI_I_QUESTION_TOPICS, SCALE_BDI_IA: BDI_IA_QUESTION_TOPICS, SCALE_BDI_II: BDI_II_QUESTION_TOPICS, } NQUESTIONS = 21 TASK_SCORED_FIELDS = strseq("q", 1, NQUESTIONS) MAX_SCORE = NQUESTIONS * 3 SUICIDALITY_QNUM = 9 # Q9 in all versions of the BDI (I, IA, II) SUICIDALITY_FN = "q9" # fieldname CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS = [4, 15, 16, 18, 19, 20, 21] CUSTOM_SOMATIC_KHANDAKER_BDI_II_FIELDS = Task.fieldnames_from_list( "q", CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS) # ============================================================================= # BDI (crippled) # ============================================================================= class BdiMetaclass(DeclarativeMeta): # noinspection PyInitNewSignature def __init__( cls: Type["Bdi"], name: str, bases: Tuple[Type, ...], classdict: Dict[str, Any], ) -> None: add_multiple_columns(
def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType: final_rows = [] # type: List[Sequence[Sequence[Any]]] colnames = [] # type: List[str] dbsession = req.dbsession group_ids = req.user.ids_of_groups_user_may_report_on superuser = req.user.superuser via_index = req.get_bool_param(ViewParam.VIA_INDEX, True) if via_index: # noinspection PyUnresolvedReferences query = ( select([ TaskIndexEntry.task_table_name.label("task"), extract_year( TaskIndexEntry.when_created_utc).label("year"), # noqa extract_month(TaskIndexEntry.when_created_utc).label( "month"), # noqa func.count().label("num_tasks_added"), ]).select_from(TaskIndexEntry.__table__).group_by( "task", "year", "month").order_by("task", desc("year"), desc("month"))) if not superuser: # Restrict to accessible groups # noinspection PyProtectedMember query = query.where(TaskIndexEntry.group_id.in_(group_ids)) rows, colnames = get_rows_fieldnames_from_query(dbsession, query) final_rows.extend(rows) else: classes = Task.all_subclasses_by_tablename() for cls in classes: # noinspection PyProtectedMember select_fields = [ literal(cls.__tablename__).label("task"), # func.year() is specific to some DBs, e.g. MySQL # so is func.extract(); # http://modern-sql.com/feature/extract extract_year(isotzdatetime_to_utcdatetime(cls.when_created) ).label("year"), extract_month( isotzdatetime_to_utcdatetime( cls.when_created)).label("month"), func.count().label("num_tasks_added"), ] # noinspection PyUnresolvedReferences select_from = cls.__table__ # noinspection PyProtectedMember wheres = [cls._current == True] # nopep8 if not superuser: # Restrict to accessible groups # noinspection PyProtectedMember wheres.append(cls._group_id.in_(group_ids)) group_by = ["year", "month"] order_by = [desc("year"), desc("month")] # ... http://docs.sqlalchemy.org/en/latest/core/tutorial.html#ordering-or-grouping-by-a-label # noqa query = select(select_fields) \ .select_from(select_from) \ .where(and_(*wheres)) \ .group_by(*group_by) \ .order_by(*order_by) rows, colnames = get_rows_fieldnames_from_query( dbsession, query) final_rows.extend(rows) return PlainReportType(rows=final_rows, column_names=colnames)
class Sfmpq2(TaskHasPatientMixin, Task, metaclass=Sfmpq2Metaclass): __tablename__ = "sfmpq2" shortname = "SF-MPQ2" N_QUESTIONS = 22 MAX_SCORE_PER_Q = 10 ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) CONTINUOUS_PAIN_QUESTIONS = Task.fieldnames_from_list( "q", {1, 5, 6, 8, 9, 10}) INTERMITTENT_PAIN_QUESTIONS = Task.fieldnames_from_list( "q", {2, 3, 4, 11, 16, 18}) NEUROPATHIC_PAIN_QUESTIONS = Task.fieldnames_from_list( "q", {7, 17, 19, 20, 21, 22}) AFFECTIVE_PAIN_QUESTIONS = Task.fieldnames_from_list("q", {12, 13, 14, 15}) @staticmethod def longname(req: CamcopsRequest) -> str: _ = req.gettext return _("Short-Form McGill Pain Questionnaire 2") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_pain", coltype=Float(), value=self.total_pain(), comment=f"Total pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="continuous_pain", coltype=Float(), value=self.continuous_pain(), comment=f"Continuous pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="intermittent_pain", coltype=Float(), value=self.intermittent_pain(), comment=f"Intermittent pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="neuropathic_pain", coltype=Float(), value=self.neuropathic_pain(), comment=f"Neuropathic pain (/{self.MAX_SCORE_PER_Q})", ), SummaryElement( name="affective_pain", coltype=Float(), value=self.affective_pain(), comment=f"Affective pain (/{self.MAX_SCORE_PER_Q})", ), ] def is_complete(self) -> bool: if self.any_fields_none(self.ALL_QUESTIONS): return False if not self.field_contents_valid(): return False return True def total_pain(self) -> float: return self.mean_fields(self.ALL_QUESTIONS) def continuous_pain(self) -> float: return self.mean_fields(self.CONTINUOUS_PAIN_QUESTIONS) def intermittent_pain(self) -> float: return self.mean_fields(self.INTERMITTENT_PAIN_QUESTIONS) def neuropathic_pain(self) -> float: return self.mean_fields(self.NEUROPATHIC_PAIN_QUESTIONS) def affective_pain(self) -> float: return self.mean_fields(self.AFFECTIVE_PAIN_QUESTIONS) def format_average(self, value) -> str: return "{} / {}".format( answer(ws.number_to_dp(value, 3, default="?")), self.MAX_SCORE_PER_Q, ) def get_task_html(self, req: CamcopsRequest) -> str: rows = "" for q_num in range(1, self.N_QUESTIONS + 1): q_field = "q" + str(q_num) question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) score = getattr(self, q_field) rows += tr_qa(question_cell, score) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_pain} {continuous_pain} {intermittent_pain} {neuropathic_pain} {affective_pain} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Answer <sup>[6]</sup></th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Average of items 1–22. [2] Average of items 1, 5, 6, 8, 9, 10. [3] Average of items 2, 3, 4, 11, 16, 18. [4] Average of items 7, 17, 19, 20, 21, 22. [5] Average of items 12, 13, 14, 15. [6] All items are rated from “0 – none” to “10 – worst possible”. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_pain=tr( self.wxstring(req, "total_pain") + " <sup>[1]</sup>", self.format_average(self.total_pain()), ), continuous_pain=tr( self.wxstring(req, "continuous_pain") + " <sup>[2]</sup>", self.format_average(self.continuous_pain()), ), intermittent_pain=tr( self.wxstring(req, "intermittent_pain") + " <sup>[3]</sup>", self.format_average(self.intermittent_pain()), ), neuropathic_pain=tr( self.wxstring(req, "neuropathic_pain") + " <sup>[4]</sup>", self.format_average(self.neuropathic_pain()), ), affective_pain=tr( self.wxstring(req, "affective_pain") + " <sup>[5]</sup>", self.format_average(self.affective_pain()), ), rows=rows, ) return html
def check_index( cls, session: SqlASession, show_all_bad: bool = False ) -> bool: """ Checks the index. Args: session: an SQLAlchemy Session show_all_bad: show all bad entries? (If false, return upon the first) Returns: bool: is the index OK? """ ok = True log.info("Checking all task indexes represent valid entries") for taskclass in Task.all_subclasses_by_tablename(): tasktablename = taskclass.tablename log.debug("Checking {}", tasktablename) # noinspection PyUnresolvedReferences,PyProtectedMember q_idx_without_original = session.query(TaskIndexEntry).filter( TaskIndexEntry.task_table_name == tasktablename, ~exists() .select_from(taskclass.__table__) .where( and_( TaskIndexEntry.task_pk == taskclass._pk, taskclass._current == True, # noqa: E712 ) ), ) # No check for a valid patient at this time. for index in q_idx_without_original: log.error("Task index without matching original: {!r}", index) ok = False if not show_all_bad: return ok log.info("Checking all tasks have an index") for taskclass in Task.all_subclasses_by_tablename(): tasktablename = taskclass.tablename log.debug("Checking {}", tasktablename) # noinspection PyUnresolvedReferences,PyProtectedMember q_original_with_idx = session.query(taskclass).filter( taskclass._current == True, # noqa: E712 ~exists() .select_from(TaskIndexEntry.__table__) .where( and_( TaskIndexEntry.task_pk == taskclass._pk, TaskIndexEntry.task_table_name == tasktablename, ) ), ) for orig in q_original_with_idx: log.error("Task without index entry: {!r}", orig) ok = False if not show_all_bad: return ok return ok
def test_all_tasks(self) -> None: self.announce("test_all_tasks") from datetime import date import hl7 from sqlalchemy.sql.schema import Column from camcops_server.cc_modules.cc_ctvinfo import CtvInfo # noqa: F811 from camcops_server.cc_modules.cc_patient import Patient # noqa: F811 from camcops_server.cc_modules.cc_simpleobjects import IdNumReference from camcops_server.cc_modules.cc_snomed import ( # noqa: F811 SnomedExpression, ) from camcops_server.cc_modules.cc_string import APPSTRING_TASKNAME from camcops_server.cc_modules.cc_summaryelement import SummaryElement from camcops_server.cc_modules.cc_trackerhelpers import ( # noqa: F811 TrackerInfo, ) from camcops_server.cc_modules.cc_spreadsheet import ( # noqa: F811 SpreadsheetPage, ) from camcops_server.cc_modules.cc_xml import XmlElement subclasses = Task.all_subclasses_by_tablename() tables = [cls.tablename for cls in subclasses] log.info("Actual task table names: {!r} (n={})", tables, len(tables)) req = self.req recipdef = self.recipdef dummy_data_factory = DummyDataInserter() for cls in subclasses: log.info("Testing {}", cls) assert cls.extrastring_taskname != APPSTRING_TASKNAME q = self.dbsession.query(cls) t = q.first() # type: Task self.assertIsNotNone(t, "Missing task!") # Name validity validate_task_tablename(t.tablename) # Core functions self.assertIsInstance(t.is_complete(), bool) self.assertIsInstance(t.get_task_html(req), str) for trackerinfo in t.get_trackers(req): self.assertIsInstance(trackerinfo, TrackerInfo) ctvlist = t.get_clinical_text(req) if ctvlist is not None: for ctvinfo in ctvlist: self.assertIsInstance(ctvinfo, CtvInfo) for est in t.get_all_summary_tables(req): self.assertIsInstance(est.get_spreadsheet_page(), SpreadsheetPage) self.assertIsInstance(est.get_xml_element(), XmlElement) self.assertIsInstance(t.has_patient, bool) self.assertIsInstance(t.is_anonymous, bool) self.assertIsInstance(t.has_clinician, bool) self.assertIsInstance(t.has_respondent, bool) self.assertIsInstance(t.tablename, str) for fn in t.get_fieldnames(): self.assertIsInstance(fn, str) self.assertIsInstance(t.field_contents_valid(), bool) for msg in t.field_contents_invalid_because(): self.assertIsInstance(msg, str) for fn in t.get_blob_fields(): self.assertIsInstance(fn, str) self.assertIsInstance(t.pk, int) # all our examples do have PKs # noqa self.assertIsInstance(t.is_preserved(), bool) self.assertIsInstance(t.was_forcibly_preserved(), bool) self.assertIsInstanceOrNone(t.get_creation_datetime(), Pendulum) self.assertIsInstanceOrNone(t.get_creation_datetime_utc(), Pendulum) self.assertIsInstanceOrNone( t.get_seconds_from_creation_to_first_finish(), float) self.assertIsInstance(t.get_adding_user_id(), int) self.assertIsInstance(t.get_adding_user_username(), str) self.assertIsInstance(t.get_removing_user_username(), str) self.assertIsInstance(t.get_preserving_user_username(), str) self.assertIsInstance(t.get_manually_erasing_user_username(), str) # Summaries for se in t.standard_task_summary_fields(): self.assertIsInstance(se, SummaryElement) # SNOMED-CT if req.snomed_supported: for snomed_code in t.get_snomed_codes(req): self.assertIsInstance(snomed_code, SnomedExpression) # Clinician self.assertIsInstance(t.get_clinician_name(), str) # Respondent self.assertIsInstance(t.is_respondent_complete(), bool) # Patient self.assertIsInstanceOrNone(t.patient, Patient) self.assertIsInstance(t.is_female(), bool) self.assertIsInstance(t.is_male(), bool) self.assertIsInstanceOrNone(t.get_patient_server_pk(), int) self.assertIsInstance(t.get_patient_forename(), str) self.assertIsInstance(t.get_patient_surname(), str) dob = t.get_patient_dob() assert (dob is None or isinstance(dob, date) or isinstance(dob, Date)) self.assertIsInstanceOrNone(t.get_patient_dob_first11chars(), str) self.assertIsInstance(t.get_patient_sex(), str) self.assertIsInstance(t.get_patient_address(), str) for idnum in t.get_patient_idnum_objects(): self.assertIsInstance(idnum.get_idnum_reference(), IdNumReference) self.assertIsInstance(idnum.is_superficially_valid(), bool) self.assertIsInstance(idnum.description(req), str) self.assertIsInstance(idnum.short_description(req), str) self.assertIsInstance(idnum.get_filename_component(req), str) # HL7 v2 pidseg = t.get_patient_hl7_pid_segment(req, recipdef) assert isinstance(pidseg, str) or isinstance(pidseg, hl7.Segment) for dataseg in t.get_hl7_data_segments(req, recipdef): self.assertIsInstance(dataseg, hl7.Segment) for dataseg in t.get_hl7_extra_data_segments(recipdef): self.assertIsInstance(dataseg, hl7.Segment) # FHIR self.assertIsInstance( t.get_fhir_bundle(req, recipdef).as_json(), dict) # the main test is not crashing! # Other properties self.assertIsInstance(t.is_erased(), bool) self.assertIsInstance(t.is_live_on_tablet(), bool) for attrname, col in t.gen_text_filter_columns(): self.assertIsInstance(attrname, str) self.assertIsInstance(col, Column) # Views for page in t.get_spreadsheet_pages(req): self.assertIsInstance(page.get_tsv(), str) self.assertIsInstance(t.get_xml(req), str) self.assertIsInstance(t.get_html(req), str) self.assertIsInstance(t.get_pdf(req), bytes) self.assertIsInstance(t.get_pdf_html(req), str) self.assertIsInstance(t.suggested_pdf_filename(req), str) self.assertIsInstance( t.get_rio_metadata( req, which_idnum=1, uploading_user_id=self.user.id, document_type="some_doc_type", ), str, ) # Help help_url = t.help_url() self.assertEqual( urllib.request.urlopen(help_url).getcode(), HttpStatus.OK, msg=f"Task help not found at {help_url}", ) # Special operations t.apply_special_note(req, "Debug: Special note! (1)", from_console=True) t.apply_special_note(req, "Debug: Special note! (2)", from_console=False) self.assertIsInstance(t.special_notes, list) t.cancel_from_export_log(req, from_console=True) t.cancel_from_export_log(req, from_console=False) # Insert random data and check it doesn't crash. dummy_data_factory.fill_in_task_fields(t) self.assertIsInstance(t.get_html(req), str) # Destructive special operations self.assertFalse(t.is_erased()) t.manually_erase(req) self.assertTrue(t.is_erased()) t.delete_entirely(req)
def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType: dbsession = req.dbsession group_ids = req.user.ids_of_groups_user_may_report_on superuser = req.user.superuser by_year = req.get_bool_param(ViewParam.BY_YEAR, DEFAULT_BY_YEAR) by_month = req.get_bool_param(ViewParam.BY_MONTH, DEFAULT_BY_MONTH) by_task = req.get_bool_param(ViewParam.BY_TASK, DEFAULT_BY_TASK) by_user = req.get_bool_param(ViewParam.BY_USER, DEFAULT_BY_USER) via_index = req.get_bool_param(ViewParam.VIA_INDEX, True) label_year = "year" label_month = "month" label_task = "task" label_user = "******" label_n = "num_tasks_added" final_rows = [] # type: List[Sequence[Sequence[Any]]] colnames = [] # type: List[str] # for type checker if via_index: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Indexed method (preferable) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ selectors = [] # type: List[FunctionElement] groupers = [] # type: List[str] sorters = [] # type: List[Union[str, UnaryExpression]] if by_year: selectors.append( extract_year( TaskIndexEntry.when_created_utc).label(label_year)) groupers.append(label_year) sorters.append(desc(label_year)) if by_month: selectors.append( extract_month( TaskIndexEntry.when_created_utc).label(label_month)) groupers.append(label_month) sorters.append(desc(label_month)) if by_task: selectors.append( TaskIndexEntry.task_table_name.label(label_task)) groupers.append(label_task) sorters.append(label_task) if by_user: selectors.append(User.username.label(label_user)) groupers.append(label_user) sorters.append(label_user) # Regardless: selectors.append(func.count().label(label_n)) # noinspection PyUnresolvedReferences query = ( select(selectors).select_from( TaskIndexEntry.__table__).group_by(*groupers).order_by( *sorters) # ... https://docs.sqlalchemy.org/en/latest/core/tutorial.html#ordering-or-grouping-by-a-label # noqa ) if by_user: # noinspection PyUnresolvedReferences query = query.select_from(User.__table__).where( TaskIndexEntry.adding_user_id == User.id) if not superuser: # Restrict to accessible groups # noinspection PyProtectedMember query = query.where(TaskIndexEntry.group_id.in_(group_ids)) rows, colnames = get_rows_fieldnames_from_query(dbsession, query) # noinspection PyTypeChecker final_rows = rows else: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Without using the server method (worse) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ groupers = [] # type: List[str] sorters = [] # type: List[Tuple[str, bool]] # ... (key, reversed/descending) if by_year: groupers.append(label_year) sorters.append((label_year, True)) if by_month: groupers.append(label_month) sorters.append((label_month, True)) if by_task: groupers.append(label_task) # ... redundant in the SQL, which involves multiple queries # (one per task type), but useful for the Python # aggregation. sorters.append((label_task, False)) if by_user: groupers.append(label_user) sorters.append((label_user, False)) classes = Task.all_subclasses_by_tablename() counter = Counter() for cls in classes: selectors = [] # type: List[FunctionElement] if by_year: selectors.append( # func.year() is specific to some DBs, e.g. MySQL # so is func.extract(); # http://modern-sql.com/feature/extract extract_year( isotzdatetime_to_utcdatetime(cls.when_created) ).label(label_year)) if by_month: selectors.append( extract_month( isotzdatetime_to_utcdatetime( cls.when_created)).label(label_month)) if by_task: selectors.append( literal(cls.__tablename__).label(label_task)) if by_user: selectors.append(User.username.label(label_user)) # Regardless: selectors.append(func.count().label(label_n)) # noinspection PyUnresolvedReferences query = ( select(selectors).select_from(cls.__table__).where( cls._current == True) # noqa: E712 .group_by(*groupers)) if by_user: # noinspection PyUnresolvedReferences query = query.select_from( User.__table__).where(cls._adding_user_id == User.id) if not superuser: # Restrict to accessible groups # noinspection PyProtectedMember query = query.where(cls._group_id.in_(group_ids)) rows, colnames = get_rows_fieldnames_from_query( dbsession, query) if by_task: final_rows.extend(rows) else: for row in rows: # type: RowProxy key = tuple(row[keyname] for keyname in groupers) count = row[label_n] counter.update({key: count}) if not by_task: PseudoRow = namedtuple("PseudoRow", groupers + [label_n]) for key, total in counter.items(): values = list(key) + [total] final_rows.append(PseudoRow(*values)) # Complex sorting: # https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts # noqa for key, descending in reversed(sorters): final_rows.sort(key=attrgetter(key), reverse=descending) return PlainReportType(rows=final_rows, column_names=colnames)
class Iesr(TaskHasPatientMixin, Task, metaclass=IesrMetaclass): """ Server implementation of the IES-R task. """ __tablename__ = "iesr" shortname = "IES-R" provides_trackers = True event = Column("event", UnicodeText, comment="Relevant event") NQUESTIONS = 22 MIN_SCORE = 0 # per question MAX_SCORE = 4 # per question MAX_TOTAL = 88 MAX_AVOIDANCE = 32 MAX_INTRUSION = 28 MAX_HYPERAROUSAL = 28 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS) AVOIDANCE_QUESTIONS = [5, 7, 8, 11, 12, 13, 17, 22] AVOIDANCE_FIELDS = Task.fieldnames_from_list("q", AVOIDANCE_QUESTIONS) INTRUSION_QUESTIONS = [1, 2, 3, 6, 9, 16, 20] INTRUSION_FIELDS = Task.fieldnames_from_list("q", INTRUSION_QUESTIONS) HYPERAROUSAL_QUESTIONS = [4, 10, 14, 15, 18, 19, 21] HYPERAROUSAL_FIELDS = Task.fieldnames_from_list("q", HYPERAROUSAL_QUESTIONS) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Impact of Events Scale – Revised") def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label="IES-R total score (lower is better)", axis_label=f"Total score (out of {self.MAX_TOTAL})", axis_min=-0.5, axis_max=self.MAX_TOTAL + 0.5, ), TrackerInfo( value=self.avoidance_score(), plot_label="IES-R avoidance score", axis_label=f"Avoidance score (out of {self.MAX_AVOIDANCE})", axis_min=-0.5, axis_max=self.MAX_AVOIDANCE + 0.5, ), TrackerInfo( value=self.intrusion_score(), plot_label="IES-R intrusion score", axis_label=f"Intrusion score (out of {self.MAX_INTRUSION})", axis_min=-0.5, axis_max=self.MAX_INTRUSION + 0.5, ), TrackerInfo( value=self.hyperarousal_score(), plot_label="IES-R hyperarousal score", axis_label= f"Hyperarousal score (out of {self.MAX_HYPERAROUSAL})", # noqa axis_min=-0.5, axis_max=self.MAX_HYPERAROUSAL + 0.5, ), ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total_score", coltype=Integer(), value=self.total_score(), comment=f"Total score (/ {self.MAX_TOTAL})", ), SummaryElement( name="avoidance_score", coltype=Integer(), value=self.avoidance_score(), comment=f"Avoidance score (/ {self.MAX_AVOIDANCE})", ), SummaryElement( name="intrusion_score", coltype=Integer(), value=self.intrusion_score(), comment=f"Intrusion score (/ {self.MAX_INTRUSION})", ), SummaryElement( name="hyperarousal_score", coltype=Integer(), value=self.hyperarousal_score(), comment=f"Hyperarousal score (/ {self.MAX_HYPERAROUSAL})", ), ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE t = self.total_score() a = self.avoidance_score() i = self.intrusion_score() h = self.hyperarousal_score() return [ CtvInfo(content=(f"IES-R total score {t}/{self.MAX_TOTAL} " f"(avoidance {a}/{self.MAX_AVOIDANCE} " f"intrusion {i}/{self.MAX_INTRUSION}, " f"hyperarousal {h}/{self.MAX_HYPERAROUSAL})")) ] def total_score(self) -> int: return self.sum_fields(self.QUESTION_FIELDS) def avoidance_score(self) -> int: return self.sum_fields(self.AVOIDANCE_FIELDS) def intrusion_score(self) -> int: return self.sum_fields(self.INTRUSION_FIELDS) def hyperarousal_score(self) -> int: return self.sum_fields(self.HYPERAROUSAL_FIELDS) def is_complete(self) -> bool: return bool(self.field_contents_valid() and self.event and self.all_fields_not_none(self.QUESTION_FIELDS)) def get_task_html(self, req: CamcopsRequest) -> str: option_dict = {None: None} for a in range(self.MIN_SCORE, self.MAX_SCORE + 1): option_dict[a] = req.wappstring(AS.IESR_A_PREFIX + str(a)) h = f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} <tr> <td>Total score</td> <td>{answer(self.total_score())} / {self.MAX_TOTAL}</td> </td> <tr> <td>Avoidance score</td> <td>{answer(self.avoidance_score())} / {self.MAX_AVOIDANCE}</td> </td> <tr> <td>Intrusion score</td> <td>{answer(self.intrusion_score())} / {self.MAX_INTRUSION}</td> </td> <tr> <td>Hyperarousal score</td> <td>{answer(self.hyperarousal_score())} / {self.MAX_HYPERAROUSAL}</td> </td> </table> </div> <table class="{CssClass.TASKDETAIL}"> {tr_qa(req.sstring(SS.EVENT), self.event)} </table> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer (0–4)</th> </tr> """ # noqa for q in range(1, self.NQUESTIONS + 1): a = getattr(self, "q" + str(q)) fa = (f"{a}: {get_from_dict(option_dict, a)}" if a is not None else None) h += tr(self.wxstring(req, "q" + str(q)), answer(fa)) h += (""" </table> """ + DATA_COLLECTION_UNLESS_UPGRADED_DIV) return h def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression(req.snomed( SnomedLookup.IESR_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.IESR_SCALE), {req.snomed(SnomedLookup.IESR_SCORE): self.total_score()}, )) return codes
class Suppsp(TaskHasPatientMixin, Task, metaclass=SuppspMetaclass): __tablename__ = "suppsp" shortname = "SUPPS-P" N_QUESTIONS = 20 MIN_SCORE_PER_Q = 1 MAX_SCORE_PER_Q = 4 MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS N_Q_PER_SUBSCALE = 4 # always MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS) NEGATIVE_URGENCY_QUESTIONS = Task.fieldnames_from_list("q", {6, 8, 13, 15}) LACK_OF_PERSEVERANCE_QUESTIONS = Task.fieldnames_from_list( "q", {1, 4, 7, 11}) LACK_OF_PREMEDITATION_QUESTIONS = Task.fieldnames_from_list( "q", {2, 5, 12, 19}) SENSATION_SEEKING_QUESTIONS = Task.fieldnames_from_list( "q", {9, 14, 16, 18}) POSITIVE_URGENCY_QUESTIONS = Task.fieldnames_from_list( "q", {3, 10, 17, 20}) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Short UPPS-P Impulsive Behaviour Scale") def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score [{self.MIN_SCORE}–{self.MAX_SCORE}]", ), SummaryElement( name="negative_urgency", coltype=Integer(), value=self.negative_urgency_score(), comment=f"Negative urgency {subscale_range}", ), SummaryElement( name="lack_of_perseverance", coltype=Integer(), value=self.lack_of_perseverance_score(), comment=f"Lack of perseverance {subscale_range}", ), SummaryElement( name="lack_of_premeditation", coltype=Integer(), value=self.lack_of_premeditation_score(), comment=f"Lack of premeditation {subscale_range}", ), SummaryElement( name="sensation_seeking", coltype=Integer(), value=self.sensation_seeking_score(), comment=f"Sensation seeking {subscale_range}", ), SummaryElement( name="positive_urgency", coltype=Integer(), value=self.positive_urgency_score(), comment=f"Positive urgency {subscale_range}", ), ] def is_complete(self) -> bool: if self.any_fields_none(self.ALL_QUESTIONS): return False if not self.field_contents_valid(): return False return True def total_score(self) -> int: return self.sum_fields(self.ALL_QUESTIONS) def negative_urgency_score(self) -> int: return self.sum_fields(self.NEGATIVE_URGENCY_QUESTIONS) def lack_of_perseverance_score(self) -> int: return self.sum_fields(self.LACK_OF_PERSEVERANCE_QUESTIONS) def lack_of_premeditation_score(self) -> int: return self.sum_fields(self.LACK_OF_PREMEDITATION_QUESTIONS) def sensation_seeking_score(self) -> int: return self.sum_fields(self.SENSATION_SEEKING_QUESTIONS) def positive_urgency_score(self) -> int: return self.sum_fields(self.POSITIVE_URGENCY_QUESTIONS) def get_task_html(self, req: CamcopsRequest) -> str: normal_score_dict = { None: None, 1: "1 — " + self.wxstring(req, "a0"), 2: "2 — " + self.wxstring(req, "a1"), 3: "3 — " + self.wxstring(req, "a2"), 4: "4 — " + self.wxstring(req, "a3"), } reverse_score_dict = { None: None, 4: "4 — " + self.wxstring(req, "a0"), 3: "3 — " + self.wxstring(req, "a1"), 2: "2 — " + self.wxstring(req, "a2"), 1: "1 — " + self.wxstring(req, "a3"), } reverse_q_nums = {3, 6, 8, 9, 10, 13, 14, 15, 16, 17, 18, 20} fullscale_range = f"[{self.MIN_SCORE}–{self.MAX_SCORE}]" subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]" rows = "" for q_num in range(1, self.N_QUESTIONS + 1): q_field = "q" + str(q_num) question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field)) score = getattr(self, q_field) score_dict = normal_score_dict if q_num in reverse_q_nums: score_dict = reverse_score_dict answer_cell = get_from_dict(score_dict, score) rows += tr_qa(question_cell, answer_cell) html = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} {negative_urgency_score} {lack_of_perseverance_score} {lack_of_premeditation_score} {sensation_seeking_score} {positive_urgency_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="60%">Question</th> <th width="40%">Score</th> </tr> {rows} </table> <div class="{CssClass.FOOTNOTES}"> [1] Sum for questions 1–20. [2] Sum for questions 6, 8, 13, 15. [3] Sum for questions 1, 4, 7, 11. [4] Sum for questions 2, 5, 12, 19. [5] Sum for questions 9, 14, 16, 18. [6] Sum for questions 3, 10, 17, 20. </div> """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>", f"{answer(self.total_score())} {fullscale_range}", ), negative_urgency_score=tr( self.wxstring(req, "negative_urgency") + " <sup>[2]</sup>", f"{answer(self.negative_urgency_score())} {subscale_range}", ), lack_of_perseverance_score=tr( self.wxstring(req, "lack_of_perseverance") + " <sup>[3]</sup>", f"{answer(self.lack_of_perseverance_score())} {subscale_range}", # noqa: E501 ), lack_of_premeditation_score=tr( self.wxstring(req, "lack_of_premeditation") + " <sup>[4]</sup>", f"{answer(self.lack_of_premeditation_score())} {subscale_range}", # noqa: E501 ), sensation_seeking_score=tr( self.wxstring(req, "sensation_seeking") + " <sup>[5]</sup>", f"{answer(self.sensation_seeking_score())} {subscale_range}", ), positive_urgency_score=tr( self.wxstring(req, "positive_urgency") + " <sup>[6]</sup>", f"{answer(self.positive_urgency_score())} {subscale_range}", ), rows=rows, ) return html