diff --git a/edc_visit_schedule/baseline.py b/edc_visit_schedule/baseline.py index 49ee363..898dc72 100644 --- a/edc_visit_schedule/baseline.py +++ b/edc_visit_schedule/baseline.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING, Any -from .site_visit_schedules import SiteVisitScheduleError, site_visit_schedules +from .exceptions import SiteVisitScheduleError, VisitScheduleBaselineError +from .site_visit_schedules import site_visit_schedules if TYPE_CHECKING: from decimal import Decimal @@ -11,8 +12,7 @@ from .visit_schedule import VisitSchedule -class VisitScheduleBaselineError(Exception): - pass +__all__ = ["Baseline"] class Baseline: diff --git a/edc_visit_schedule/exceptions.py b/edc_visit_schedule/exceptions.py index e0a5ac2..4126db8 100644 --- a/edc_visit_schedule/exceptions.py +++ b/edc_visit_schedule/exceptions.py @@ -48,3 +48,19 @@ class ScheduledVisitWindowError(Exception): class UnScheduledVisitWindowError(Exception): pass + + +class SiteVisitScheduleError(Exception): + pass + + +class RegistryNotLoaded(Exception): + pass + + +class AlreadyRegisteredVisitSchedule(Exception): + pass + + +class VisitScheduleBaselineError(Exception): + pass diff --git a/edc_visit_schedule/migrations/0015_historicalonschedule_offschedule_onschedule.py b/edc_visit_schedule/migrations/0015_historicalonschedule_offschedule_onschedule.py new file mode 100644 index 0000000..a7a3262 --- /dev/null +++ b/edc_visit_schedule/migrations/0015_historicalonschedule_offschedule_onschedule.py @@ -0,0 +1,496 @@ +# Generated by Django 5.0 on 2024-01-09 23:21 + +import _socket +import django.db.models.deletion +import django_audit_fields.fields.hostname_modification_field +import django_audit_fields.fields.userfield +import django_audit_fields.fields.uuid_auto_field +import django_audit_fields.models.audit_model_mixin +import django_revision.revision_field +import edc_identifier.managers +import edc_model.validators.date +import edc_protocol.validators +import edc_sites.managers +import edc_utils.date +import simple_history.models +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("edc_visit_schedule", "0014_alter_subjectschedulehistory_options_and_more"), + ("sites", "0002_alter_domain_unique"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="HistoricalOnSchedule", + fields=[ + ( + "revision", + django_revision.revision_field.RevisionField( + blank=True, + editable=False, + help_text="System field. Git repository tag:branch:commit.", + max_length=75, + null=True, + verbose_name="Revision", + ), + ), + ( + "created", + models.DateTimeField( + blank=True, + default=django_audit_fields.models.audit_model_mixin.utcnow, + ), + ), + ( + "modified", + models.DateTimeField( + blank=True, + default=django_audit_fields.models.audit_model_mixin.utcnow, + ), + ), + ( + "user_created", + django_audit_fields.fields.userfield.UserField( + blank=True, + help_text="Updated by admin.save_model", + max_length=50, + verbose_name="user created", + ), + ), + ( + "user_modified", + django_audit_fields.fields.userfield.UserField( + blank=True, + help_text="Updated by admin.save_model", + max_length=50, + verbose_name="user modified", + ), + ), + ( + "hostname_created", + models.CharField( + blank=True, + default=_socket.gethostname, + help_text="System field. (modified on create only)", + max_length=60, + verbose_name="Hostname created", + ), + ), + ( + "hostname_modified", + django_audit_fields.fields.hostname_modification_field.HostnameModificationField( + blank=True, + help_text="System field. (modified on every save)", + max_length=50, + verbose_name="Hostname modified", + ), + ), + ( + "device_created", + models.CharField(blank=True, max_length=10, verbose_name="Device created"), + ), + ( + "device_modified", + models.CharField( + blank=True, max_length=10, verbose_name="Device modified" + ), + ), + ( + "locale_created", + models.CharField( + blank=True, + help_text="Auto-updated by Modeladmin", + max_length=10, + null=True, + verbose_name="Locale created", + ), + ), + ( + "locale_modified", + models.CharField( + blank=True, + help_text="Auto-updated by Modeladmin", + max_length=10, + null=True, + verbose_name="Locale modified", + ), + ), + ( + "id", + django_audit_fields.fields.uuid_auto_field.UUIDAutoField( + blank=True, + db_index=True, + editable=False, + help_text="System auto field. UUID primary key.", + ), + ), + ("subject_identifier", models.CharField(db_index=True, max_length=50)), + ( + "onschedule_datetime", + models.DateTimeField( + default=edc_utils.date.get_utcnow, + validators=[ + edc_protocol.validators.datetime_not_before_study_start, + edc_model.validators.date.datetime_not_future, + ], + ), + ), + ("report_datetime", models.DateTimeField(editable=False)), + ( + "history_id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "site", + models.ForeignKey( + blank=True, + db_constraint=False, + editable=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="sites.site", + ), + ), + ], + options={ + "verbose_name": "historical on schedule", + "verbose_name_plural": "historical on schedules", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="OffSchedule", + fields=[ + ( + "revision", + django_revision.revision_field.RevisionField( + blank=True, + editable=False, + help_text="System field. Git repository tag:branch:commit.", + max_length=75, + null=True, + verbose_name="Revision", + ), + ), + ( + "created", + models.DateTimeField( + blank=True, + default=django_audit_fields.models.audit_model_mixin.utcnow, + ), + ), + ( + "modified", + models.DateTimeField( + blank=True, + default=django_audit_fields.models.audit_model_mixin.utcnow, + ), + ), + ( + "user_created", + django_audit_fields.fields.userfield.UserField( + blank=True, + help_text="Updated by admin.save_model", + max_length=50, + verbose_name="user created", + ), + ), + ( + "user_modified", + django_audit_fields.fields.userfield.UserField( + blank=True, + help_text="Updated by admin.save_model", + max_length=50, + verbose_name="user modified", + ), + ), + ( + "hostname_created", + models.CharField( + blank=True, + default=_socket.gethostname, + help_text="System field. (modified on create only)", + max_length=60, + verbose_name="Hostname created", + ), + ), + ( + "hostname_modified", + django_audit_fields.fields.hostname_modification_field.HostnameModificationField( + blank=True, + help_text="System field. (modified on every save)", + max_length=50, + verbose_name="Hostname modified", + ), + ), + ( + "device_created", + models.CharField(blank=True, max_length=10, verbose_name="Device created"), + ), + ( + "device_modified", + models.CharField( + blank=True, max_length=10, verbose_name="Device modified" + ), + ), + ( + "locale_created", + models.CharField( + blank=True, + help_text="Auto-updated by Modeladmin", + max_length=10, + null=True, + verbose_name="Locale created", + ), + ), + ( + "locale_modified", + models.CharField( + blank=True, + help_text="Auto-updated by Modeladmin", + max_length=10, + null=True, + verbose_name="Locale modified", + ), + ), + ( + "id", + django_audit_fields.fields.uuid_auto_field.UUIDAutoField( + blank=True, + editable=False, + help_text="System auto field. UUID primary key.", + primary_key=True, + serialize=False, + ), + ), + ("subject_identifier", models.CharField(max_length=50, unique=True)), + ( + "offschedule_datetime", + models.DateTimeField( + default=edc_utils.date.get_utcnow, + validators=[ + edc_protocol.validators.datetime_not_before_study_start, + edc_model.validators.date.datetime_not_future, + ], + verbose_name="Date and time subject taken off schedule", + ), + ), + ("report_datetime", models.DateTimeField(editable=False)), + ( + "site", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="sites.site", + ), + ), + ], + options={ + "abstract": False, + "default_permissions": ( + "add", + "change", + "delete", + "view", + "export", + "import", + ), + "default_manager_name": "objects", + "indexes": [ + models.Index( + fields=["subject_identifier", "offschedule_datetime", "site"], + name="edc_visit_s_subject_fca20d_idx", + ) + ], + }, + managers=[ + ("on_site", edc_sites.managers.CurrentSiteManager()), + ("objects", edc_identifier.managers.SubjectIdentifierManager()), + ], + ), + migrations.CreateModel( + name="OnSchedule", + fields=[ + ( + "revision", + django_revision.revision_field.RevisionField( + blank=True, + editable=False, + help_text="System field. Git repository tag:branch:commit.", + max_length=75, + null=True, + verbose_name="Revision", + ), + ), + ( + "created", + models.DateTimeField( + blank=True, + default=django_audit_fields.models.audit_model_mixin.utcnow, + ), + ), + ( + "modified", + models.DateTimeField( + blank=True, + default=django_audit_fields.models.audit_model_mixin.utcnow, + ), + ), + ( + "user_created", + django_audit_fields.fields.userfield.UserField( + blank=True, + help_text="Updated by admin.save_model", + max_length=50, + verbose_name="user created", + ), + ), + ( + "user_modified", + django_audit_fields.fields.userfield.UserField( + blank=True, + help_text="Updated by admin.save_model", + max_length=50, + verbose_name="user modified", + ), + ), + ( + "hostname_created", + models.CharField( + blank=True, + default=_socket.gethostname, + help_text="System field. (modified on create only)", + max_length=60, + verbose_name="Hostname created", + ), + ), + ( + "hostname_modified", + django_audit_fields.fields.hostname_modification_field.HostnameModificationField( + blank=True, + help_text="System field. (modified on every save)", + max_length=50, + verbose_name="Hostname modified", + ), + ), + ( + "device_created", + models.CharField(blank=True, max_length=10, verbose_name="Device created"), + ), + ( + "device_modified", + models.CharField( + blank=True, max_length=10, verbose_name="Device modified" + ), + ), + ( + "locale_created", + models.CharField( + blank=True, + help_text="Auto-updated by Modeladmin", + max_length=10, + null=True, + verbose_name="Locale created", + ), + ), + ( + "locale_modified", + models.CharField( + blank=True, + help_text="Auto-updated by Modeladmin", + max_length=10, + null=True, + verbose_name="Locale modified", + ), + ), + ( + "id", + django_audit_fields.fields.uuid_auto_field.UUIDAutoField( + blank=True, + editable=False, + help_text="System auto field. UUID primary key.", + primary_key=True, + serialize=False, + ), + ), + ("subject_identifier", models.CharField(max_length=50, unique=True)), + ( + "onschedule_datetime", + models.DateTimeField( + default=edc_utils.date.get_utcnow, + validators=[ + edc_protocol.validators.datetime_not_before_study_start, + edc_model.validators.date.datetime_not_future, + ], + ), + ), + ("report_datetime", models.DateTimeField(editable=False)), + ( + "site", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="sites.site", + ), + ), + ], + options={ + "abstract": False, + "default_permissions": ( + "add", + "change", + "delete", + "view", + "export", + "import", + ), + "default_manager_name": "objects", + "indexes": [ + models.Index( + fields=["subject_identifier", "onschedule_datetime", "site"], + name="edc_visit_s_subject_899e3b_idx", + ) + ], + }, + managers=[ + ("on_site", edc_sites.managers.CurrentSiteManager()), + ("objects", edc_identifier.managers.SubjectIdentifierManager()), + ], + ), + ] diff --git a/edc_visit_schedule/model_mixins/crf/crf_schedule_model_mixin.py b/edc_visit_schedule/model_mixins/crf/crf_schedule_model_mixin.py index 4a0f97f..a07d668 100644 --- a/edc_visit_schedule/model_mixins/crf/crf_schedule_model_mixin.py +++ b/edc_visit_schedule/model_mixins/crf/crf_schedule_model_mixin.py @@ -46,10 +46,11 @@ def schedule(self) -> Schedule: def is_onschedule_or_raise(self) -> None: subject_schedule = SubjectSchedule( - visit_schedule=self.visit_schedule, schedule=self.schedule + self.subject_identifier, + visit_schedule=self.visit_schedule, + schedule=self.schedule, ) subject_schedule.onschedule_or_raise( - subject_identifier=self.subject_identifier, report_datetime=self.report_datetime, compare_as_datetimes=self.offschedule_compare_dates_as_datetimes, ) diff --git a/edc_visit_schedule/modelform_mixins/crf/visit_schedule_crf_modelform_mixin.py b/edc_visit_schedule/modelform_mixins/crf/visit_schedule_crf_modelform_mixin.py index 69b90c8..e6d26ea 100644 --- a/edc_visit_schedule/modelform_mixins/crf/visit_schedule_crf_modelform_mixin.py +++ b/edc_visit_schedule/modelform_mixins/crf/visit_schedule_crf_modelform_mixin.py @@ -45,10 +45,11 @@ def is_onschedule_or_raise(self) -> None: if self.report_datetime and self.related_visit: visit_schedule = self.visit_schedule schedule = self.schedule - subject_schedule = SubjectSchedule(visit_schedule, schedule) + subject_schedule = SubjectSchedule( + self.get_subject_identifier(), visit_schedule=visit_schedule, schedule=schedule + ) try: subject_schedule.onschedule_or_raise( - subject_identifier=self.get_subject_identifier(), report_datetime=self.report_datetime, compare_as_datetimes=( self._meta.model.offschedule_compare_dates_as_datetimes diff --git a/edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py b/edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py index 10e54fa..0954605 100644 --- a/edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py +++ b/edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py @@ -20,9 +20,8 @@ def clean(self): visit_schedule_name=self.visit_schedule_name, ) try: - self.schedule.subject.update_history_or_raise( + self.schedule.subject(self.get_subject_identifier()).update_history_or_raise( history_obj=history_obj, - subject_identifier=self.get_subject_identifier(), offschedule_datetime=self.offschedule_datetime, update=False, ) diff --git a/edc_visit_schedule/modelform_mixins/visit_schedule_non_crf_modelform_mixin.py b/edc_visit_schedule/modelform_mixins/visit_schedule_non_crf_modelform_mixin.py index 03f9e67..0611dc8 100644 --- a/edc_visit_schedule/modelform_mixins/visit_schedule_non_crf_modelform_mixin.py +++ b/edc_visit_schedule/modelform_mixins/visit_schedule_non_crf_modelform_mixin.py @@ -65,10 +65,11 @@ def schedule(self) -> Schedule: return schedule def is_onschedule_or_raise(self) -> None: - subject_schedule = SubjectSchedule(self.visit_schedule, self.schedule) + subject_schedule = SubjectSchedule( + self.get_subject_identifier(), self.visit_schedule, self.schedule + ) try: subject_schedule.onschedule_or_raise( - subject_identifier=self.get_subject_identifier(), report_datetime=self.report_datetime, compare_as_datetimes=self.offschedule_compare_dates_as_datetimes, ) diff --git a/edc_visit_schedule/models/signals.py b/edc_visit_schedule/models/signals.py index c989d17..d168410 100644 --- a/edc_visit_schedule/models/signals.py +++ b/edc_visit_schedule/models/signals.py @@ -14,8 +14,7 @@ def offschedule_model_on_post_save(sender, instance, raw, update_fields, **kwarg instance._meta.label_lower ) schedule.take_off_schedule( - subject_identifier=instance.subject_identifier, - offschedule_datetime=instance.offschedule_datetime, + instance.subject_identifier, instance.offschedule_datetime ) diff --git a/edc_visit_schedule/schedule/schedule.py b/edc_visit_schedule/schedule/schedule.py index 13beee7..df7fed7 100644 --- a/edc_visit_schedule/schedule/schedule.py +++ b/edc_visit_schedule/schedule/schedule.py @@ -1,11 +1,19 @@ from __future__ import annotations import re +from datetime import datetime from decimal import Decimal -from typing import Type +from typing import TYPE_CHECKING, Type from django.apps import apps as django_apps from django.core.management.color import color_style +from edc_consent.consent_definition import ConsentDefinition +from edc_consent.exceptions import ( + ConsentDefinitionDoesNotExist, + ConsentDefinitionValidityPeriodError, +) +from edc_sites.single_site import SingleSite +from edc_utils import formatted_date from ..site_visit_schedules import SiteVisitScheduleError, site_visit_schedules from ..subject_schedule import ( @@ -18,6 +26,18 @@ from .visit_collection import VisitCollection from .window import Window +if TYPE_CHECKING: + from edc_appointment.models import Appointment + from edc_model.models import BaseUuidModel + from edc_sites.model_mixins import SiteModelMixin + from edc_visit_tracking.model_mixins import VisitModelMixin as Base + + from ..models import OffSchedule, OnSchedule, SubjectScheduleHistory + + class RelatedVisitModel(SiteModelMixin, Base, BaseUuidModel): + pass + + style = color_style() @@ -54,19 +74,14 @@ def __init__( onschedule_model: str = None, offschedule_model: str = None, loss_to_followup_model: str = None, - consent_model: str = None, appointment_model: str | None = None, + history_model: str | None = None, + consent_definitions: list[ConsentDefinition] | ConsentDefinition = None, offstudymedication_model: str | None = None, sequence: str | None = None, base_timepoint: float | Decimal | None = None, ): self._subject = None - if isinstance(base_timepoint, (float,)): - base_timepoint = Decimal(str(base_timepoint)) - elif isinstance(base_timepoint, (int,)): - base_timepoint = Decimal(str(base_timepoint) + ".0") - self.base_timepoint = base_timepoint or Decimal("0.0") - self.visits = self.visit_collection_cls() if not name or not re.match(r"[a-z0-9_\-]+$", name): raise ScheduleNameError( f"Invalid name. Got '{name}'. May only contains numbers, " @@ -74,19 +89,28 @@ def __init__( ) else: self.name = name + self.consent_definitions = consent_definitions + if isinstance(consent_definitions, (ConsentDefinition,)): + self.consent_definitions = [consent_definitions] + self.consent_definitions = sorted(self.consent_definitions) + if isinstance(base_timepoint, (float,)): + base_timepoint = Decimal(str(base_timepoint)) + elif isinstance(base_timepoint, (int,)): + base_timepoint = Decimal(str(base_timepoint) + ".0") + self.base_timepoint = base_timepoint or Decimal("0.0") + self.visits = self.visit_collection_cls() self.verbose_name = verbose_name or name self.sequence = sequence or name - - self.appointment_model = appointment_model.lower() or "edc_appointment.appointment" - self.consent_model = consent_model.lower() - self.offschedule_model = offschedule_model.lower() - self.onschedule_model = onschedule_model.lower() + self.appointment_model: str = appointment_model or "edc_appointment.appointment" + self.offschedule_model: str = offschedule_model.lower() + self.onschedule_model: str = onschedule_model.lower() self.loss_to_followup_model = ( None if loss_to_followup_model is None else loss_to_followup_model.lower() ) self.offstudymedication_model = ( None if offstudymedication_model is None else offstudymedication_model.lower() ) + self.history_model = history_model or "edc_visit_schedule.subjectschedulehistory" def check(self): warnings = [] @@ -156,8 +180,7 @@ def requisition_required_at(self, requisition_panel) -> list: visit_codes.append(visit_code) return visit_codes - @property - def subject(self): + def subject(self, subject_identifier: str) -> SubjectSchedule: """Returns a SubjectSchedule instance. Note: SubjectSchedule puts a subject on to a schedule or takes a subject @@ -173,25 +196,29 @@ def subject(self): f"Expected {repr(self)} for onschedule_model={self.onschedule_model}. " f"Got {repr(schedule)}." ) - self._subject = SubjectSchedule(visit_schedule=visit_schedule, schedule=self) + self._subject = SubjectSchedule( + subject_identifier, visit_schedule=visit_schedule, schedule=self + ) return self._subject - def put_on_schedule(self, **kwargs): + def put_on_schedule(self, subject_identifier: str, onschedule_datetime: datetime | None): """Wrapper method to puts a subject onto this schedule.""" - self.subject.put_on_schedule(**kwargs) + self.subject(subject_identifier).put_on_schedule(onschedule_datetime) - def refresh_schedule(self, **kwargs): + def refresh_schedule(self, subject_identifier: str): """Resaves the onschedule model to, for example, refresh appointments. """ - self.subject.resave(**kwargs) + self.subject(subject_identifier).resave() - def take_off_schedule(self, offschedule_datetime=None, **kwargs): - self.subject.take_off_schedule(offschedule_datetime=offschedule_datetime, **kwargs) + def take_off_schedule(self, subject_identifier: str, offschedule_datetime: datetime): + self.subject(subject_identifier).take_off_schedule(offschedule_datetime) - def is_onschedule(self, **kwargs): + def is_onschedule(self, subject_identifier: str, report_datetime: datetime): try: - self.subject.onschedule_or_raise(compare_as_datetimes=True, **kwargs) + self.subject(subject_identifier).onschedule_or_raise( + report_datetime=report_datetime, compare_as_datetimes=True + ) except (NotOnScheduleError, NotOnScheduleForDateError): return False return True @@ -200,12 +227,12 @@ def datetime_in_window(self, **kwargs): return self.window_cls(name=self.name, visits=self.visits, **kwargs).datetime_in_window @property - def onschedule_model_cls(self): - return self.subject.onschedule_model_cls + def onschedule_model_cls(self) -> Type[OnSchedule]: + return django_apps.get_model(self.onschedule_model) @property - def offschedule_model_cls(self): - return self.subject.offschedule_model_cls + def offschedule_model_cls(self) -> Type[OffSchedule]: + return django_apps.get_model(self.offschedule_model) @property def loss_to_followup_model_cls(self): @@ -216,20 +243,53 @@ def ltfu_model_cls(self): return self.loss_to_followup_model_cls @property - def history_model_cls(self): - return self.subject.history_model_cls + def history_model_cls(self) -> Type[SubjectScheduleHistory]: + return django_apps.get_model(self.history_model) @property - def appointment_model_cls(self): - return self.subject.appointment_model_cls + def appointment_model_cls(self) -> Type[Appointment]: + return django_apps.get_model(self.appointment_model) @property - def visit_model_cls(self): - return self.subject.visit_model_cls + def visit_model_cls(self) -> Type[RelatedVisitModel]: + return self.appointment_model_cls.related_visit_model_cls() + + def get_consent_definition( + self, report_datetime: datetime = None, site: SingleSite = None + ) -> ConsentDefinition: + """Returns the ConsentDefinition valid for the given report + date or raises an exception. + """ + consent_definition = None + cdefs = [cdef for cdef in self.consent_definitions if site in cdef.sites] + if not cdefs: + cdefs_as_string = ", ".join( + [cdef.display_name for cdef in self.consent_definitions] + ) + raise ConsentDefinitionDoesNotExist( + "This site does not match any consent definitions for this schedule. " + f"Consent definitions are: {cdefs_as_string}. Got {site.name}." + ) - @property - def consent_model_cls(self): - return self.subject.consent_model_cls + for cdef in cdefs: + try: + cdef.valid_for_datetime_or_raise(report_datetime) + except ConsentDefinitionValidityPeriodError: + pass + else: + consent_definition = cdef + break + if not consent_definition: + date_string = formatted_date(report_datetime) + cdefs_as_string = ", ".join( + [cdef.display_name for cdef in self.consent_definitions] + ) + raise ConsentDefinitionDoesNotExist( + "Date does not fall within the validity period of any consent definition " + f"for this schedule. Consent definitions are: " + f"{cdefs_as_string}. Got {date_string}." + ) + return consent_definition def to_dict(self): return {k: v.to_dict() for k, v in self.visits.items()} diff --git a/edc_visit_schedule/site_visit_schedules.py b/edc_visit_schedule/site_visit_schedules.py index 06daa95..dd31e4c 100644 --- a/edc_visit_schedule/site_visit_schedules.py +++ b/edc_visit_schedule/site_visit_schedules.py @@ -8,22 +8,21 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.module_loading import import_module, module_has_submodule +from .exceptions import ( + AlreadyRegisteredVisitSchedule, + RegistryNotLoaded, + SiteVisitScheduleError, +) + if TYPE_CHECKING: + from edc_sites.single_site import SingleSite + from .models import VisitSchedule as VisitScheduleModel from .schedule import Schedule from .visit_schedule import VisitSchedule -class RegistryNotLoaded(Exception): - pass - - -class AlreadyRegisteredVisitSchedule(Exception): - pass - - -class SiteVisitScheduleError(Exception): - pass +__all__ = ["site_visit_schedules"] class SiteVisitSchedules: @@ -95,7 +94,9 @@ def get_visit_schedules(self, *visit_schedule_names) -> dict[str, VisitSchedule] visit_schedules[visit_schedule_name] = self.get_visit_schedule(visit_schedule_name) return visit_schedules or self.registry - def get_by_onschedule_model(self, onschedule_model=None) -> Tuple[VisitSchedule, Schedule]: + def get_by_onschedule_model( + self, onschedule_model: str = None + ) -> Tuple[VisitSchedule, Schedule]: """Returns a tuple of (visit_schedule, schedule) for the given onschedule model. @@ -104,7 +105,7 @@ def get_by_onschedule_model(self, onschedule_model=None) -> Tuple[VisitSchedule, return self.get_by_model(attr="onschedule_model", model=onschedule_model) def get_by_offschedule_model( - self, offschedule_model=None + self, offschedule_model: str = None ) -> Tuple[VisitSchedule, Schedule]: """Returns a tuple of visit_schedule, schedule for the given offschedule model. @@ -114,7 +115,7 @@ def get_by_offschedule_model( return self.get_by_model(attr="offschedule_model", model=offschedule_model) def get_by_loss_to_followup_model( - self, loss_to_followup_model=None + self, loss_to_followup_model: str = None ) -> Tuple[VisitSchedule, Schedule]: """Returns a tuple of visit_schedule, schedule for the given loss_to_followup model. @@ -149,7 +150,7 @@ def get_by_model( visit_schedule, schedule = ret[0] return visit_schedule, schedule - def get_by_offstudy_model(self, offstudy_model=None) -> list[VisitSchedule]: + def get_by_offstudy_model(self, offstudy_model: str = None) -> list[VisitSchedule]: """Returns a list of visit_schedules for the given offstudy model. """ @@ -164,10 +165,24 @@ def get_by_offstudy_model(self, offstudy_model=None) -> list[VisitSchedule]: ) return visit_schedules - def get_consent_model(self, visit_schedule_name: str, schedule_name: str) -> str: - """Returns the consent model name""" + def get_consent_model( + self, + visit_schedule_name: str, + schedule_name: str, + site: SingleSite | None = None, + ) -> str: + """Returns the consent model name specified on the schedule""" schedule = self.get_visit_schedule(visit_schedule_name).schedules.get(schedule_name) - return schedule.consent_model + if isinstance(schedule.consent_model, (dict,)): + # schedule returns a dict, get model name for this + # site_id or country + consent_model = schedule.consent_model.get( + site.site_id + ) or schedule.consent_model.get(site.country) + else: + # schedule returns a string + consent_model = schedule.consent_model + return consent_model def get_onschedule_model(self, visit_schedule_name: str, schedule_name: str) -> str: """Returns the onschedule model name""" @@ -209,18 +224,6 @@ def all_post_consent_models(self) -> dict[str, str]: self._all_post_consent_models = models return self._all_post_consent_models - def check(self) -> dict[str, list]: - if not self.loaded: - raise SiteVisitScheduleError("Registry is not loaded.") - errors = {"visit_schedules": [], "schedules": [], "visits": []} - for visit_schedule in site_visit_schedules.visit_schedules.values(): - errors["visit_schedules"].extend(visit_schedule.check()) - for schedule in visit_schedule.schedules.values(): - errors["schedules"].extend(schedule.check()) - for visit in schedule.visits.values(): - errors["visits"].extend(visit.check()) - return errors - @staticmethod def to_model(model_cls: VisitScheduleModel) -> None: """Updates the VisitSchedule model with the current visit diff --git a/edc_visit_schedule/subject_schedule.py b/edc_visit_schedule/subject_schedule.py index f690eee..01efec7 100644 --- a/edc_visit_schedule/subject_schedule.py +++ b/edc_visit_schedule/subject_schedule.py @@ -1,14 +1,13 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Type from django.apps import apps as django_apps from django.conf import settings from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from edc_appointment.constants import COMPLETE_APPT, IN_PROGRESS_APPT from edc_appointment.creators import AppointmentsCreator -from edc_consent import NotConsentedError, site_consents from edc_sites.utils import valid_site_for_subject_or_raise from edc_utils import convert_php_dateformat, formatted_datetime, get_utcnow @@ -23,7 +22,18 @@ ) if TYPE_CHECKING: + from edc_appointment.models import Appointment + from edc_model.models import BaseUuidModel from edc_registration.models import RegisteredSubject + from edc_sites.model_mixins import SiteModelMixin + from edc_visit_tracking.model_mixins import VisitModelMixin as Base + + from .models import OffSchedule, OnSchedule, SubjectScheduleHistory + from .schedule import Schedule + from .visit_schedule import VisitSchedule + + class RelatedVisitModel(SiteModelMixin, Base, BaseUuidModel): + pass class SubjectSchedule: @@ -37,45 +47,44 @@ class SubjectSchedule: registered_subject_model = "edc_registration.registeredsubject" appointments_creator_cls = AppointmentsCreator - def __init__(self, visit_schedule=None, schedule=None): - self.visit_schedule = visit_schedule - self.schedule = schedule - self.schedule_name = schedule.name - self.visit_schedule_name = self.visit_schedule.name - self.onschedule_model = schedule.onschedule_model - self.consent_model = schedule.consent_model - self.offschedule_model = schedule.offschedule_model - self.appointment_model = schedule.appointment_model + def __init__( + self, + subject_identifier: str, + visit_schedule: VisitSchedule = None, + schedule: Schedule = None, + ): + self.subject_identifier: str = subject_identifier + self.visit_schedule: VisitSchedule = visit_schedule + self.schedule: Schedule = schedule + self.schedule_name: str = schedule.name + self.visit_schedule_name: str = self.visit_schedule.name + self.onschedule_model: str = schedule.onschedule_model + self.offschedule_model: str = schedule.offschedule_model + self.appointment_model: str = schedule.appointment_model @property - def onschedule_model_cls(self): + def onschedule_model_cls(self) -> Type[OnSchedule]: return django_apps.get_model(self.onschedule_model) @property - def offschedule_model_cls(self): + def offschedule_model_cls(self) -> Type[OffSchedule]: return django_apps.get_model(self.offschedule_model) @property - def history_model_cls(self): + def history_model_cls(self) -> Type[SubjectScheduleHistory]: return django_apps.get_model(self.history_model) @property - def appointment_model_cls(self): + def appointment_model_cls(self) -> Type[Appointment]: return django_apps.get_model(self.appointment_model) - @property - def visit_model_cls(self): - return self.appointment_model_cls.visit_model_cls() - - @property - def consent_model_cls(self): - cdef = site_consents.get_consent_definition(model=self.consent_model) - return cdef.model_cls + # @property + # def visit_model_cls(self) -> Type[RelatedVisitModel]: + # return self.appointment_model_cls.related_visit_model_cls() def put_on_schedule( self, - subject_identifier: str | None = None, - onschedule_datetime: datetime | None = None, + onschedule_datetime: datetime | None, first_appt_datetime: datetime | None = None, skip_baseline: bool | None = None, skip_get_current_site: bool | None = None, @@ -88,26 +97,31 @@ def put_on_schedule( """ onschedule_datetime = onschedule_datetime or get_utcnow() site = valid_site_for_subject_or_raise( - subject_identifier, skip_get_current_site=skip_get_current_site + self.subject_identifier, skip_get_current_site=skip_get_current_site ) if not self.onschedule_model_cls.objects.filter( - subject_identifier=subject_identifier + subject_identifier=self.subject_identifier ).exists(): - self.consented_or_raise(subject_identifier=subject_identifier) + # use the first cdef in list (version 1) to + # check for a consent + self.schedule.consent_definitions[0].get_consent_for( + subject_identifier=self.subject_identifier, report_datetime=onschedule_datetime + ) + # self.consented_or_raise(subject_identifier=subject_identifier) self.onschedule_model_cls.objects.create( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, onschedule_datetime=onschedule_datetime, site=site, ) try: history_obj = self.history_model_cls.objects.get( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule_name=self.schedule_name, visit_schedule_name=self.visit_schedule_name, ) except ObjectDoesNotExist: history_obj = self.history_model_cls.objects.create( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, onschedule_model=self.onschedule_model, offschedule_model=self.offschedule_model, schedule_name=self.schedule_name, @@ -120,7 +134,7 @@ def put_on_schedule( # create appointments per schedule creator = self.appointments_creator_cls( report_datetime=onschedule_datetime, - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule=self.schedule, visit_schedule=self.visit_schedule, appointment_model=self.appointment_model, @@ -133,7 +147,7 @@ def put_on_schedule( ) creator.create_appointments(first_appt_datetime or onschedule_datetime) - def take_off_schedule(self, subject_identifier=None, offschedule_datetime=None): + def take_off_schedule(self, offschedule_datetime: datetime): """Takes a subject off-schedule. A person is taken off-schedule by: @@ -143,12 +157,12 @@ def take_off_schedule(self, subject_identifier=None, offschedule_datetime=None): * deleting future appointments """ # create offschedule_model_obj if it does not exist - rs_obj = self.registered_or_raise(subject_identifier=subject_identifier) + rs_obj = self.registered_or_raise() if not self.offschedule_model_cls.objects.filter( - subject_identifier=subject_identifier + subject_identifier=self.subject_identifier ).exists(): self.offschedule_model_cls.objects.create( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, offschedule_datetime=offschedule_datetime, site=rs_obj.site, ) @@ -156,7 +170,7 @@ def take_off_schedule(self, subject_identifier=None, offschedule_datetime=None): # get existing history obj or raise try: history_obj = self.history_model_cls.objects.get( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule_name=self.schedule_name, visit_schedule_name=self.visit_schedule_name, ) @@ -165,21 +179,20 @@ def take_off_schedule(self, subject_identifier=None, offschedule_datetime=None): "Failed to take subject off schedule. " f"Subject has not been put on schedule " f"'{self.visit_schedule_name}.{self.schedule_name}'. " - f"Got '{subject_identifier}'." + f"Got '{self.subject_identifier}'." ) if history_obj: self.update_history_or_raise( history_obj=history_obj, - subject_identifier=subject_identifier, offschedule_datetime=offschedule_datetime, ) - self._update_in_progress_appointment(subject_identifier=subject_identifier) + self._update_in_progress_appointment() # clear future appointments self.appointment_model_cls.objects.delete_for_subject_after_date( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, cutoff_datetime=offschedule_datetime, visit_schedule_name=self.visit_schedule_name, schedule_name=self.schedule_name, @@ -188,7 +201,6 @@ def take_off_schedule(self, subject_identifier=None, offschedule_datetime=None): def update_history_or_raise( self, history_obj=None, - subject_identifier=None, offschedule_datetime=None, update=None, ): @@ -199,7 +211,7 @@ def update_history_or_raise( """ update = True if update is None else update if not self.history_model_cls.objects.filter( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule_name=self.schedule_name, visit_schedule_name=self.visit_schedule_name, onschedule_datetime__lte=offschedule_datetime, @@ -215,7 +227,7 @@ def update_history_or_raise( related_visit_model_attr = self.appointment_model_cls.related_visit_model_attr() try: appointments = self.appointment_model_cls.objects.get( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule_name=self.schedule_name, visit_schedule_name=self.visit_schedule_name, **{f"{related_visit_model_attr}__report_datetime__gt": offschedule_datetime}, @@ -224,7 +236,7 @@ def update_history_or_raise( appointments = None except MultipleObjectsReturned: appointments = self.appointment_model_cls.objects.filter( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule_name=self.schedule_name, visit_schedule_name=self.visit_schedule_name, **{f"{related_visit_model_attr}__report_datetime__gt": offschedule_datetime}, @@ -241,12 +253,12 @@ def update_history_or_raise( history_obj.schedule_status = OFF_SCHEDULE history_obj.save() - def _update_in_progress_appointment(self, subject_identifier=None): + def _update_in_progress_appointment(self): """Updates the "in_progress" appointment and clears future appointments. """ for obj in self.appointment_model_cls.objects.filter( - subject_identifier=subject_identifier, + subject_identifier=self.subject_identifier, schedule_name=self.schedule_name, visit_schedule_name=self.visit_schedule_name, appt_status=IN_PROGRESS_APPT, @@ -254,60 +266,47 @@ def _update_in_progress_appointment(self, subject_identifier=None): obj.appt_status = COMPLETE_APPT obj.save() - def resave(self, subject_identifier=None): + def resave(self): """Resaves the onschedule model instance to trigger, for example, appointment creation (if using edc_appointment mixin). """ - obj = self.onschedule_model_cls.objects.get(subject_identifier=subject_identifier) + obj = self.onschedule_model_cls.objects.get(subject_identifier=self.subject_identifier) obj.save() - def registered_or_raise(self, subject_identifier=None) -> RegisteredSubject: + def registered_or_raise(self) -> RegisteredSubject: """Return an instance RegisteredSubject or raise an exception if instance does not exist. """ model_cls = django_apps.get_model(self.registered_subject_model) try: - obj = model_cls.objects.get(subject_identifier=subject_identifier) + obj = model_cls.objects.get(subject_identifier=self.subject_identifier) except ObjectDoesNotExist: raise UnknownSubjectError( f"Failed to put subject on schedule. Unknown subject. " f"Searched `{self.registered_subject_model}`. " - f"Got subject_identifier=`{subject_identifier}`." + f"Got subject_identifier=`{self.subject_identifier}`." ) return obj - def consented_or_raise(self, subject_identifier=None): - """Raises an exception if one or more consents do not exist.""" - if not self.consent_model_cls.objects.filter( - subject_identifier=subject_identifier - ).exists(): - raise NotConsentedError( - f"Failed to put subject on schedule. Consent not found. " - f"Using consent model '{self.consent_model}' " - f"subject identifier={subject_identifier}." - ) - - def onschedule_or_raise( - self, subject_identifier=None, report_datetime=None, compare_as_datetimes=None - ): + def onschedule_or_raise(self, report_datetime=None, compare_as_datetimes=None): """Raise an exception if subject is not on the schedule during the given date. """ compare_as_datetimes = True if compare_as_datetimes is None else compare_as_datetimes try: onschedule_obj = self.onschedule_model_cls.objects.get( - subject_identifier=subject_identifier + subject_identifier=self.subject_identifier ) except ObjectDoesNotExist: raise NotOnScheduleError( f"Subject has not been put on a schedule `{self.schedule_name}`. " - f"Got subject_identifier=`{subject_identifier}`." + f"Got subject_identifier=`{self.subject_identifier}`." ) try: offschedule_datetime = self.offschedule_model_cls.objects.values_list( "offschedule_datetime", flat=True - ).get(subject_identifier=subject_identifier) + ).get(subject_identifier=self.subject_identifier) except ObjectDoesNotExist: offschedule_datetime = None @@ -334,7 +333,7 @@ def onschedule_or_raise( raise NotOnScheduleForDateError( f"Subject not on schedule '{self.schedule_name}' for " f"report date '{formatted_report_datetime}'. " - f"Got '{subject_identifier}' was taken " + f"Got '{self.subject_identifier}' was taken " f"off this schedule on '{formatted_offschedule_datetime}'." ) return None diff --git a/edc_visit_schedule/system_checks.py b/edc_visit_schedule/system_checks.py index ba362cd..4812afa 100644 --- a/edc_visit_schedule/system_checks.py +++ b/edc_visit_schedule/system_checks.py @@ -1,7 +1,18 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from django.apps import apps as django_apps from django.core.checks import Warning +from .exceptions import SiteVisitScheduleError from .site_visit_schedules import site_visit_schedules +if TYPE_CHECKING: + from .schedule import Schedule + from .visit import Visit + from .visit_schedule import VisitSchedule + def visit_schedule_check(app_configs, **kwargs): errors = [] @@ -10,8 +21,58 @@ def visit_schedule_check(app_configs, **kwargs): errors.append( Warning("No visit schedules have been registered!", id="edc_visit_schedule.001") ) - site_results = site_visit_schedules.check() + site_results = check_models() for key, results in site_results.items(): for result in results: errors.append(Warning(result, id=f"edc_visit_schedule.{key}")) return errors + + +def check_models() -> dict[str, list]: + if not site_visit_schedules.loaded: + raise SiteVisitScheduleError("Registry is not loaded.") + errors = {"visit_schedules": [], "schedules": [], "visits": []} + for visit_schedule in site_visit_schedules.visit_schedules.values(): + errors["visit_schedules"].extend(check_visit_schedule_models(visit_schedule)) + for schedule in visit_schedule.schedules.values(): + errors["schedules"].extend(check_schedule_models(schedule)) + for visit in schedule.visits.values(): + errors["visits"].extend(check_visit_models(visit)) + return errors + + +def check_visit_schedule_models(visit_schedule: VisitSchedule) -> list[str]: + warnings = [] + for model in ["death_report", "locator", "offstudy"]: + try: + getattr(visit_schedule, f"{model}_model_cls") + except LookupError as e: + warnings.append(f"{e} See visit schedule '{visit_schedule.name}'.") + return warnings + + +def check_schedule_models(schedule: Schedule) -> list[str]: + warnings = [] + for model in ["onschedule", "offschedule", "appointment"]: + try: + getattr(schedule, f"{model}_model_cls") + except LookupError as e: + warnings.append(f"{e} See visit schedule '{schedule.name}'.") + return warnings + + +def check_visit_models(visit: Visit): + warnings = [] + models = list(set([f.model for f in visit.all_crfs])) + for model in models: + try: + django_apps.get_model(model) + except LookupError as e: + warnings.append(f"{e} Got Visit {visit.code} crf.model={model}.") + models = list(set([f.model for f in visit.all_requisitions])) + for model in models: + try: + django_apps.get_model(model) + except LookupError as e: + warnings.append(f"{e} Got Visit {visit.code} requisition.model={model}.") + return warnings diff --git a/edc_visit_schedule/tests/tests/test_model_form.py b/edc_visit_schedule/tests/tests/test_model_form.py index bc4179a..0b09777 100644 --- a/edc_visit_schedule/tests/tests/test_model_form.py +++ b/edc_visit_schedule/tests/tests/test_model_form.py @@ -1,14 +1,12 @@ from dateutil.relativedelta import relativedelta -from django.test import TestCase, override_settings -from edc_consent import site_consents -from edc_consent.consent_definition import ConsentDefinition -from edc_constants.constants import FEMALE, MALE +from django.test import TestCase, override_settings, tag +from edc_consent.site_consents import site_consents from edc_facility.import_holidays import import_holidays -from edc_protocol import Protocol from edc_sites.tests import SiteTestCaseMixin from edc_utils import get_utcnow from edc_visit_schedule.site_visit_schedules import site_visit_schedules +from visit_schedule_app.consents import v1_consent from visit_schedule_app.forms import OffScheduleForm from visit_schedule_app.models import OnSchedule, SubjectConsent from visit_schedule_app.visit_schedule import visit_schedule @@ -28,20 +26,11 @@ def setUp(self): site_visit_schedules.loaded = False site_visit_schedules._registry = {} site_visit_schedules.register(visit_schedule) - v1_consent = ConsentDefinition( - "visit_schedule_app.subjectconsent", - version="1", - start=Protocol().study_open_datetime, - end=Protocol().study_close_datetime, - age_min=18, - age_is_adult=18, - age_max=64, - gender=[MALE, FEMALE], - ) self.subject_identifier = "1234" site_consents.registry = {} site_consents.register(v1_consent) + @tag("2") def test_offschedule_ok(self): SubjectConsent.objects.create(subject_identifier=self.subject_identifier) onschedule = OnSchedule.objects.create( diff --git a/edc_visit_schedule/tests/tests/test_models.py b/edc_visit_schedule/tests/tests/test_models.py index 5f97f29..5dd50bd 100644 --- a/edc_visit_schedule/tests/tests/test_models.py +++ b/edc_visit_schedule/tests/tests/test_models.py @@ -2,11 +2,8 @@ from django.core.exceptions import ImproperlyConfigured from django.test import TestCase, override_settings from edc_appointment.models import Appointment -from edc_consent import site_consents -from edc_consent.consent_definition import ConsentDefinition -from edc_constants.constants import FEMALE, MALE +from edc_consent.site_consents import site_consents from edc_facility.import_holidays import import_holidays -from edc_protocol import Protocol from edc_sites.tests import SiteTestCaseMixin from edc_utils import get_utcnow from edc_visit_tracking.constants import SCHEDULED @@ -17,6 +14,7 @@ RegistryNotLoaded, site_visit_schedules, ) +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import ( BadOffSchedule1, CrfOne, @@ -53,16 +51,6 @@ def setUp(self): site_visit_schedules.loaded = False site_visit_schedules._registry = {} site_visit_schedules.register(visit_schedule) - v1_consent = ConsentDefinition( - "visit_schedule_app.subjectconsent", - version="1", - start=Protocol().study_open_datetime, - end=Protocol().study_close_datetime, - age_min=18, - age_is_adult=18, - age_max=64, - gender=[MALE, FEMALE], - ) self.subject_identifier = "1234" site_consents.registry = {} site_consents.register(v1_consent) @@ -248,11 +236,11 @@ def test_crf(self): """Assert can enter a CRF.""" SubjectConsent.objects.create( subject_identifier=self.subject_identifier, - consent_datetime=get_utcnow() - relativedelta(years=3), + consent_datetime=get_utcnow() - relativedelta(months=6), ) OnSchedule.objects.create( subject_identifier=self.subject_identifier, - onschedule_datetime=get_utcnow() - relativedelta(years=3), + onschedule_datetime=get_utcnow() - relativedelta(months=6), ) appointments = Appointment.objects.all().order_by("timepoint", "visit_code_sequence") self.assertEqual(appointments.count(), 4) diff --git a/edc_visit_schedule/tests/tests/test_schedule.py b/edc_visit_schedule/tests/tests/test_schedule.py index 37c96e4..615329c 100644 --- a/edc_visit_schedule/tests/tests/test_schedule.py +++ b/edc_visit_schedule/tests/tests/test_schedule.py @@ -7,7 +7,9 @@ from edc_visit_schedule.schedule import AlreadyRegisteredVisit, Schedule from edc_visit_schedule.schedule.schedule import ScheduleNameError, VisitTimepointError from edc_visit_schedule.schedule.visit_collection import VisitCollectionError +from edc_visit_schedule.system_checks import check_schedule_models from edc_visit_schedule.visit import Visit +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import OffSchedule, OnSchedule @@ -21,7 +23,7 @@ def test_visit_schedule_repr(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) self.assertTrue(schedule.__repr__()) @@ -31,26 +33,20 @@ def test_visit_schedule_field_value(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) self.assertEqual(schedule.field_value, "schedule") - def test_schedule_onschedule_model_is_none(self): - self.assertRaises(AttributeError, Schedule, name="schedule", onschedule_model=None) - - def test_schedule_offschedule_model_is_none(self): - self.assertRaises(AttributeError, Schedule, name="schedule", offschedule_model=None) - def test_schedule_bad_label_lower(self): schedule = Schedule( name="schedule", onschedule_model="x.x", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) - errors = schedule.check() + errors = check_schedule_models(schedule) self.assertIsNotNone(errors) def test_schedule_bad_label_lower2(self): @@ -58,10 +54,10 @@ def test_schedule_bad_label_lower2(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="x.x", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) - errors = schedule.check() + errors = check_schedule_models(schedule) self.assertIsNotNone(errors) def test_schedule_onschedule_model_cls(self): @@ -69,7 +65,7 @@ def test_schedule_onschedule_model_cls(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) self.assertEqual(schedule.onschedule_model_cls, OnSchedule) @@ -79,7 +75,7 @@ def test_schedule_offschedule_model_cls(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="edc_visit_schedule.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) self.assertEqual(schedule.offschedule_model_cls, OffSchedule) @@ -89,7 +85,7 @@ def test_schedule_ok(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) @@ -98,7 +94,7 @@ def test_add_visits(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) for i in range(0, 5): @@ -119,7 +115,7 @@ def test_add_visits_duplicate_code(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) visit = Visit( @@ -146,7 +142,7 @@ def test_add_visits_duplicate_title(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) visit = Visit( @@ -173,7 +169,7 @@ def test_add_visits_custom_base_timepoint(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", base_timepoint=1, ) @@ -195,7 +191,7 @@ def test_add_visits_base_timepoint_mismatch(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", base_timepoint=1, ) @@ -214,7 +210,7 @@ def test_add_visits_duplicate_timepoint(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) visit = Visit( @@ -239,7 +235,7 @@ def test_add_visits_duplicate_rbase(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) visit = Visit( @@ -266,7 +262,7 @@ def setUp(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) @@ -275,7 +271,7 @@ def test_wont_accept_visit_before_base_timepoint(self): name="schedule", onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", base_timepoint=1, ) diff --git a/edc_visit_schedule/tests/tests/test_site_visit_schedule.py b/edc_visit_schedule/tests/tests/test_site_visit_schedule.py index ef99ef9..78652d3 100644 --- a/edc_visit_schedule/tests/tests/test_site_visit_schedule.py +++ b/edc_visit_schedule/tests/tests/test_site_visit_schedule.py @@ -7,6 +7,7 @@ site_visit_schedules, ) from edc_visit_schedule.visit_schedule import VisitSchedule +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import OffSchedule, OnSchedule @@ -32,7 +33,7 @@ def test_already_registered(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) self.visit_schedule.add_schedule(schedule) @@ -58,7 +59,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) @@ -76,7 +77,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onscheduletwo", offschedule_model="visit_schedule_app.offscheduletwo", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) diff --git a/edc_visit_schedule/tests/tests/test_subject_schedule.py b/edc_visit_schedule/tests/tests/test_subject_schedule.py index ba7f297..8f67290 100644 --- a/edc_visit_schedule/tests/tests/test_subject_schedule.py +++ b/edc_visit_schedule/tests/tests/test_subject_schedule.py @@ -1,9 +1,8 @@ +from datetime import datetime + from django.core.exceptions import ObjectDoesNotExist -from django.test import TestCase, override_settings -from edc_consent import site_consents -from edc_consent.consent_definition import ConsentDefinition -from edc_constants.constants import FEMALE, MALE -from edc_protocol import Protocol +from django.test import TestCase, override_settings, tag +from edc_consent.site_consents import site_consents from edc_sites.tests import SiteTestCaseMixin from edc_utils import get_utcnow @@ -12,22 +11,13 @@ from edc_visit_schedule.site_visit_schedules import site_visit_schedules from edc_visit_schedule.subject_schedule import SubjectSchedule, SubjectScheduleError from edc_visit_schedule.visit_schedule import VisitSchedule +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import OffSchedule, OnSchedule, SubjectConsent @override_settings(SITE_ID=30) class TestSubjectSchedule(SiteTestCaseMixin, TestCase): def setUp(self): - v1_consent = ConsentDefinition( - "visit_schedule_app.subjectconsent", - version="1", - start=Protocol().study_open_datetime, - end=Protocol().study_close_datetime, - age_min=18, - age_is_adult=18, - age_max=64, - gender=[MALE, FEMALE], - ) site_consents.registry = {} site_consents.register(v1_consent) site_visit_schedules._registry = {} @@ -43,7 +33,7 @@ def setUp(self): onschedule_model="visit_schedule_app.OnSchedule", offschedule_model="visit_schedule_app.OffSchedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) self.schedule3 = Schedule( @@ -51,7 +41,7 @@ def setUp(self): onschedule_model="visit_schedule_app.OnScheduleThree", offschedule_model="visit_schedule_app.OffScheduleThree", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) @@ -71,20 +61,20 @@ def setUp(self): onschedule_model="visit_schedule_app.OnScheduleTwo", offschedule_model="visit_schedule_app.OffScheduleTwo", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], ) self.schedule_two_2 = Schedule( name="schedule_four", onschedule_model="visit_schedule_app.OnScheduleFour", offschedule_model="visit_schedule_app.OffScheduleFour", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], ) self.visit_schedule_two.add_schedule(self.schedule_two_1) self.visit_schedule_two.add_schedule(self.schedule_two_2) site_visit_schedules.register(self.visit_schedule_two) - self.subject_identifier = "111111" + self.subject_identifier: str = "111111" SubjectConsent.objects.create(subject_identifier=self.subject_identifier) def test_onschedule_updates_history(self): @@ -98,12 +88,11 @@ def test_onschedule_updates_history(self): onschedule_model ) subject_schedule = SubjectSchedule( - visit_schedule=visit_schedule, schedule=schedule - ) - subject_schedule.put_on_schedule( - subject_identifier=self.subject_identifier, - onschedule_datetime=get_utcnow(), + self.subject_identifier, + visit_schedule=visit_schedule, + schedule=schedule, ) + subject_schedule.put_on_schedule(onschedule_datetime=get_utcnow()) try: SubjectScheduleHistory.objects.get( subject_identifier=self.subject_identifier, @@ -122,24 +111,28 @@ def test_multpile_consents(self): visit_schedule, schedule = site_visit_schedules.get_by_onschedule_model( "visit_schedule_app.onscheduletwo" ) - subject_schedule = SubjectSchedule(visit_schedule=visit_schedule, schedule=schedule) + subject_schedule = SubjectSchedule( + subject_identifier, visit_schedule=visit_schedule, schedule=schedule + ) try: - subject_schedule.put_on_schedule( - subject_identifier=subject_identifier, onschedule_datetime=get_utcnow() - ) + subject_schedule.put_on_schedule(onschedule_datetime=get_utcnow()) except SubjectScheduleError: self.fail("SubjectScheduleError unexpectedly raised.") + @tag("1") def test_resave(self): """Asserts returns the correct instances for the schedule.""" visit_schedule, schedule = site_visit_schedules.get_by_onschedule_model( "visit_schedule_app.onscheduletwo" ) - subject_schedule = SubjectSchedule(visit_schedule=visit_schedule, schedule=schedule) - subject_schedule.put_on_schedule( - subject_identifier=self.subject_identifier, onschedule_datetime=get_utcnow() + subject_schedule = SubjectSchedule( + self.subject_identifier, + visit_schedule=visit_schedule, + schedule=schedule, ) - subject_schedule.resave(subject_identifier=self.subject_identifier) + onschedule_datetime: datetime = get_utcnow() + subject_schedule.put_on_schedule(onschedule_datetime) + subject_schedule.resave() def test_put_on_schedule(self): _, schedule = site_visit_schedules.get_by_onschedule_model( @@ -163,11 +156,8 @@ def test_take_off_schedule(self): visit_schedule_name="visit_schedule" ) schedule = visit_schedule.schedules.get("schedule") - schedule.put_on_schedule(subject_identifier=self.subject_identifier) - schedule.take_off_schedule( - subject_identifier=self.subject_identifier, - offschedule_datetime=get_utcnow(), - ) + schedule.put_on_schedule(self.subject_identifier, get_utcnow()) + schedule.take_off_schedule(self.subject_identifier, get_utcnow()) try: OffSchedule.objects.get(subject_identifier=self.subject_identifier) except ObjectDoesNotExist: diff --git a/edc_visit_schedule/tests/tests/test_system_checks.py b/edc_visit_schedule/tests/tests/test_system_checks.py index 78f0f88..8d8de71 100644 --- a/edc_visit_schedule/tests/tests/test_system_checks.py +++ b/edc_visit_schedule/tests/tests/test_system_checks.py @@ -8,6 +8,7 @@ from edc_visit_schedule.visit import CrfCollection, Visit from edc_visit_schedule.visit.crf import Crf from edc_visit_schedule.visit_schedule import VisitSchedule +from visit_schedule_app.consents import v1_consent class TestSystemChecks(TestCase): @@ -31,7 +32,7 @@ def test_visit_schedule_ok(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) visit_schedule.add_schedule(schedule) @@ -53,7 +54,7 @@ def test_visit_schedule_bad_model(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) visit_schedule.add_schedule(schedule) @@ -76,7 +77,7 @@ def test_schedule_bad_model(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="blah.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) visit_schedule.add_schedule(schedule) @@ -99,7 +100,7 @@ def test_schedule_bad_crf_model(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) crfs = CrfCollection( diff --git a/edc_visit_schedule/tests/tests/test_utils.py b/edc_visit_schedule/tests/tests/test_utils.py index 84cdb59..2d01a65 100644 --- a/edc_visit_schedule/tests/tests/test_utils.py +++ b/edc_visit_schedule/tests/tests/test_utils.py @@ -1,11 +1,9 @@ from datetime import date from dateutil.relativedelta import relativedelta -from django.test import TestCase, override_settings, tag +from django.test import TestCase, override_settings from edc_appointment.models import Appointment -from edc_consent import site_consents -from edc_consent.site_consents import AlreadyRegistered -from edc_consent.tests.consent_test_utils import consent_definition_factory +from edc_consent.site_consents import site_consents from edc_facility.import_holidays import import_holidays from edc_sites.tests import SiteTestCaseMixin from edc_utils import get_utcnow @@ -17,6 +15,7 @@ from edc_visit_schedule.utils import is_baseline from edc_visit_schedule.visit import Visit from edc_visit_schedule.visit_schedule import VisitSchedule +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import SubjectVisit @@ -43,7 +42,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=1, ) @@ -70,15 +69,13 @@ def setUp(self): site_consents.registry = {} for schedule in self.visit_schedule.schedules.values(): - try: - consent_definition_factory(model=schedule.consent_model) - except AlreadyRegistered: - pass + for cdef in schedule.consent_definitions: + site_consents.register(cdef) _, schedule = site_visit_schedules.get_by_onschedule_model( "visit_schedule_app.onschedule" ) - cdef = site_consents.get_consent_definition(model=schedule.consent_model) + cdef = schedule.consent_definitions[0] self.subject_consent = cdef.model_cls.objects.create( subject_identifier="12345", consent_datetime=get_utcnow() - relativedelta(seconds=1), @@ -97,7 +94,6 @@ def setUp(self): "timepoint", "visit_code_sequence" ) - @tag("1") def test_is_baseline_with_instance(self): subject_visit_0 = SubjectVisit.objects.create( appointment=self.appointments[0], @@ -168,7 +164,7 @@ def test_is_baseline_with_params(self): with self.assertRaises(VisitScheduleBaselineError) as cm: is_baseline( - timepoint=100, + timepoint=100.0, visit_schedule_name=subject_visit_0.visit_schedule_name, schedule_name=subject_visit_0.schedule_name, visit_code_sequence=0, diff --git a/edc_visit_schedule/tests/tests/test_view.py b/edc_visit_schedule/tests/tests/test_view.py index 9526488..97197ca 100644 --- a/edc_visit_schedule/tests/tests/test_view.py +++ b/edc_visit_schedule/tests/tests/test_view.py @@ -5,10 +5,7 @@ from django.test.client import RequestFactory from django.test.utils import override_settings from django.views.generic.base import ContextMixin -from edc_consent import site_consents -from edc_consent.consent_definition import ConsentDefinition -from edc_constants.constants import FEMALE, MALE -from edc_protocol import Protocol +from edc_consent.site_consents import site_consents from edc_sites.tests import SiteTestCaseMixin from edc_utils import get_utcnow @@ -16,6 +13,7 @@ from edc_visit_schedule.site_visit_schedules import site_visit_schedules from edc_visit_schedule.view_mixins import VisitScheduleViewMixin from edc_visit_schedule.visit_schedule import VisitSchedule +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import OnSchedule, SubjectConsent @@ -34,16 +32,6 @@ class MyViewCurrent(VisitScheduleViewMixin, ContextMixin): ) class TestViewMixin(SiteTestCaseMixin, TestCase): def setUp(self): - v1_consent = ConsentDefinition( - "visit_schedule_app.subjectconsent", - version="1", - start=Protocol().study_open_datetime, - end=Protocol().study_close_datetime, - age_min=18, - age_is_adult=18, - age_max=64, - gender=[MALE, FEMALE], - ) site_consents.registry = {} site_consents.register(v1_consent) self.visit_schedule = VisitSchedule( @@ -57,14 +45,14 @@ def setUp(self): name="schedule", onschedule_model="visit_schedule_app.OnSchedule", offschedule_model="visit_schedule_app.OffSchedule", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) self.schedule3 = Schedule( name="schedule_three", onschedule_model="visit_schedule_app.OnScheduleThree", offschedule_model="visit_schedule_app.OffScheduleThree", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], appointment_model="edc_appointment.appointment", ) diff --git a/edc_visit_schedule/tests/tests/test_visit_schedule.py b/edc_visit_schedule/tests/tests/test_visit_schedule.py index aae609c..ad61eaa 100644 --- a/edc_visit_schedule/tests/tests/test_visit_schedule.py +++ b/edc_visit_schedule/tests/tests/test_visit_schedule.py @@ -3,10 +3,9 @@ from dateutil.relativedelta import relativedelta from django.test import TestCase, override_settings from edc_appointment.models import Appointment -from edc_consent import site_consents +from edc_consent import NotConsentedError from edc_consent.consent_definition import ConsentDefinition -from edc_consent.site_consents import AlreadyRegistered -from edc_consent.tests.consent_test_utils import consent_definition_factory +from edc_consent.site_consents import site_consents from edc_constants.constants import FEMALE, MALE from edc_facility.import_holidays import import_holidays from edc_protocol import Protocol @@ -25,15 +24,16 @@ ) from edc_visit_schedule.subject_schedule import ( InvalidOffscheduleDate, - NotConsentedError, NotOnScheduleError, ) +from edc_visit_schedule.system_checks import check_visit_schedule_models from edc_visit_schedule.visit import Crf, FormsCollection, FormsCollectionError, Visit from edc_visit_schedule.visit_schedule import ( AlreadyRegisteredSchedule, VisitSchedule, VisitScheduleNameError, ) +from visit_schedule_app.consents import v1_consent from visit_schedule_app.models import ( OffSchedule, OnSchedule, @@ -99,7 +99,7 @@ def test_visit_schedule_validates(self): death_report_model="visit_schedule_app.deathreport", locator_model="edc_locator.subjectlocator", ) - errors = visit_schedule.check() + errors = check_visit_schedule_models(visit_schedule) if errors: self.fail("visit_schedule.check() unexpectedly failed") @@ -115,16 +115,6 @@ def setUpTestData(cls): import_holidays() def setUp(self): - v1_consent = ConsentDefinition( - "visit_schedule_app.subjectconsent", - version="1", - start=Protocol().study_open_datetime, - end=Protocol().study_close_datetime, - age_min=18, - age_is_adult=18, - age_max=64, - gender=[MALE, FEMALE], - ) site_consents.registry = {} site_consents.register(v1_consent) @@ -141,7 +131,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=v1_consent, ) self.schedule2 = Schedule( @@ -149,7 +139,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onscheduletwo", offschedule_model="visit_schedule_app.offscheduletwo", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=v1_consent, ) self.schedule3 = Schedule( @@ -157,7 +147,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onschedulethree", offschedule_model="visit_schedule_app.offschedulethree", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=v1_consent, ) def test_visit_schedule_add_schedule(self): @@ -166,17 +156,6 @@ def test_visit_schedule_add_schedule(self): except AlreadyRegisteredSchedule: self.fail("AlreadyRegisteredSchedule unexpectedly raised.") - def test_visit_schedule_add_schedule_invalid_appointment_model(self): - self.assertRaises( - AttributeError, - Schedule, - name="schedule_bad", - onschedule_model="visit_schedule_app.onschedule", - offschedule_model="visit_schedule_app.offschedule", - appointment_model=None, - consent_model="visit_schedule_app.subjectconsent", - ) - def test_visit_schedule_add_schedule_with_appointment_model(self): self.visit_schedule.add_schedule(self.schedule3) for schedule in self.visit_schedule.schedules.values(): @@ -227,7 +206,7 @@ def setUp(self): onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=v1_consent, base_timepoint=1, ) @@ -244,12 +223,9 @@ def setUp(self): site_visit_schedules.register(self.visit_schedule) site_consents.registry = {} - try: - consent_definition_factory(model=self.schedule.consent_model) - except AlreadyRegistered: - pass - - cdef = site_consents.get_consent_definition(model=self.schedule.consent_model) + for cdef in self.schedule.consent_definitions: + site_consents.register(cdef) + cdef = self.schedule.consent_definitions[0] self.subject_consent = cdef.model_cls.objects.create( subject_identifier="12345", consent_datetime=get_utcnow() - relativedelta(years=1), diff --git a/edc_visit_schedule/utils.py b/edc_visit_schedule/utils.py index ae7b3f7..3a358d7 100644 --- a/edc_visit_schedule/utils.py +++ b/edc_visit_schedule/utils.py @@ -119,9 +119,7 @@ def off_schedule_or_raise( visit_schedule_name=visit_schedule_name ) schedule = visit_schedule.schedules.get(schedule_name) - if schedule.is_onschedule( - subject_identifier=subject_identifier, report_datetime=report_datetime - ): + if schedule.is_onschedule(subject_identifier, report_datetime): raise OnScheduleError( f"Not allowed. Subject {subject_identifier} is on schedule " f"{visit_schedule.verbose_name}.{schedule_name} on " diff --git a/edc_visit_schedule/view_mixins.py b/edc_visit_schedule/view_mixins.py index fab96eb..f823692 100644 --- a/edc_visit_schedule/view_mixins.py +++ b/edc_visit_schedule/view_mixins.py @@ -26,6 +26,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) def get_context_data(self, **kwargs) -> dict[str, Any]: + # TODO: What if active on more than one schedule?? for visit_schedule in site_visit_schedules.visit_schedules.values(): if self.subject_identifier: for schedule in visit_schedule.schedules.values(): @@ -36,15 +37,12 @@ def get_context_data(self, **kwargs) -> dict[str, Any]: except ObjectDoesNotExist: pass else: - if schedule.is_onschedule( - subject_identifier=self.subject_identifier, - report_datetime=get_utcnow(), - ): + self.onschedule_models.append(onschedule_model_obj) + self.visit_schedules.update({visit_schedule.name: visit_schedule}) + if schedule.is_onschedule(self.subject_identifier, get_utcnow()): self.current_schedule = schedule self.current_visit_schedule = visit_schedule self.current_onschedule_model = onschedule_model_obj - self.onschedule_models.append(onschedule_model_obj) - self.visit_schedules.update({visit_schedule.name: visit_schedule}) kwargs.update( visit_schedules=self.visit_schedules, current_onschedule_model=self.current_onschedule_model, diff --git a/edc_visit_schedule/visit/visit.py b/edc_visit_schedule/visit/visit.py index e5aabb0..b0a0c65 100644 --- a/edc_visit_schedule/visit/visit.py +++ b/edc_visit_schedule/visit/visit.py @@ -295,22 +295,6 @@ def timepoint_datetime(self) -> datetime: def timepoint_datetime(self, dt=None): self.dates.base = to_utc(dt) - def check(self): - warnings = [] - models = list(set([f.model for f in self.all_crfs])) - for model in models: - try: - django_apps.get_model(model) - except LookupError as e: - warnings.append(f"{e} Got Visit {self.code} crf.model={model}.") - models = list(set([f.model for f in self.all_requisitions])) - for model in models: - try: - django_apps.get_model(model) - except LookupError as e: - warnings.append(f"{e} Got Visit {self.code} requisition.model={model}.") - return warnings - def to_dict(self): return dict( crfs=[(crf.model, crf.required) for crf in self.crfs], diff --git a/edc_visit_schedule/visit_schedule/visit_schedule.py b/edc_visit_schedule/visit_schedule/visit_schedule.py index 839be5b..dc58571 100644 --- a/edc_visit_schedule/visit_schedule/visit_schedule.py +++ b/edc_visit_schedule/visit_schedule/visit_schedule.py @@ -110,19 +110,6 @@ def add_schedule(self, schedule=None): self._all_post_consent_models = None return schedule - def check(self): - warnings = [] - for model in [ - "death_report", - "locator", - "offstudy", - ]: - try: - getattr(self, f"{model}_model_cls") - except LookupError as e: - warnings.append(f"{e} See visit schedule '{self.name}'.") - return warnings - @property def all_post_consent_models(self): """Returns a dictionary of models and the needed consent diff --git a/runtests.py b/runtests.py index 64068ef..c656161 100644 --- a/runtests.py +++ b/runtests.py @@ -26,6 +26,7 @@ "multisite", "django_crypto_fields.apps.AppConfig", "django_revision.apps.AppConfig", + "edc_sites.apps.AppConfig", "edc_appointment.apps.AppConfig", "edc_list_data.apps.AppConfig", "edc_action_item.apps.AppConfig", @@ -40,7 +41,6 @@ "edc_timepoint.apps.AppConfig", "edc_offstudy.apps.AppConfig", "edc_protocol.apps.AppConfig", - "edc_sites.apps.AppConfig", "edc_visit_tracking.apps.AppConfig", "edc_visit_schedule.apps.AppConfig", "visit_schedule_app.apps.EdcMetadataAppConfig", diff --git a/visit_schedule_app/admin.py b/visit_schedule_app/admin.py new file mode 100644 index 0000000..b7fa464 --- /dev/null +++ b/visit_schedule_app/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from .admin_site import visit_schedule_app_admin +from .models import CrfOne + + +@admin.register(CrfOne, site=visit_schedule_app_admin) +class CrfOneAdmin(admin.ModelAdmin): + pass diff --git a/visit_schedule_app/admin_site.py b/visit_schedule_app/admin_site.py new file mode 100644 index 0000000..16c01ce --- /dev/null +++ b/visit_schedule_app/admin_site.py @@ -0,0 +1,7 @@ +from edc_model_admin.admin_site import EdcAdminSite + +from .apps import AppConfig + +visit_schedule_app_admin = EdcAdminSite( + name="visit_schedule_app_admin", app_label=AppConfig.name +) diff --git a/visit_schedule_app/urls.py b/visit_schedule_app/urls.py new file mode 100644 index 0000000..8d425c9 --- /dev/null +++ b/visit_schedule_app/urls.py @@ -0,0 +1,7 @@ +from django.urls.conf import path + +from .admin_site import visit_schedule_app_admin + +app_name = "visit_schedule_app" + +urlpatterns = [path("admin/", visit_schedule_app_admin.urls)] diff --git a/visit_schedule_app/visit_schedule.py b/visit_schedule_app/visit_schedule.py index cfd962f..43dfe80 100644 --- a/visit_schedule_app/visit_schedule.py +++ b/visit_schedule_app/visit_schedule.py @@ -1,10 +1,12 @@ from dateutil.relativedelta import relativedelta from edc_visit_schedule.schedule import Schedule -from edc_visit_schedule.visit import Crf, FormsCollection, Visit +from edc_visit_schedule.visit import Crf, CrfCollection, Visit from edc_visit_schedule.visit_schedule import VisitSchedule -crfs = FormsCollection(Crf(show_order=1, model="visit_schedule_app.crfone", required=True)) +from .consents import v1_consent + +crfs = CrfCollection(Crf(show_order=1, model="visit_schedule_app.crfone", required=True)) visit0 = Visit( code="1000", @@ -51,7 +53,7 @@ onschedule_model="visit_schedule_app.onschedule", offschedule_model="visit_schedule_app.offschedule", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], ) schedule.add_visit(visit0) @@ -73,7 +75,7 @@ onschedule_model="visit_schedule_app.onschedule2", offschedule_model="visit_schedule_app.offschedule2", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], base_timepoint=3, ) @@ -92,7 +94,7 @@ onschedule_model="visit_schedule_app.onschedulefive", offschedule_model="visit_schedule_app.offschedulefive", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], ) schedule5.add_visit(visit0) @@ -110,7 +112,7 @@ onschedule_model="visit_schedule_app.onschedulesix", offschedule_model="visit_schedule_app.offschedulesix", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], ) schedule6.add_visit(visit0) @@ -128,7 +130,7 @@ onschedule_model="visit_schedule_app.onscheduleseven", offschedule_model="visit_schedule_app.offscheduleseven", appointment_model="edc_appointment.appointment", - consent_model="visit_schedule_app.subjectconsent", + consent_definitions=[v1_consent], ) schedule7.add_visit(visit0)