-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'release/0.1.14' into main
- Loading branch information
Showing
9 changed files
with
248 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
from .note_model_mixin import NoteModelMixin | ||
from .on_study_missing_values_model_mixin import OnStudyMissingValuesModelMixin | ||
from .qa_report_model_mixin import QaReportModelMixin | ||
from .qa_reports_permissions import qa_reports_permissions |
24 changes: 24 additions & 0 deletions
24
edc_qareports/model_mixins/on_study_missing_values_model_mixin.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from django.db import models | ||
|
||
|
||
class OnStudyMissingValuesModelMixin(models.Model): | ||
original_id = models.UUIDField(null=True) | ||
|
||
label_lower = models.CharField(max_length=150, null=True) | ||
|
||
subject_visit_id = models.UUIDField(null=True) | ||
|
||
report_datetime = models.DateTimeField(null=True) | ||
|
||
label = models.CharField(max_length=50, null=True) | ||
|
||
visit_code = models.CharField(max_length=25, null=True) | ||
|
||
visit_code_sequence = models.IntegerField(null=True) | ||
|
||
schedule_name = models.CharField(max_length=25, null=True) | ||
|
||
modified = models.DateTimeField(null=True) | ||
|
||
class Meta: | ||
abstract = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,6 @@ | ||
from .list_filters import NoteStatusListFilter | ||
from .note_modeladmin_mixin import NoteModelAdminMixin | ||
from .on_study_missing_values_modeladmin_mixin import ( | ||
OnStudyMissingValuesModelAdminMixin, | ||
) | ||
from .qa_report_modeladmin_mixin import QaReportModelAdminMixin |
73 changes: 73 additions & 0 deletions
73
edc_qareports/modeladmin_mixins/on_study_missing_values_modeladmin_mixin.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from django.apps import apps as django_apps | ||
from django.contrib import admin | ||
from django.template.loader import render_to_string | ||
from django.urls import reverse | ||
from django.utils.translation import gettext as _ | ||
from edc_model_admin.dashboard import ModelAdminDashboardMixin | ||
from edc_model_admin.mixins import TemplatesModelAdminMixin | ||
from edc_sites.admin import SiteModelAdminMixin | ||
from edc_visit_schedule.admin import ScheduleStatusListFilter | ||
|
||
from .qa_report_modeladmin_mixin import QaReportModelAdminMixin | ||
|
||
|
||
class OnStudyMissingValuesModelAdminMixin( | ||
QaReportModelAdminMixin, | ||
SiteModelAdminMixin, | ||
ModelAdminDashboardMixin, | ||
TemplatesModelAdminMixin, | ||
): | ||
include_note_column: bool = True | ||
project_reports_admin: str = "meta_reports_admin" | ||
project_subject_admin: str = "meta_subject_admin" | ||
|
||
ordering = ["site", "subject_identifier"] | ||
|
||
list_display = [ | ||
"dashboard", | ||
"subject_identifier", | ||
"site", | ||
"label", | ||
"crf", | ||
"visit", | ||
"report_date", | ||
"created", | ||
] | ||
|
||
list_filter = [ | ||
ScheduleStatusListFilter, | ||
"label", | ||
"visit_code", | ||
"report_datetime", | ||
] | ||
|
||
search_fields = ["subject_identifier", "label"] | ||
|
||
def dashboard(self, obj=None, label=None) -> str: | ||
url = self.get_subject_dashboard_url(obj=obj) | ||
if not url: | ||
url = reverse( | ||
f"{self.project_subject_admin}:{obj.label_lower.replace('.', '_')}_change", | ||
args=(obj.original_id,), | ||
) | ||
url = ( | ||
f"{url}?next={self.project_reports_admin}:" | ||
f"{self.model._meta.label_lower.replace('.', '_')}_changelist" | ||
) | ||
context = dict(title=_("Go to CRF"), url=url, label=label) | ||
return render_to_string("dashboard_button.html", context=context) | ||
|
||
@admin.display(description="CRF", ordering="label_lower") | ||
def crf(self, obj=None) -> str: | ||
model_cls = django_apps.get_model(obj.label_lower) | ||
return model_cls._meta.verbose_name | ||
|
||
@admin.display(description="Visit", ordering="visit_code") | ||
def visit(self, obj=None) -> str: | ||
return f"{obj.visit_code}.{obj.visit_code_sequence}" | ||
|
||
@admin.display(description="Report date", ordering="report_datetime") | ||
def report_date(self, obj) -> str | None: | ||
if obj.report_datetime: | ||
return obj.report_datetime.date() | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .generate_subquery_for_missing_values import generate_subquery_for_missing_values | ||
from .select_from import SelectFrom | ||
from .sql_view_generator import SqlViewGenerator |
29 changes: 29 additions & 0 deletions
29
edc_qareports/sql_generator/generate_subquery_for_missing_values.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from .select_from import SelectFrom | ||
|
||
|
||
def generate_subquery_for_missing_values( | ||
cases: list[dict[str:str, str:str, str:str]], | ||
as_list: bool | None = False, | ||
) -> str | list: | ||
"""Returns an SQL select statement as a union of the select | ||
statements of each case. | ||
args: | ||
cases = [{ | ||
"label_lower": "my_app.hivhistory", | ||
"dbtable": "my_app_hivhistory", | ||
"field": "hiv_init_date", | ||
"label": "missing HIV initiation date", | ||
"list_tables": [(list_field, list_dbtable, alias), ...], | ||
}, ...] | ||
Note: `list_field` is the CRF id field, for example: | ||
left join <list_dbtable> as <alias> on crf.<list_field>=<alias>.id | ||
""" | ||
select_from_list = [] | ||
for case in cases: | ||
select_from = SelectFrom(**case) | ||
select_from_list.append(select_from.sql) | ||
if as_list: | ||
return select_from_list | ||
return " UNION ".join(select_from_list) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from dataclasses import dataclass, field | ||
|
||
|
||
@dataclass(kw_only=True) | ||
class SelectFrom: | ||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from dataclasses import dataclass, field | ||
|
||
import sqlglot | ||
|
||
|
||
@dataclass(kw_only=True) | ||
class SqlViewGenerator: | ||
"""A class to generate SQL view statements given a select | ||
statement. | ||
Generated SQL is compatible with mysql, pgsql and sqlite3. | ||
The given `select_statment` is not validated. | ||
For use with view definitions. | ||
""" | ||
|
||
report_model: str = None | ||
ordering: list[str] | None = field(default_factory=list) | ||
order_by: str | None = field(init=False, default=None) | ||
with_stmt: str | None = field(default="") | ||
footer: str = field(init=False, default=None) | ||
template: str | None = field( | ||
default="{with_stmt} {select_stmt} from ({subquery}) as A ORDER BY {order_by};" | ||
) | ||
|
||
def __post_init__(self): | ||
ordering = [f"{fld[1:]} desc" if fld.startswith("~") else fld for fld in self.ordering] | ||
self.order_by = ", ".join(ordering) or "subject_identifier" | ||
self.footer = f") as A ORDER BY {self.order_by}" | ||
|
||
@staticmethod | ||
def transpile(sql: str, read: str | None = None, write: str = None) -> str: | ||
read = read or "mysql" | ||
sql = sql.replace(";", "") | ||
return sqlglot.transpile(sql, read=read, write=write)[0] | ||
|
||
def as_mysql(self, subquery: str, read: str | None = None) -> str: | ||
select_stmt = f"select *, uuid() as id, now() as `created`, '{self.report_model}' as `report_model`" # noqa | ||
subquery = self.transpile(subquery, read=read, write="mysql") | ||
return self.template.format( | ||
with_stmt=self.with_stmt, | ||
select_stmt=select_stmt, | ||
subquery=subquery, | ||
order_by=self.order_by, | ||
) | ||
|
||
def as_postgres(self, subquery: str, read: str | None = None) -> str: | ||
select_stmt = f"select *, get_random_uuid() as id, now() as created, '{self.report_model}' as report_model" # noqa | ||
subquery = self.transpile(subquery, read=read, write="postgres") | ||
return self.template.format( | ||
with_stmt=self.with_stmt, | ||
select_stmt=select_stmt, | ||
subquery=subquery, | ||
order_by=self.order_by, | ||
) | ||
|
||
def as_sqlite(self, subquery: str, read: str | None = None) -> str: | ||
"""For UUID support in sqlite, install sqlite/uuid. | ||
See https://github.com/nalgeon/sqlpkg-cli?tab=readme-ov-file | ||
and https://sqlpkg.org/?q=uuid | ||
$ curl -sS https://webi.sh/sqlpkg | sh | ||
$ sqlpkg install sqlite/uuid | ||
""" | ||
select_stmt = f"select *, uuid() as id, datetime() as created, '{self.report_model}' as report_model" # noqa | ||
subquery = self.transpile(subquery, read=read, write="sqlite") | ||
return self.template.format( | ||
with_stmt=self.with_stmt, | ||
select_stmt=select_stmt, | ||
subquery=subquery, | ||
order_by=self.order_by, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters