From 2d813de5ae039bb23f72131c832ecc472e049a23 Mon Sep 17 00:00:00 2001 From: erikvw Date: Wed, 28 Aug 2024 19:08:00 -0500 Subject: [PATCH] refactor QaCase into CrfCase and RequisitionCase --- ...n_study_missing_values_modeladmin_mixin.py | 89 ++++++++- .../qa_report_modeladmin_mixin.py | 9 +- edc_qareports/sql_generator/__init__.py | 7 +- .../sql_generator/{qa_case.py => crf_case.py} | 23 +-- edc_qareports/sql_generator/crf_subquery.py | 63 +++++++ .../sql_generator/requisition_case.py | 16 ++ .../sql_generator/requisition_subquery.py | 58 ++++++ edc_qareports/sql_generator/subquery.py | 39 ---- .../edc_qareports/columns/add_button.html | 1 + .../edc_qareports/columns/change_button.html | 1 + .../edc_qareports/columns/notes_column.html | 2 +- .../columns/subject_identifier_column.html | 1 + edc_qareports/tests/models.py | 52 ++++++ edc_qareports/tests/test_settings.py | 15 +- edc_qareports/tests/tests/test_qa.py | 171 +++++++++++++++++- 15 files changed, 473 insertions(+), 74 deletions(-) rename edc_qareports/sql_generator/{qa_case.py => crf_case.py} (52%) create mode 100644 edc_qareports/sql_generator/crf_subquery.py create mode 100644 edc_qareports/sql_generator/requisition_case.py create mode 100644 edc_qareports/sql_generator/requisition_subquery.py delete mode 100644 edc_qareports/sql_generator/subquery.py create mode 100644 edc_qareports/templates/edc_qareports/columns/add_button.html create mode 100644 edc_qareports/templates/edc_qareports/columns/change_button.html create mode 100644 edc_qareports/templates/edc_qareports/columns/subject_identifier_column.html create mode 100644 edc_qareports/tests/models.py diff --git a/edc_qareports/modeladmin_mixins/on_study_missing_values_modeladmin_mixin.py b/edc_qareports/modeladmin_mixins/on_study_missing_values_modeladmin_mixin.py index 3b0e8b5..0f325c6 100644 --- a/edc_qareports/modeladmin_mixins/on_study_missing_values_modeladmin_mixin.py +++ b/edc_qareports/modeladmin_mixins/on_study_missing_values_modeladmin_mixin.py @@ -1,5 +1,6 @@ from django.apps import apps as django_apps from django.contrib import admin +from django.core.exceptions import ObjectDoesNotExist from django.template.loader import render_to_string from django.urls import reverse from django.utils.translation import gettext as _ @@ -7,6 +8,7 @@ from edc_model_admin.mixins import TemplatesModelAdminMixin from edc_sites.admin import SiteModelAdminMixin from edc_visit_schedule.admin import ScheduleStatusListFilter +from edc_visit_tracking.utils import get_related_visit_model_cls from .qa_report_modeladmin_mixin import QaReportModelAdminMixin @@ -18,14 +20,14 @@ class OnStudyMissingValuesModelAdminMixin( TemplatesModelAdminMixin, ): include_note_column: bool = True - project_reports_admin: str = "meta_reports_admin" - project_subject_admin: str = "meta_subject_admin" - + site_list_display_insert_pos: int = 2 + qa_report_list_display_insert_pos = 4 ordering = ["site", "subject_identifier"] list_display = [ "dashboard", - "subject_identifier", + "render_button", + "subject", "site", "label", "crf", @@ -43,20 +45,72 @@ class OnStudyMissingValuesModelAdminMixin( search_fields = ["subject_identifier", "label"] - def dashboard(self, obj=None, label=None) -> str: - url = self.get_subject_dashboard_url(obj=obj) - if not url: + @admin.display(description="Update") + def render_button(self, obj=None): + crf_model_cls = django_apps.get_model(obj.label_lower) + try: + django_apps.get_model(obj.label_lower).objects.get(id=obj.original_id) + except ObjectDoesNotExist: + url = reverse( + f"{self.crf_admin_site_name(crf_model_cls)}:" + f"{obj.label_lower.replace('.', '_')}_add" + ) + url = ( + f"{url}?next={self.admin_site.name}:" + f"{self.model._meta.label_lower.replace('.', '_')}_changelist" + f"&subject_identifier={obj.subject_identifier}" + f"&subject_visit={obj.subject_visit_id}" + f"&appointment={self.related_visit(obj).appointment.id}" + f"&requisition={obj.original_id}" + ) + title = _(f"Add {crf_model_cls._meta.verbose_name}") + label = _("Add CRF") + crf_button = render_to_string( + "edc_qareports/columns/add_button.html", + context=dict(title=title, url=url, label=label), + ) + else: url = reverse( - f"{self.project_subject_admin}:{obj.label_lower.replace('.', '_')}_change", + f"{self.crf_admin_site_name(crf_model_cls)}:" + f"{obj.label_lower.replace('.', '_')}_change", args=(obj.original_id,), ) url = ( - f"{url}?next={self.project_reports_admin}:" + f"{url}?next={self.admin_site.name}:" f"{self.model._meta.label_lower.replace('.', '_')}_changelist" ) - context = dict(title=_("Go to CRF"), url=url, label=label) + title = _(f"Change {crf_model_cls._meta.verbose_name}") + label = _("Change CRF") + crf_button = render_to_string( + "edc_qareports/columns/change_button.html", + context=dict(title=title, url=url, label=label), + ) + return crf_button + + def dashboard(self, obj=None, label=None) -> str: + dashboard_url = reverse( + self.get_subject_dashboard_url_name(obj=obj), + kwargs=dict( + subject_identifier=obj.subject_identifier, + appointment=self.related_visit(obj).appointment.id, + ), + ) + context = dict(title=_("Go to subject's dashboard"), url=dashboard_url, label=label) return render_to_string("dashboard_button.html", context=context) + @staticmethod + def crf_admin_site_name(crf_model_cls) -> str: + """Returns the name of the admin site CRFs are registered + by assuming admin site name follows the edc naming convention. + + For example: 'meta_subject_admin' or 'effect_subject_admin' + """ + return f"{crf_model_cls._meta.label_lower.split('.')[0]}_admin" + + @staticmethod + def related_visit(obj=None): + return get_related_visit_model_cls().objects.get(id=obj.subject_visit_id) + @admin.display(description="CRF", ordering="label_lower") def crf(self, obj=None) -> str: model_cls = django_apps.get_model(obj.label_lower) @@ -71,3 +125,18 @@ def report_date(self, obj) -> str | None: if obj.report_datetime: return obj.report_datetime.date() return None + + @admin.display(description="Subject", ordering="subject_identifier") + def subject(self, obj) -> str | None: + url = reverse( + f"{self.admin_site.name}:{self.model._meta.label_lower.replace('.', '_')}" + "_changelist" + ) + return render_to_string( + "edc_qareports/columns/subject_identifier_column.html", + { + "subject_identifier": obj.subject_identifier, + "url": url, + "title": _("Filter by subject"), + }, + ) diff --git a/edc_qareports/modeladmin_mixins/qa_report_modeladmin_mixin.py b/edc_qareports/modeladmin_mixins/qa_report_modeladmin_mixin.py index dc4efb1..40fa89c 100644 --- a/edc_qareports/modeladmin_mixins/qa_report_modeladmin_mixin.py +++ b/edc_qareports/modeladmin_mixins/qa_report_modeladmin_mixin.py @@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.template.loader import render_to_string from django.urls import reverse +from django.utils.translation import gettext as _ from edc_constants.constants import NEW from ..models import QaReportLog @@ -93,13 +94,13 @@ def notes(self, obj=None) -> str: except ObjectDoesNotExist: note_model_obj = None url = reverse(f"{note_app_label}_admin:{note_url_name}_add") - title = "Add" + title = _("Add if pending or cannot be resolved") else: url = reverse( f"{note_app_label}_admin:{note_url_name}_change", args=(note_model_obj.id,), ) - title = "Edit" + title = _("Edit if pending or cannot be resolved") url = ( f"{url}?next={next_url_name},subject_identifier,q" @@ -112,9 +113,9 @@ def notes(self, obj=None) -> str: def get_notes_label(self, obj) -> str: if not obj: - label = "Add" + label = _("Add") elif not obj.note: - label = "Edit" + label = _("Edit") else: label = truncate_string(obj.note, max_length=35) return label diff --git a/edc_qareports/sql_generator/__init__.py b/edc_qareports/sql_generator/__init__.py index 779a65b..a8aec23 100644 --- a/edc_qareports/sql_generator/__init__.py +++ b/edc_qareports/sql_generator/__init__.py @@ -1,4 +1,5 @@ -from .qa_case import QaCase, QaCaseError +from .crf_case import CrfCase, CrfCaseError +from .crf_subquery import CrfSubquery +from .requisition_case import RequisitionCase +from .requisition_subquery import RequisitionSubquery from .sql_view_generator import SqlViewGenerator -from .subquery import Subquery -from .subquery_from_dict import subquery_from_dict diff --git a/edc_qareports/sql_generator/qa_case.py b/edc_qareports/sql_generator/crf_case.py similarity index 52% rename from edc_qareports/sql_generator/qa_case.py rename to edc_qareports/sql_generator/crf_case.py index 61cbca1..4013471 100644 --- a/edc_qareports/sql_generator/qa_case.py +++ b/edc_qareports/sql_generator/crf_case.py @@ -1,43 +1,40 @@ from dataclasses import dataclass, field +import sqlglot from django.apps import apps as django_apps from django.db import OperationalError, connection -from .subquery_from_dict import subquery_from_dict +from .crf_subquery import CrfSubquery -class QaCaseError(Exception): +class CrfCaseError(Exception): pass @dataclass(kw_only=True) -class QaCase: +class CrfCase: label: str = None dbtable: str = None label_lower: str = None fld_name: str | None = None where: str | None = None list_tables: list[tuple[str, str, str]] | None = field(default_factory=list) - - def __post_init__(self): - if self.fld_name is None and self.where is None: - raise QaCaseError("Expected either 'fld_name' or 'where'. Got None for both.") - elif self.fld_name is not None and self.where is not None: - raise QaCaseError("Expected either 'fld_name' or 'where', not both.") + subjectvisit_dbtable: str | None = None @property def sql(self): - return subquery_from_dict([self.__dict__]) + sql = CrfSubquery(**self.__dict__).sql + vendor = "postgres" if connection.vendor.startswith("postgres") else connection.vendor + return sqlglot.transpile(sql, read="mysql", write=vendor)[0] @property def model_cls(self): return django_apps.get_model(self.label_lower) def fetchall(self): - sql = subquery_from_dict([self.__dict__]) with connection.cursor() as cursor: try: - cursor.execute(sql) + cursor.execute(self.sql) except OperationalError as e: - raise QaCaseError(f"{e}. See {self}.") + raise CrfCaseError(f"{e}. See {self}.") return cursor.fetchall() diff --git a/edc_qareports/sql_generator/crf_subquery.py b/edc_qareports/sql_generator/crf_subquery.py new file mode 100644 index 0000000..6e7d1ad --- /dev/null +++ b/edc_qareports/sql_generator/crf_subquery.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass, field +from string import Template + + +class CrfSubqueryError(Exception): + pass + + +@dataclass(kw_only=True) +class CrfSubquery: + label: str = None + label_lower: str = None + dbtable: str = None + fld_name: str | None = None + subjectvisit_dbtable: str | None = None + where: str | None = None + list_tables: list[tuple[str, str, str]] | None = field(default_factory=list) + template: Template = field( + init=False, + default=Template( + "select v.subject_identifier, crf.id as original_id, crf.subject_visit_id, " + "crf.report_datetime, crf.site_id, v.visit_code, " + "v.visit_code_sequence, v.schedule_name, crf.modified, " + "'${label_lower}' as label_lower, " + "'${label}' as label, count(*) as records " + "from ${dbtable} as crf " + "left join ${subjectvisit_dbtable} as v on v.id=crf.subject_visit_id " + "${left_joins} " + "where ${where} " + "group by v.subject_identifier, crf.subject_visit_id, crf.report_datetime, " + "crf.site_id, v.visit_code, v.visit_code_sequence, v.schedule_name, crf.modified" + ), + ) + + def __post_init__(self): + # default where statement if not provided and have fld_name. + if self.where is None and self.fld_name: + self.where = f"crf.{self.fld_name} is null" + if not self.label_lower: + raise CrfSubqueryError("label_lower is required") + if not self.subjectvisit_dbtable: + self.subjectvisit_dbtable = f"{self.label_lower.split('.')[0]}_subjectvisit" + + @property + def left_joins(self) -> str: + """Add list tbls to access list cols by 'name' instead of 'id'""" + left_join = [] + for opts in self.list_tables or []: + list_field, list_dbtable, alias = opts + left_join.append( + f"left join {list_dbtable} as {alias} on crf.{list_field}={alias}.id" + ) + return " ".join(left_join) + + @property + def sql(self): + opts = {k: v for k, v in self.__dict__.items() if v is not None} + opts.update(left_joins=self.left_joins) + try: + sql = self.template.substitute(**opts).replace(";", "") + except KeyError as e: + raise CrfSubqueryError(e) + return sql diff --git a/edc_qareports/sql_generator/requisition_case.py b/edc_qareports/sql_generator/requisition_case.py new file mode 100644 index 0000000..cd31126 --- /dev/null +++ b/edc_qareports/sql_generator/requisition_case.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + +from .crf_case import CrfCase +from .requisition_subquery import RequisitionSubquery + + +@dataclass(kw_only=True) +class RequisitionCase(CrfCase): + panel: str = None + subjectrequisition_dbtable: str | None = None + panel_dbtable: str | None = None + + @property + def sql(self): + sql = RequisitionSubquery(**self.__dict__).sql + return sql.format(panel=self.panel) diff --git a/edc_qareports/sql_generator/requisition_subquery.py b/edc_qareports/sql_generator/requisition_subquery.py new file mode 100644 index 0000000..0a24984 --- /dev/null +++ b/edc_qareports/sql_generator/requisition_subquery.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass, field +from string import Template + +from edc_constants.constants import YES + +from .crf_subquery import CrfSubquery + + +class RequisitionSubqueryError(Exception): + pass + + +@dataclass(kw_only=True) +class RequisitionSubquery(CrfSubquery): + """Generate a SELECT query returning requisitions where + is_drawn=Yes for a given panel but results have not been captured + in the result CRF. + + For example requisition exists for panel FBC but results_fbc + CRF does not exist. + """ + + panel: str = None + subjectrequisition_dbtable: str | None = None + panel_dbtable: str | None = None + template: str = field( + init=False, + default=Template( + "select req.subject_identifier, req.id as original_id, req.subject_visit_id, " + "req.report_datetime, req.site_id, v.visit_code, v.visit_code_sequence, " + "v.schedule_name, req.modified, '${label_lower}' as label_lower, " + "'${label}' as label, count(*) as records " + "from ${subjectrequisition_dbtable} as req " + "left join ${dbtable} as crf on req.id=crf.requisition_id " + "left join ${subjectvisit_dbtable} as v on v.id=req.subject_visit_id " + "${left_joins} " + "left join ${panel_dbtable} as panel on req.panel_id=panel.id " + f"where panel.name='${{panel}}' and req.is_drawn='{YES}' and crf.id is null " + "group by req.id, req.subject_identifier, req.subject_visit_id, " + "req.report_datetime, req.site_id, v.visit_code, v.visit_code_sequence, " + "v.schedule_name, req.modified" + ), + ) + + def __post_init__(self): + # default where statement if not provided and have fld_name. + if self.where is None and self.fld_name: + self.where = f"crf.{self.fld_name} is null" + if not self.label_lower: + raise RequisitionSubqueryError("label_lower is required") + if not self.subjectvisit_dbtable: + self.subjectvisit_dbtable = f"{self.label_lower.split('.')[0]}_subjectvisit" + if not self.subjectrequisition_dbtable: + self.subjectrequisition_dbtable = ( + f"{self.label_lower.split('.')[0]}_subjectrequisition" + ) + if not self.panel_dbtable: + self.panel_dbtable = "edc_lab_panel" diff --git a/edc_qareports/sql_generator/subquery.py b/edc_qareports/sql_generator/subquery.py deleted file mode 100644 index 0e25a47..0000000 --- a/edc_qareports/sql_generator/subquery.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass(kw_only=True) -class Subquery: - label: str = None - label_lower: str = None - dbtable: str = None - fld_name: str | None = None - where: str | None = None - list_tables: list[tuple[str, str, str]] | None = field(default_factory=list) - template: str = field( - init=False, - default="select v.subject_identifier, crf.id as original_id, crf.subject_visit_id, crf.report_datetime, crf.site_id, v.visit_code, v.visit_code_sequence, v.schedule_name, crf.modified, '{label_lower}' as label_lower, '{label}' as label, count(*) as records from {dbtable} as crf left join meta_subject_subjectvisit as v on v.id=crf.subject_visit_id {left_joins} where {where} group by v.subject_identifier, crf.subject_visit_id, crf.report_datetime, crf.site_id, v.visit_code, v.visit_code_sequence, v.schedule_name, crf.modified", # noqa - ) - sql: str | None = field(init=False, default=None) - - def __post_init__(self): - if self.where is None: - self.where = f"crf.{self.fld_name} is null" - self.sql = self.template.format( - label=self.label, - label_lower=self.label_lower, - dbtable=self.dbtable, - where=self.where, - left_joins=self.left_joins, - ) - self.sql = self.sql.replace(";", "") - - @property - def left_joins(self) -> str: - """Add list tbls to access list cols by 'name' instead of 'id'""" - left_join = [] - for opts in self.list_tables or []: - list_field, list_dbtable, alias = opts - left_join.append( - f"left join {list_dbtable} as {alias} on crf.{list_field}={alias}.id" - ) - return " ".join(left_join) diff --git a/edc_qareports/templates/edc_qareports/columns/add_button.html b/edc_qareports/templates/edc_qareports/columns/add_button.html new file mode 100644 index 0000000..a71ef9e --- /dev/null +++ b/edc_qareports/templates/edc_qareports/columns/add_button.html @@ -0,0 +1 @@ +Add CRF diff --git a/edc_qareports/templates/edc_qareports/columns/change_button.html b/edc_qareports/templates/edc_qareports/columns/change_button.html new file mode 100644 index 0000000..6275969 --- /dev/null +++ b/edc_qareports/templates/edc_qareports/columns/change_button.html @@ -0,0 +1 @@ +Change CRF diff --git a/edc_qareports/templates/edc_qareports/columns/notes_column.html b/edc_qareports/templates/edc_qareports/columns/notes_column.html index 6838091..9e3a411 100644 --- a/edc_qareports/templates/edc_qareports/columns/notes_column.html +++ b/edc_qareports/templates/edc_qareports/columns/notes_column.html @@ -1 +1 @@ -{{ label }} +{{ label }} diff --git a/edc_qareports/templates/edc_qareports/columns/subject_identifier_column.html b/edc_qareports/templates/edc_qareports/columns/subject_identifier_column.html new file mode 100644 index 0000000..b8ecdfb --- /dev/null +++ b/edc_qareports/templates/edc_qareports/columns/subject_identifier_column.html @@ -0,0 +1 @@ +{{ label|default:subject_identifier }} \ No newline at end of file diff --git a/edc_qareports/tests/models.py b/edc_qareports/tests/models.py new file mode 100644 index 0000000..78bf778 --- /dev/null +++ b/edc_qareports/tests/models.py @@ -0,0 +1,52 @@ +from django.db import models +from django.db.models import PROTECT +from edc_appointment_app.models import SubjectVisit +from edc_crf.model_mixins import CrfStatusModelMixin +from edc_lab.model_mixins import CrfWithRequisitionModelMixin, requisition_fk_options +from edc_lab_panel.panels import fbc_panel +from edc_lab_results.model_mixins import ( + BloodResultsModelMixin, + HaemoglobinModelMixin, + HctModelMixin, + MchcModelMixin, + MchModelMixin, + McvModelMixin, + PlateletsModelMixin, + RbcModelMixin, + WbcModelMixin, +) +from edc_model.models import BaseUuidModel +from edc_sites.model_mixins import SiteModelMixin + +requisition_fk_options.update(to="edc_appointment_app.SubjectRequisition") + + +class BloodResultsFbc( + SiteModelMixin, + CrfWithRequisitionModelMixin, + HaemoglobinModelMixin, + HctModelMixin, + RbcModelMixin, + WbcModelMixin, + PlateletsModelMixin, + MchModelMixin, + MchcModelMixin, + McvModelMixin, + BloodResultsModelMixin, + CrfStatusModelMixin, + BaseUuidModel, +): + lab_panel = fbc_panel + + subject_visit = models.ForeignKey(SubjectVisit, on_delete=PROTECT) + + requisition = models.ForeignKey( + limit_choices_to={"panel__name": fbc_panel.name}, **requisition_fk_options + ) + + def get_summary(self): + return "" + + class Meta(CrfStatusModelMixin.Meta, BaseUuidModel.Meta): + verbose_name = "Blood Result: FBC" + verbose_name_plural = "Blood Results: FBC" diff --git a/edc_qareports/tests/test_settings.py b/edc_qareports/tests/test_settings.py index 926457b..9f01894 100644 --- a/edc_qareports/tests/test_settings.py +++ b/edc_qareports/tests/test_settings.py @@ -19,7 +19,7 @@ "edc_consent.E001", ], EDC_AUTH_SKIP_AUTH_UPDATER=True, - EDC_SITES_REGISTER_DEFAULT=True, + EDC_SITES_REGISTER_DEFAULT=False, INSTALLED_APPS=[ "django.contrib.admin", "django.contrib.auth", @@ -32,14 +32,25 @@ "multisite", "django_db_views", "edc_auth.apps.AppConfig", + "edc_action_item.apps.AppConfig", + "edc_appointment.apps.AppConfig", + "edc_device.apps.AppConfig", + "edc_timepoint.apps.AppConfig", + "edc_facility.apps.AppConfig", "edc_lab.apps.AppConfig", + "edc_lab_panel.apps.AppConfig", + "edc_lab_results.apps.AppConfig", + "edc_identifier.apps.AppConfig", "edc_notification.apps.AppConfig", "edc_registration.apps.AppConfig", "edc_sites.apps.AppConfig", + "edc_visit_schedule.apps.AppConfig", + "edc_visit_tracking.apps.AppConfig", "edc_qareports.apps.AppConfig", + "edc_appointment_app.apps.AppConfig", "edc_appconfig.apps.AppConfig", ], - add_dashboard_middleware=False, + add_dashboard_middleware=True, add_lab_dashboard_middleware=False, add_adverse_event_dashboard_middleware=False, ).settings diff --git a/edc_qareports/tests/tests/test_qa.py b/edc_qareports/tests/tests/test_qa.py index a890051..100e3ce 100644 --- a/edc_qareports/tests/tests/test_qa.py +++ b/edc_qareports/tests/tests/test_qa.py @@ -1,22 +1,189 @@ -from django.test import TestCase +import datetime as dt +from zoneinfo import ZoneInfo + +import time_machine +from django.db import OperationalError, connection +from django.test import TestCase, override_settings +from edc_appointment.models import Appointment +from edc_appointment.tests.helper import Helper +from edc_appointment_app.models import CrfOne, Panel, SubjectRequisition, SubjectVisit +from edc_appointment_app.tests import AppointmentAppTestCaseMixin from edc_auth.get_app_codenames import get_app_codenames +from edc_constants.constants import YES +from edc_lab_panel.constants import FBC +from edc_reportable import TEN_X_9_PER_LITER + +from edc_qareports.sql_generator import CrfCase, CrfCaseError, RequisitionCase +from edc_qareports.sql_generator.crf_subquery import CrfSubqueryError + +from ..models import BloodResultsFbc + +utc_tz = ZoneInfo("UTC") + +@override_settings(SITE_ID=10) +@time_machine.travel(dt.datetime(2019, 6, 11, 8, 00, tzinfo=utc_tz)) +class TestQA(AppointmentAppTestCaseMixin, TestCase): + helper_cls = Helper -class TestQA(TestCase): + def create_unscheduled_appointments(self, appointment): + pass def test_codenames(self): + """Assert default codenames. + + Note: in tests this will include codenames for test models. + """ codenames = get_app_codenames("edc_qareports") codenames.sort() expected_codenames = [ + "edc_qareports.add_bloodresultsfbc", "edc_qareports.add_edcpermissions", "edc_qareports.add_note", + "edc_qareports.change_bloodresultsfbc", "edc_qareports.change_edcpermissions", "edc_qareports.change_note", + "edc_qareports.delete_bloodresultsfbc", "edc_qareports.delete_edcpermissions", "edc_qareports.delete_note", + "edc_qareports.view_bloodresultsfbc", "edc_qareports.view_edcpermissions", "edc_qareports.view_note", "edc_qareports.view_qareportlog", "edc_qareports.view_qareportlogsummary", ] self.assertEqual(codenames, expected_codenames) + + def test_crfcase_invalid(self): + crf_case = CrfCase() + # sql template requires a complete dictionary of values + self.assertRaises(CrfSubqueryError, getattr, crf_case, "sql") + + def test_fldname_crfcase(self): + """Assert generates valid SQL or raises""" + # raise for bad fld_name + crf_case = CrfCase( + label="F1 is missing", + dbtable="edc_appointment_app_crfone", + label_lower="edc_appointment_app.crfone", + fld_name="bad_fld_name", + ) + + try: + with connection.cursor() as cursor: + cursor.execute(crf_case.sql) + except OperationalError as e: + self.assertIn("bad_fld_name", str(e)) + else: + self.fail("OperationalError not raised for invalid fld_name.") + + # ok + crf_case = CrfCase( + label="F1 is missing", + dbtable="edc_appointment_app_crfone", + label_lower="edc_appointment_app.crfone", + fld_name="f1", + ) + try: + with connection.cursor() as cursor: + cursor.execute(crf_case.sql) + except OperationalError as e: + self.fail(f"OperationalError unexpectedly raised, Got {e}.") + + def test_where_instead_of_fldname_crfcase(self): + """Assert generates valid SQL or raises""" + # raise for bad fld_name + crf_case = CrfCase( + label="No F1 when F2 is YES", + dbtable="edc_appointment_app_crfone", + label_lower="edc_appointment_app.crfone", + where="bad_fld_name is null and f2='Yes'", + ) + + try: + with connection.cursor() as cursor: + cursor.execute(crf_case.sql) + except OperationalError as e: + self.assertIn("bad_fld_name", str(e)) + else: + self.fail("OperationalError not raised for invalid fld_name.") + + # ok + crf_case = CrfCase( + label="No F1 when F2 is YES", + dbtable="edc_appointment_app_crfone", + label_lower="edc_appointment_app.crfone", + where="f1 is null and f2='Yes'", + ) + try: + with connection.cursor() as cursor: + cursor.execute(crf_case.sql) + except OperationalError as e: + self.fail(f"OperationalError unexpectedly raised, Got {e}.") + + def test_subquery_crfcase(self): + crf_case = CrfCase( + label="No F1 when F2 is YES", + dbtable="edc_appointment_app_crfone", + label_lower="edc_appointment_app.crfone", + where="f1 is null and f2='Yes'", + ) + try: + crf_case.fetchall() + except CrfCaseError as e: + self.fail(f"CrfCaseError unexpectedly raised, Got {e}.") + + def test_subquery_with_recs_crfcase(self): + appointment = Appointment.objects.get(visit_code="1000", visit_code_sequence=0) + subject_visit = SubjectVisit.objects.get(appointment=appointment) + CrfOne.objects.create(subject_visit=subject_visit, f1=None, f2=YES) + crf_case = CrfCase( + label="No F1 when F2 is YES", + dbtable="edc_appointment_app_crfone", + label_lower="edc_appointment_app.crfone", + where="f1 is null and f2='Yes'", + ) + try: + rows = crf_case.fetchall() + except CrfCaseError as e: + self.fail(f"CrfCaseError unexpectedly raised, Got {e}.") + self.assertEqual(len(rows), 1) + + def test_requisition_case(self): + appointment = Appointment.objects.get(visit_code="1000", visit_code_sequence=0) + subject_visit = SubjectVisit.objects.get(appointment=appointment) + panel = Panel.objects.create(name=FBC) + subject_requisition = SubjectRequisition.objects.create( + subject_visit=subject_visit, is_drawn=YES, panel=panel + ) + # need to pass table names explicitly since app_name for + # BloodResultsFbc CRF is not the same as subject_visit and + # subject_requisition. Normally the defaults are correct. + requisition_case = RequisitionCase( + label="FBC Requisition, no results", + dbtable="edc_qareports_bloodresultsfbc", + label_lower="edc_qareports.bloodresultsfbc", + panel=FBC, + subjectvisit_dbtable="edc_appointment_app_subjectvisit", + subjectrequisition_dbtable="edc_appointment_app_subjectrequisition", + panel_dbtable="edc_appointment_app_panel", + ) + try: + rows = requisition_case.fetchall() + except CrfCaseError as e: + self.fail(f"CrfCaseError unexpectedly raised, Got {e}.") + self.assertEqual(len(rows), 1) + + # add the result CRF + BloodResultsFbc.objects.create( + subject_visit=subject_visit, + requisition=subject_requisition, + wbc_value=10.0, + wbc_units=TEN_X_9_PER_LITER, + site_id=10, + ) + try: + rows = requisition_case.fetchall() + except CrfCaseError as e: + self.fail(f"CrfCaseError unexpectedly raised, Got {e}.") + self.assertEqual(len(rows), 0)