From 04597b7a697a275a2e1b906275f0d34ec235e534 Mon Sep 17 00:00:00 2001 From: erikvw Date: Mon, 10 Jul 2023 11:14:09 -0500 Subject: [PATCH] refactor ReferenceGetter to require site attr, ModelAdmin: link to subject dashboard,search on field values --- CHANGES | 6 + edc_reference/admin.py | 16 ++- ...alter_reference_managers_reference_site.py | 34 ++++++ edc_reference/models/reference.py | 7 +- edc_reference/reference/reference_getter.py | 105 ++++++++++-------- edc_reference/tests/tests/test_reference.py | 8 +- reference_app/models.py | 4 +- 7 files changed, 127 insertions(+), 53 deletions(-) create mode 100644 edc_reference/migrations/0012_alter_reference_managers_reference_site.py diff --git a/CHANGES b/CHANGES index e69de29..3e548f4 100644 --- a/CHANGES +++ b/CHANGES @@ -0,0 +1,6 @@ +0.3.16 +------ +- refactor ReferenceGetter to require `site` +- link ModelAdmin class to subject dashboard +- allow search of field values in ModelAdmin + diff --git a/edc_reference/admin.py b/edc_reference/admin.py index 8325923..2180caf 100644 --- a/edc_reference/admin.py +++ b/edc_reference/admin.py @@ -1,24 +1,34 @@ from django.contrib import admin -from edc_model_admin.mixins import TemplatesModelAdminMixin +from edc_model_admin.dashboard import ModelAdminSubjectDashboardMixin +from edc_sites.admin import SiteModelAdminMixin from .admin_site import edc_reference_admin from .models import Reference @admin.register(Reference, site=edc_reference_admin) -class ReferenceAdmin(TemplatesModelAdminMixin, admin.ModelAdmin): +class ReferenceAdmin(SiteModelAdminMixin, ModelAdminSubjectDashboardMixin, admin.ModelAdmin): date_hierarchy = "report_datetime" list_display = ( "identifier", + "dashboard", "model", "report_datetime", "visit", "timepoint", "field_name", + "value", ) list_filter = ("model", "timepoint", "field_name") - search_fields = ("identifier",) + search_fields = ( + "identifier", + "value_str", + "value_int", + "value_date", + "value_datetime", + "value_uuid", + ) def visit(self, obj=None): return f"{obj.visit_code}.{obj.visit_code_sequence}" diff --git a/edc_reference/migrations/0012_alter_reference_managers_reference_site.py b/edc_reference/migrations/0012_alter_reference_managers_reference_site.py new file mode 100644 index 0000000..5d2ca9e --- /dev/null +++ b/edc_reference/migrations/0012_alter_reference_managers_reference_site.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.1 on 2023-07-08 16:25 + +from django.db import migrations, models +import django.db.models.deletion +import django.db.models.manager +import edc_sites.model_mixins + + +class Migration(migrations.Migration): + dependencies = [ + ("sites", "0002_alter_domain_unique"), + ("edc_reference", "0011_auto_20200929_0238"), + ] + + operations = [ + migrations.AlterModelManagers( + name="reference", + managers=[ + ("objects", django.db.models.manager.Manager()), + ("on_site", edc_sites.model_mixins.CurrentSiteManager()), + ], + ), + migrations.AddField( + model_name="reference", + name="site", + field=models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="sites.site", + ), + ), + ] diff --git a/edc_reference/models/reference.py b/edc_reference/models/reference.py index 01b79f8..1af3216 100644 --- a/edc_reference/models/reference.py +++ b/edc_reference/models/reference.py @@ -1,6 +1,7 @@ from django.core.exceptions import FieldError from django.db import models from edc_model.models import BaseUuidModel +from edc_sites.model_mixins import SiteModelMixin from .managers import ReferenceManager @@ -9,7 +10,7 @@ class ReferenceFieldDatatypeNotFound(Exception): pass -class Reference(BaseUuidModel): +class Reference(SiteModelMixin, BaseUuidModel): identifier = models.CharField(max_length=50) visit_schedule_name = models.CharField(max_length=150, null=True) @@ -76,6 +77,10 @@ def natural_key(self): self.field_name, ) + @property + def subject_identifier(self): + return self.identifier + def update_value(self, value=None, internal_type=None, field=None, related_name=None): """Updates the correct `value` field based on the field class datatype. diff --git a/edc_reference/reference/reference_getter.py b/edc_reference/reference/reference_getter.py index c384f5e..a3513ab 100644 --- a/edc_reference/reference/reference_getter.py +++ b/edc_reference/reference/reference_getter.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +from datetime import datetime +from decimal import Decimal + from django.apps import apps as django_apps from django.core.exceptions import ObjectDoesNotExist @@ -21,17 +26,18 @@ class ReferenceGetter: def __init__( self, - name=None, - field_name=None, + name: str | None = None, + field_name: str | None = None, model_obj=None, - visit_obj=None, - subject_identifier=None, - report_datetime=None, - visit_schedule_name=None, - schedule_name=None, - visit_code=None, - visit_code_sequence=None, - timepoint=None, + related_visit=None, + subject_identifier: str | None = None, + report_datetime: datetime | None = None, + visit_schedule_name: str | None = None, + schedule_name: str | None = None, + visit_code: str | None = None, + visit_code_sequence: str | None = None, + timepoint: Decimal | None = None, + site=None, create=None, ): self._object = None @@ -44,44 +50,49 @@ def __init__( if model_obj: try: # given a crf model as model_obj + self.name = model_obj.reference_name self.report_datetime = model_obj.related_visit.report_datetime - self.subject_identifier = model_obj.related_visit.subject_identifier - self.visit_schedule_name = model_obj.related_visit.visit_schedule_name self.schedule_name = model_obj.related_visit.schedule_name + self.site = model_obj.related_visit.site + self.subject_identifier = model_obj.related_visit.subject_identifier + self.timepoint = model_obj.related_visit.timepoint self.visit_code = model_obj.related_visit.visit_code self.visit_code_sequence = model_obj.related_visit.visit_code_sequence - self.timepoint = model_obj.related_visit.timepoint - self.name = model_obj.reference_name - except AttributeError: + self.visit_schedule_name = model_obj.related_visit.visit_schedule_name + except AttributeError as e: + if "related_visit" not in str(e): + raise # given a visit model as model_obj - self.subject_identifier = model_obj.subject_identifier + self.name = model_obj.reference_name self.report_datetime = model_obj.report_datetime - self.visit_schedule_name = model_obj.visit_schedule_name self.schedule_name = model_obj.schedule_name + self.site = model_obj.site + self.subject_identifier = model_obj.subject_identifier + self.timepoint = model_obj.timepoint self.visit_code = model_obj.visit_code self.visit_code_sequence = model_obj.visit_code_sequence - self.timepoint = model_obj.timepoint - self.name = model_obj.reference_name - elif visit_obj: + self.visit_schedule_name = model_obj.visit_schedule_name + elif related_visit: self.name = name - self.subject_identifier = visit_obj.subject_identifier - self.report_datetime = visit_obj.report_datetime - self.visit_schedule_name = visit_obj.visit_schedule_name - self.schedule_name = visit_obj.schedule_name - self.visit_code = visit_obj.visit_code - self.visit_code_sequence = visit_obj.visit_code_sequence - self.timepoint = visit_obj.timepoint + self.report_datetime = related_visit.report_datetime + self.schedule_name = related_visit.schedule_name + self.site = related_visit.site + self.subject_identifier = related_visit.subject_identifier + self.timepoint = related_visit.timepoint + self.visit_code = related_visit.visit_code + self.visit_code_sequence = related_visit.visit_code_sequence + self.visit_schedule_name = related_visit.visit_schedule_name else: # given only the attrs self.name = name - self.subject_identifier = subject_identifier self.report_datetime = report_datetime - self.visit_schedule_name = visit_schedule_name self.schedule_name = schedule_name + self.site = site + self.subject_identifier = subject_identifier + self.timepoint = timepoint self.visit_code = visit_code self.visit_code_sequence = visit_code_sequence - self.timepoint = timepoint - + self.visit_schedule_name = visit_schedule_name reference_model = site_reference_configs.get_reference_model(name=self.name) self.reference_model_cls = django_apps.get_model(reference_model) @@ -102,10 +113,13 @@ def object(self): """Returns a reference model instance.""" if not self._object: self.created = False - opts = dict( - **self.required_options, - **{k: v for k, v in self.visit_options.items() if v is not None}, - ) + opts = self.required_options + opts.update(**{k: v for k, v in self.visit_options.items() if v is not None}) + if {k: v for k, v in opts.items() if v is None}: + raise ReferenceGetterError( + "Unable to get a reference instance. Null values for attrs " + f"not allowed. {self}. Got {opts}." + ) try: self._object = self.reference_model_cls.objects.get(**opts) except ObjectDoesNotExist as e: @@ -121,23 +135,25 @@ def required_options(self): """Returns a dictionary of query options required for both get and create. """ - return dict( + opts = dict( identifier=self.subject_identifier, model=self.name, report_datetime=self.report_datetime, field_name=self.field_name, + site=self.site, ) + return opts @property def visit_options(self): """Returns a dictionary of query options of the visit attrs.""" - opts = {} - opts.update( + opts = dict( visit_schedule_name=self.visit_schedule_name, schedule_name=self.schedule_name, visit_code=self.visit_code, visit_code_sequence=self.visit_code_sequence, timepoint=self.timepoint, + site=self.site, ) return opts @@ -146,12 +162,11 @@ def create_reference_obj(self): Note: updater needs to "update_value". """ - if {k: v for k, v in self.visit_options.items() if v is None}: + opts = self.required_options + opts.update(**{k: v for k, v in self.visit_options.items() if v is not None}) + if {k: v for k, v in opts.items() if v is None}: raise ReferenceGetterError( - f"Unable to create a reference instance. " - f"Null values for visit attrs not allowed. " - f"Got {self.visit_options}." + "Unable to create a reference instance. Null values for attrs " + f"not allowed. {self}. Got {opts}." ) - return self.reference_model_cls.objects.create( - **self.required_options, **self.visit_options - ) + return self.reference_model_cls.objects.create(**opts) diff --git a/edc_reference/tests/tests/test_reference.py b/edc_reference/tests/tests/test_reference.py index 70a7491..9b924d5 100644 --- a/edc_reference/tests/tests/test_reference.py +++ b/edc_reference/tests/tests/test_reference.py @@ -43,6 +43,7 @@ def setUp(self): self.panel_vl = Panel.objects.create(name="vl") self.panel_wb = Panel.objects.create(name="wb") self.subject_identifier = "123456789" + self.site = Site.objects.get_current() dte = get_utcnow() @@ -422,7 +423,7 @@ def test_reference_getter_without_crf_type_model(self): reference = ReferenceGetter( name="reference_app.crfone", field_name="field_int", - visit_obj=crf_one.related_visit, + related_visit=crf_one.related_visit, ) self.assertEqual(reference.field_int, integer) @@ -443,6 +444,7 @@ def test_reference_getter_without_using_model_obj(self): visit_code=crf_one.related_visit.visit_code, visit_code_sequence=crf_one.related_visit.visit_code_sequence, timepoint=crf_one.related_visit.timepoint, + site=crf_one.site, ) reference = ReferenceGetter(**opts) self.assertEqual(reference.field_int, integer) @@ -463,6 +465,7 @@ def test_reference_getter_without_using_model_obj_and_missing_visit_attr(self): schedule_name=crf_one.related_visit.schedule_name, visit_code=crf_one.related_visit.visit_code, timepoint=crf_one.related_visit.timepoint, + site=crf_one.site, ) reference = ReferenceGetter(**opts) self.assertEqual(reference.field_int, integer) @@ -476,6 +479,7 @@ def test_reference_getter_raises(self): field_name="field_int", subject_identifier=self.subject_identifier, report_datetime=self.subject_visit.report_datetime, + site=self.site, ) self.assertRaises(ReferenceObjectDoesNotExist, ReferenceGetter, **opts) @@ -517,7 +521,7 @@ def test_reference_getter_object_doesnotexist(self): ReferenceGetter, field_name="blah", model_obj=crf_one_obj, - visit_obj=self.subject_visit, + related_visit=self.subject_visit, create=False, ) diff --git a/reference_app/models.py b/reference_app/models.py index bd94877..6cf8fb6 100644 --- a/reference_app/models.py +++ b/reference_app/models.py @@ -59,10 +59,10 @@ class SubjectConsent( SiteModelMixin, BaseUuidModel, ): - on_site = CurrentSiteManager() - objects = SubjectIdentifierManager() + on_site = CurrentSiteManager() + def natural_key(self): return (self.subject_identifier,)