class SubjectRefusalScreening(SiteModelMixin, BaseUuidModel): mocca_register = models.OneToOneField( "mocca_screening.moccaregister", on_delete=models.PROTECT, null=True, verbose_name="MOCCA (original) register details", ) report_datetime = models.DateTimeField(verbose_name="Report Date and Time", default=get_utcnow) reason = models.CharField( verbose_name="Reason for refusal to screen", max_length=25, choices=REFUSAL_REASONS_SCREENING, ) other_reason = OtherCharField() comment = models.TextField( verbose_name="Additional Comments", null=True, blank=True, ) on_site = CurrentSiteManager() objects = Manager() history = HistoricalRecords() def __str__(self): return self.mocca_register.mocca_study_identifier def natural_key(self): return tuple(self.mocca_register) @staticmethod def get_search_slug_fields(): return ["screening_identifier"] class Meta(BaseUuidModel.Meta): verbose_name = "Refusal to Screen" verbose_name_plural = "Refusal to Screen"
class UnblindingReview( NonUniqueSubjectIdentifierFieldMixin, SiteModelMixin, ActionModelMixin, TrackingModelMixin, BaseUuidModel, ): action_name = UNBLINDING_REVIEW_ACTION tracking_identifier_prefix = "UR" report_datetime = models.DateTimeField(verbose_name="Report Date and Time", default=get_utcnow) reviewer = models.ForeignKey( UnblindingReviewerUser, related_name="+", on_delete=models.PROTECT, verbose_name="Unblinding request reviewed by", help_text="Select a name from the list", ) approved = models.CharField(max_length=15, default=TBD, choices=YES_NO_TBD) comment = models.TextField(verbose_name="Comment", null=True) on_site = CurrentSiteManager() objects = SubjectIdentifierManager() def natural_key(self): return (self.action_identifier, ) class Meta(BaseUuidModel.Meta): verbose_name = "Unblinding Review" verbose_name_plural = "Unblinding Reviews" indexes = [ models.Index(fields=[ "subject_identifier", "action_identifier", "site", "id" ]) ]
class IncomingTransaction(TransactionModelMixin, SiteModelMixin, BaseUuidModel): """ Transactions received from a remote host. """ site = models.ForeignKey(Site, on_delete=models.CASCADE, null=True, editable=False) is_consumed = models.BooleanField(default=False) is_self = models.BooleanField(default=False) on_site = CurrentSiteManager() objects = models.Manager() class Meta: ordering = ["timestamp"]
class Appointment(AppointmentModelMixin, SiteModelMixin, BaseUuidModel): on_site = CurrentSiteManager() objects = AppointmentManager() history = HistoricalRecords() def natural_key(self) -> tuple: return ( self.subject_identifier, self.visit_schedule_name, self.schedule_name, self.visit_code, self.visit_code_sequence, ) # noinspection PyTypeHints natural_key.dependencies = ["sites.Site"] # type: ignore class Meta(AppointmentModelMixin.Meta, BaseUuidModel.Meta): pass
class SubjectRefusal(SiteModelMixin, BaseUuidModel): screening_identifier = models.CharField(max_length=50, unique=True) report_datetime = models.DateTimeField(verbose_name="Report Date and Time", default=get_utcnow) reason = models.CharField( verbose_name="Reason for refusal to join", max_length=25, choices=REFUSAL_REASONS, ) other_reason = OtherCharField() comment = models.TextField( verbose_name="Additional Comments", null=True, blank=True, ) on_site = CurrentSiteManager() objects = SubjectRefusalManager() history = HistoricalRecords() def __str__(self): return self.screening_identifier def natural_key(self): return (self.screening_identifier, ) @staticmethod def get_search_slug_fields(): return ["screening_identifier"] class Meta(BaseUuidModel.Meta): verbose_name = "Refusal to Consent" verbose_name_plural = "Refusal to Consent"
class CareStatus(SiteModelMixin, CareModelMixin, BaseUuidModel): willing_to_answer = models.CharField( verbose_name= "Is the patient willing to provide information about their care status?", max_length=15, choices=YES_NO, default=YES, ) report_datetime = models.DateTimeField( verbose_name="Report Date and Time", default=get_utcnow, help_text="Date and time of report.", ) mocca_register = models.OneToOneField( "mocca_screening.moccaregister", on_delete=models.PROTECT, null=True, verbose_name="MOCCA (original) register details", ) on_site = CurrentSiteManager() objects = Manager() history = HistoricalRecords() def natural_key(self): return tuple(self.mocca_register) natural_key.dependencies = [ "sites.Site", "mocca_screen ing.MoccaRegister", ] class Meta(BaseUuidModel.Meta): verbose_name = "Care Status" verbose_name_plural = "Care Status"
class Order(SiteModelMixin, edc_models.BaseUuidModel): aliquot = models.ForeignKey(Aliquot, on_delete=PROTECT) order_identifier = models.CharField(max_length=25, editable=False, unique=True) order_datetime = models.DateTimeField(default=get_utcnow, validators=[datetime_not_future]) panel_name = models.CharField(max_length=25) on_site = CurrentSiteManager() objects = OrderManager() history = edc_models.HistoricalRecords() def natural_key(self): return (self.report_datetime,) + self.aliquot.natural_key() natural_key.dependencies = ["edc_lab.aliquot", "sites.Site"] class Meta(edc_models.BaseUuidModel.Meta): verbose_name = "Order"
class Manifest(ManifestModelMixin, SearchSlugModelMixin, edc_models.BaseUuidModel): def get_search_slug_fields(self): return [ "manifest_identifier", "human_readable_identifier", "shipper.name", "consignee.name", ] consignee = models.ForeignKey(Consignee, verbose_name="Consignee", on_delete=PROTECT) shipper = models.ForeignKey(Shipper, verbose_name="Shipper/Exporter", on_delete=PROTECT) on_site = CurrentSiteManager() objects = Manager() history = edc_models.HistoricalRecords() def natural_key(self): return (self.manifest_identifier,) natural_key.dependencies = ["edc_lab.shipper", "edc_lab.consignee"] def __str__(self): return "{} created on {} by {}".format( self.manifest_identifier, self.manifest_datetime.strftime("%Y-%m-%d"), self.user_created, ) @property def count(self): return self.manifestitem_set.all().count() class Meta(ManifestModelMixin.Meta, edc_models.BaseUuidModel.Meta): verbose_name = "Manifest"
class DailyClosingLog(SiteModelMixin, BaseUuidModel): site = models.ForeignKey( Site, on_delete=models.PROTECT, null=True, related_name="+", blank=False, ) log_date = models.DateField(verbose_name="Clinic date", default=get_utcnow) clinic_services = models.CharField( verbose_name="Which services are being offered at the clinic today?", max_length=25, choices=CLINIC_DAYS, ) attended = models.IntegerField( verbose_name="Total number of patients who attended the clinic today", validators=[MinValueValidator(0)], ) selection_method = models.CharField( verbose_name="How were patients selected to be approached?", max_length=25, choices=SELECTION_METHOD, ) approached = models.IntegerField( verbose_name= "Of those who attended, how many were approached by the study team", validators=[MinValueValidator(0)], ) agreed_to_screen = models.IntegerField( verbose_name="Of those approached, how many agreed to be screened", validators=[MinValueValidator(0)], ) comment = models.TextField( verbose_name="Additional Comments", null=True, blank=True, ) on_site = CurrentSiteManager() objects = DailyClosingLogManager() history = HistoricalRecords() def __str__(self): return self.log_date.strftime( convert_php_dateformat(settings.DATE_FORMAT)) def natural_key(self): return (self.log_date, self.site) class Meta: verbose_name = "Daily Closing Log" verbose_name_plural = "Daily Closing Logs" constraints = [ models.UniqueConstraint(fields=["log_date", "site"], name="unique_date_for_site"), ]
class RequisitionMetadata(CrfMetadataModelMixin, SiteModelMixin, edc_models.BaseUuidModel): panel_name = models.CharField(max_length=50, null=True) on_site = CurrentSiteManager() objects = RequisitionMetadataManager() def __str__(self) -> str: return ( f"RequisitionMeta {self.model} {self.visit_schedule_name}." f"{self.schedule_name}.{self.visit_code}.{self.visit_code_sequence}@" f"{self.timepoint} {self.panel_name} {self.entry_status} " f"{self.subject_identifier}") @property def verbose_name(self) -> str: from edc_lab.site_labs import site_labs return site_labs.panel_names.get(self.panel_name) or self.panel_name def natural_key(self) -> tuple: return ( self.panel_name, self.model, self.subject_identifier, self.visit_schedule_name, self.schedule_name, self.visit_code, self.visit_code_sequence, ) # noinspection PyTypeHints natural_key.dependencies = ["sites.Site"] # type: ignore class Meta(CrfMetadataModelMixin.Meta, edc_models.BaseUuidModel.Meta): app_label = "edc_metadata" verbose_name = "Requisition Metadata" verbose_name_plural = "Requisition Metadata" unique_together = (( "subject_identifier", "visit_schedule_name", "schedule_name", "visit_code", "visit_code_sequence", "model", "panel_name", ), ) indexes = [ models.Index(fields=[ "subject_identifier", "visit_schedule_name", "schedule_name", "visit_code", "visit_code_sequence", "timepoint", "model", "entry_status", "show_order", "panel_name", ]) ]
class EndOfStudy( OffScheduleModelMixin, VisitScheduleFieldsModelMixin, ActionModelMixin, TrackingModelMixin, edc_models.BaseUuidModel, ): action_name = END_OF_STUDY_ACTION tracking_identifier_prefix = "ST" offschedule_datetime = models.DateTimeField( verbose_name="Date patient was terminated from the study", validators=[edc_models.datetime_not_future], blank=False, null=True, ) last_study_fu_date = models.DateField( verbose_name="Date of last research follow up (if different):", validators=[edc_models.date_not_future], blank=True, null=True, ) offschedule_reason = models.ForeignKey( OffstudyReasons, verbose_name="Reason patient was terminated from the study", on_delete=models.PROTECT, null=True, ) other_offschedule_reason = models.TextField( verbose_name="If OTHER, please specify", max_length=500, blank=True, null=True) ltfu_last_alive_date = models.DateField( verbose_name="If lost to followup, date last known to be alive", validators=[edc_models.date_not_future], blank=True, null=True, ) death_date = models.DateField( verbose_name="If deceased, date of death", validators=[edc_models.date_not_future], blank=True, null=True, ) ltfu_date = models.DateField( verbose_name="Date lost to followup, if applicable", validators=[edc_models.date_not_future], blank=True, null=True, ) transfer_date = models.DateField( verbose_name="Date transferred, if applicable", validators=[edc_models.date_not_future], blank=True, null=True, ) transferred_consent = models.CharField( verbose_name= "If transferred, has the patient provided consent to be followed-up?", choices=YES_NO_NA, max_length=15, default=NOT_APPLICABLE, ) comment = models.TextField(verbose_name="Comments", null=True, blank=True) on_site = CurrentSiteManager() def save(self, *args, **kwargs): if self.action_name == END_OF_STUDY_ACTION: raise ImproperlyConfigured( f"Invalid action name. Set this on the proxy model. Got {END_OF_STUDY_ACTION}." ) super().save(*args, **kwargs) class Meta(OffScheduleModelMixin.Meta): verbose_name = "End of Study" verbose_name_plural = "End of Study"
class RxRefill( MedicationOrderModelMixin, VisitCodeFieldsModelMixin, SiteModelMixin, edc_models.BaseUuidModel, ): rx = models.ForeignKey(Rx, on_delete=PROTECT) dosage_guideline = models.ForeignKey(DosageGuideline, on_delete=PROTECT) formulation = models.ForeignKey(Formulation, on_delete=PROTECT, null=True) dose = models.DecimalField( max_digits=6, decimal_places=1, null=True, blank=True, help_text="dose per frequency if NOT considering weight", ) calculate_dose = models.BooleanField(default=True) frequency = models.IntegerField( validators=[MinValueValidator(1)], null=True, blank=True, ) frequency_units = models.ForeignKey( FrequencyUnits, verbose_name="per", on_delete=PROTECT, null=True, blank=True, ) weight_in_kgs = models.DecimalField(max_digits=6, decimal_places=1, null=True, blank=True) refill_date = models.DateField(verbose_name="Refill date", default=get_utcnow_as_date, help_text="") number_of_days = models.IntegerField(null=True) total = models.DecimalField( max_digits=6, decimal_places=1, null=True, blank=True, help_text="Leave blank to auto-calculate", ) remaining = models.DecimalField( max_digits=6, decimal_places=1, null=True, blank=True, help_text="Leave blank to auto-calculate", ) notes = models.TextField( max_length=250, null=True, blank=True, help_text="Additional information for patient", ) active = models.BooleanField(default=False) verified = models.BooleanField(default=False) verified_datetime = models.DateTimeField(null=True, blank=True) as_string = models.CharField(max_length=150, editable=False) on_site = CurrentSiteManager() objects = Manager() history = edc_models.HistoricalRecords() def __str__(self): return ( f"{self.rx} " f"Take {self.dose} {self.formulation.formulation_type.display_name} {self.formulation.route.display_name} " # f"{self.frequency} {self.frequency_units.display_name}" ) def natural_key(self): return ( self.rx, self.medication, self.refill_date, ) def save(self, *args, **kwargs): if self.active: opts = dict(id=self.id) if self.id else {} if (self.__class__.objects.filter( rx__subject_identifier=self.rx.subject_identifier, dosage_guideline=self.dosage_guideline, active=True, ).exclude(**opts).exists()): raise ActivePrescriptionRefillExists( f"Unable to save as an active refill. An active refill already exists." ) self.medication = self.dosage_guideline.medication # if not self.dose and self.calculate_dose: self.dose = dosage_per_day( self.dosage_guideline, weight_in_kgs=self.weight_in_kgs, strength=self.formulation.strength, strength_units=self.formulation.units.name, ) self.frequency = self.dosage_guideline.frequency self.frequency_units = self.dosage_guideline.frequency_units self.total = float(self.dose) * float(self.number_of_days) if not self.id: self.remaining = self.total self.as_string = str(self) super().save(*args, **kwargs) @property def subject_identifier(self): return self.rx.subject_identifier class Meta(edc_models.BaseUuidModel.Meta): verbose_name = "RX refill" verbose_name_plural = "RX refills" unique_together = [ ["rx", "dosage_guideline", "refill_date"], ["rx", "visit_code", "visit_code_sequence"], ]
class RandomizationListModelMixin(models.Model): """ A model mixin for the randomization list. The default expects and ACTIVE vs PLACEBO randomization. If yours differs, you need to re-declare field "assignment" and model method "treatment_description". The default `Randomizer` class MAY also need to be customized. """ assignment = EncryptedCharField() randomizer_name = models.CharField(max_length=50, default="default") subject_identifier = models.CharField(verbose_name="Subject Identifier", max_length=50, null=True, unique=True) sid = models.IntegerField(unique=True) site_name = models.CharField(max_length=100) allocation = EncryptedCharField(verbose_name="Original integer allocation", null=True) allocated = models.BooleanField(default=False) allocated_datetime = models.DateTimeField(null=True) allocated_user = models.CharField(max_length=50, null=True) allocated_site = models.ForeignKey(Site, null=True, on_delete=models.PROTECT, related_name="+") verified = models.BooleanField(default=False) verified_datetime = models.DateTimeField(null=True) verified_user = models.CharField(max_length=50, null=True) objects = RandomizationListManager() history = HistoricalRecords(inherit=True) on_site = CurrentSiteManager("allocated_site") def __str__(self): return f"{self.site_name}.{self.sid} subject={self.subject_identifier}" def save(self, *args, **kwargs): self.randomizer_name = self.randomizer_cls.name try: getattr(self, "assignment_description") except RandomizationError as e: raise RandomizationListModelError(e) try: Site.objects.get(name=self.site_name) except ObjectDoesNotExist: site_names = [obj.name for obj in Site.objects.all()] raise RandomizationListModelError( f"Invalid site name. Got {self.site_name}. " f"Expected one of {site_names}.") super().save(*args, **kwargs) @property def short_label(self): return f"{self.assignment} SID:{self.site_name}.{self.sid}" @property def randomizer_cls(self): return site_randomizers.get(self.randomizer_name) # customize if approriate @property def assignment_description(self): """May be overridden.""" if self.assignment not in self.randomizer_cls.assignment_map: raise RandomizationError( f"Invalid assignment. Expected one of " f"{list(self.randomizer_cls.assignment_map.keys())}. " f"Got `{self.assignment}`. See ") return self.assignment def natural_key(self): return (self.sid, ) class Meta: abstract = True ordering = ("site_name", "sid") unique_together = ("site_name", "sid") permissions = (("display_assignment", "Can display assignment"), )
class IcpReferral(SiteModelMixin, BaseUuidModel): subject_screening = models.OneToOneField(SubjectScreening, null=True, on_delete=models.PROTECT) screening_identifier = models.CharField( verbose_name="META Screening Identifier", max_length=25, unique=True) report_datetime = models.DateTimeField( verbose_name="Report Date and Time", default=get_utcnow, help_text="Date and time of report.", ) hospital_identifier = models.CharField(max_length=25, unique=True) gender = models.CharField(choices=GENDER, max_length=10) age_in_years = models.IntegerField() initials = models.CharField(max_length=3) ethnicity = models.CharField(max_length=15, choices=ETHNICITY, help_text="Used for eGFR calculation") hiv_pos = models.CharField(verbose_name="HIV positive", max_length=15, choices=YES_NO) art_six_months = models.CharField( verbose_name="On anti-retroviral therapy for at least 6 months", max_length=15, choices=YES_NO_NA, ) ifg_value = models.DecimalField( verbose_name="Fasting glucose levels", max_digits=8, decimal_places=4, null=True, help_text="mmol/L", ) hba1c_value = models.DecimalField( verbose_name="HbA1c", max_digits=8, decimal_places=4, null=True, help_text="in %", ) ogtt_value = models.DecimalField( verbose_name= "Blood glucose levels 2-hours after glucose solution given", max_digits=8, decimal_places=4, null=True, help_text="mmol/L", ) meta_eligible = models.BooleanField(verbose_name="META eligibile") meta_eligibility_datetime = models.DateTimeField( null=True, help_text="Date and time META eligibility was determined") referred = models.BooleanField(verbose_name="Referred", default=False) referred = models.DateTimeField(null=True, help_text="Date and time of referral") referral_reasons = models.TextField(null=True) on_site = CurrentSiteManager() objects = IcpReferralManager() history = HistoricalRecords(inherit=True) def save(self, *args, **kwargs): try: SubjectScreening.objects.get( screening_identifier=self.screening_identifier) except ObjectDoesNotExist: raise IcpReferralError( f"Invalid META screening identifier. Got {self.screening_identifier}" ) super().save(*args, **kwargs) def __str__(self): return f"{self.screening_identifier} {self.gender} {self.age_in_years}" def natural_key(self): return tuple(self.screening_identifier, ) class Meta: pass
class ScreeningFieldsModeMixin(SiteModelMixin, models.Model): reference = models.UUIDField(verbose_name="Reference", unique=True, default=uuid4, editable=False) screening_identifier = models.CharField( verbose_name="Screening ID", max_length=50, blank=True, unique=True, editable=False, ) report_datetime = models.DateTimeField( verbose_name="Report Date and Time", default=get_utcnow, help_text="Date and time of report.", ) gender = models.CharField(choices=GENDER, max_length=10) age_in_years = models.IntegerField( validators=[MinValueValidator(0), MaxValueValidator(110)]) consent_ability = models.CharField( verbose_name="Participant or legal guardian/representative able and " "willing to give informed consent.", max_length=25, choices=YES_NO, ) unsuitable_for_study = models.CharField( verbose_name=("Is there any other reason the patient is " "deemed to not be suitable for the study?"), max_length=5, choices=YES_NO, default=NO, help_text="If YES, patient NOT eligible, please give reason below.", ) reasons_unsuitable = models.TextField( verbose_name="Reason not suitable for the study", max_length=150, null=True, blank=True, ) unsuitable_agreed = models.CharField( verbose_name=("Does the study coordinator agree that the patient " "is not suitable for the study?"), max_length=5, choices=YES_NO_NA, default=NOT_APPLICABLE, ) eligible = models.BooleanField(default=False, editable=False) reasons_ineligible = models.TextField(verbose_name="Reason not eligible", max_length=150, null=True, editable=False) eligibility_datetime = models.DateTimeField( null=True, editable=False, help_text="Date and time eligibility was determined") consented = models.BooleanField(default=False, editable=False) refused = models.BooleanField(default=False, editable=False) on_site = CurrentSiteManager() objects = ScreeningManager() history = HistoricalRecords(inherit=True) class Meta: abstract = True
class MoccaRegisterContact(SiteModelMixin, BaseUuidModel): mocca_register = models.ForeignKey(MoccaRegister, on_delete=models.PROTECT) report_datetime = models.DateTimeField(default=get_utcnow) answered = models.CharField(max_length=15, choices=YES_NO, null=True, blank=False) respondent = models.CharField(max_length=15, choices=RESPONDENT_CHOICES, default=NOT_APPLICABLE) survival_status = models.CharField( max_length=15, choices=ALIVE_DEAD_UNKNOWN_NA, default=NOT_APPLICABLE, ) death_date = models.DateField(verbose_name="Date of death", null=True, blank=True) willing_to_attend = models.CharField(max_length=15, choices=YES_NO_UNSURE_NA, default=NOT_APPLICABLE) icc = models.CharField( verbose_name= "Does the patient currently receive regular integrated care", max_length=25, choices=YES_NO_UNSURE_NA, null=True, blank=False, help_text="Either at this facility or elsewhere", ) next_appt_date = models.DateField(verbose_name="Next Appt.", null=True, blank=True) call_again = models.CharField(verbose_name="Call again?", max_length=15, choices=YES_NO) comment = EncryptedTextField(verbose_name="Note", null=True, blank=True) on_site = CurrentSiteManager() objects = Manager() history = HistoricalRecords() def __str__(self): return str(self.mocca_register) def natural_key(self): return (self.mocca_register, ) natural_key.dependencies = [ "sites.Site", "mocca_screening.MoccaRegister", ] class Meta: verbose_name = "MOCCA Patient Register Contact" verbose_name_plural = "MOCCA Patient Register Contacts" ordering = ["report_datetime"]
class RegisteredSubject(UniqueSubjectIdentifierModelMixin, SiteModelMixin, edc_models.BaseUuidModel): """A model mixin for the RegisteredSubject model (only).""" # may not be available when instance created (e.g. infants prior to birth # report) first_name = FirstnameField(null=True) # may not be available when instance created (e.g. infants or household # subject before consent) last_name = LastnameField(verbose_name="Last name", null=True) # may not be available when instance created (e.g. infants) initials = EncryptedCharField( validators=[ RegexValidator( regex=r"^[A-Z]{2,3}$", message=("Ensure initials consist of letters " "only in upper case, no spaces."), ) ], null=True, ) dob = models.DateField( verbose_name=_("Date of birth"), null=True, blank=False, help_text=_("Format is YYYY-MM-DD"), ) is_dob_estimated = IsDateEstimatedField( verbose_name=_("Is date of birth estimated?"), null=True, blank=False) gender = models.CharField(verbose_name="Gender", max_length=1, choices=GENDER, null=True, blank=False) subject_consent_id = models.CharField(max_length=100, null=True, blank=True) registration_identifier = models.CharField(max_length=36, null=True, blank=True) sid = models.CharField(verbose_name="SID", max_length=15, null=True, blank=True) subject_type = models.CharField(max_length=25, null=True, blank=True) relative_identifier = models.CharField( verbose_name="Identifier of immediate relation", max_length=36, null=True, blank=True, help_text= "For example, mother's identifier, if available / appropriate", ) identity = IdentityField(null=True, blank=True) identity_type = IdentityTypeField(null=True, blank=True) screening_identifier = models.CharField(max_length=36, null=True, blank=True) screening_datetime = models.DateTimeField(null=True, blank=True) screening_age_in_years = models.IntegerField(null=True, blank=True) registration_datetime = models.DateTimeField(null=True, blank=True) # For simplicity, if going straight from screen to rando, # update both registration date and randomization date randomization_datetime = models.DateTimeField(null=True, blank=True) registration_status = models.CharField(verbose_name="Registration status", max_length=25, null=True, blank=True) consent_datetime = models.DateTimeField(null=True, blank=True) comment = models.TextField(verbose_name="Comment", max_length=250, null=True, blank=True) additional_key = models.CharField( max_length=36, verbose_name="-", editable=False, default=None, null=True, help_text=( "A uuid (or some other text value) to be added to bypass the " "unique constraint of just firstname, initials, and dob." "The default constraint proves limiting since the source " "model usually has some other attribute in additional to " "first_name, initials and dob which is not captured in " "this model"), ) dm_comment = models.CharField( verbose_name="Data Management comment", max_length=150, null=True, editable=False, ) randomization_list_model = models.CharField(max_length=150, null=True) on_site = CurrentSiteManager() history = edc_models.HistoricalRecords() objects = RegisteredSubjectManager() def save(self, *args, **kwargs): if self.identity: self.additional_key = None self.set_uuid_as_subject_identifier_if_none() self.raise_on_duplicate("subject_identifier") self.raise_on_duplicate("identity") self.raise_on_changed_subject_identifier() super().save(*args, **kwargs) def natural_key(self): return tuple(self.subject_identifier_as_pk) def __str__(self): return self.masked_subject_identifier natural_key.dependencies = ["sites.Site"] def update_subject_identifier_on_save(self): """Overridden to not set the subject identifier on save.""" if not self.subject_identifier: self.subject_identifier = self.subject_identifier_as_pk.hex elif re.match(UUID_PATTERN, self.subject_identifier): pass return self.subject_identifier def make_new_identifier(self): return self.subject_identifier_as_pk.hex @property def masked_subject_identifier(self): """Returns the subject identifier, if set, otherwise the string '<identifier not set>'. """ if not self.subject_identifier_is_set: return "<identifier not set>" return self.subject_identifier @property def subject_identifier_is_set(self): """Returns True if subject identifier has been set to a subject identifier; that is, no longer the default UUID. """ is_set = True try: obj = self.__class__.objects.get(pk=self.id) except ObjectDoesNotExist: is_set = False else: if re.match(UUID_PATTERN, obj.subject_identifier): return False return is_set def raise_on_changed_subject_identifier(self): """Raises an exception if there is an attempt to change the subject identifier for an existing instance if the subject identifier is already set. """ if self.id and self.subject_identifier_is_set: with transaction.atomic(): obj = self.__class__.objects.get(pk=self.id) if obj.subject_identifier != self.subject_identifier_as_pk.hex: if self.subject_identifier != obj.subject_identifier: raise RegisteredSubjectError( "Subject identifier cannot be changed for " "existing registered subject. " f"Got {self.subject_identifier} <> {obj.subject_identifier}." ) def raise_on_duplicate(self, attrname): """Checks if the subject identifier (or other attr) is in use, for new and existing instances. """ if getattr(self, attrname): with transaction.atomic(): error_msg = ( f"Cannot {{action}} registered subject with a duplicate " f"'{attrname}'. Got {getattr(self, attrname)}.") try: obj = self.__class__.objects.exclude(**{ "pk": self.pk } if self.id else {}).get( **{attrname: getattr(self, attrname)}) if not self.id: raise RegisteredSubjectError( error_msg.format(action="insert")) elif self.subject_identifier_is_set and obj.id != self.id: raise RegisteredSubjectError( error_msg.format(action="update")) else: raise RegisteredSubjectError( error_msg.format(action="update")) except ObjectDoesNotExist: pass def set_uuid_as_subject_identifier_if_none(self): """Inserts a random uuid as a dummy identifier for a new instance. Model uses subject_identifier_as_pk as a natural key for serialization/deserialization. Value must not change once set. """ if not self.subject_identifier: self.subject_identifier = self.subject_identifier_as_pk.hex class Meta(edc_models.BaseUuidModel.Meta): verbose_name = "Registered Subject" ordering = ["subject_identifier"] unique_together = ("first_name", "dob", "initials", "additional_key") indexes = [ models.Index( fields=["first_name", "dob", "initials", "additional_key"]), models.Index(fields=[ "identity", "subject_identifier", "screening_identifier" ]), ] permissions = ( ("display_firstname", "Can display first name"), ("display_lastname", "Can display last name"), ("display_dob", "Can display DOB"), ("display_identity", "Can display identity number"), ("display_initials", "Can display initials"), )
class Rx( NonUniqueSubjectIdentifierFieldMixin, SiteModelMixin, ActionModelMixin, SearchSlugModelMixin, edc_models.BaseUuidModel, ): action_name = PRESCRIPTION_ACTION action_identifier = models.CharField(max_length=50, unique=True, null=True) registered_subject = models.ForeignKey( Subject, verbose_name="Subject Identifier", on_delete=PROTECT, null=True, blank=False, ) report_datetime = models.DateTimeField(default=get_utcnow) rx_date = models.DateField(verbose_name="Date RX written", default=get_utcnow) rx_expiration_date = models.DateField( verbose_name="Date RX expires", null=True, blank=True, help_text= "Leave blank. Will be filled when end of study report is submitted", ) status = models.CharField(max_length=25, default=NEW, choices=PRESCRIPTION_STATUS) medication = models.ForeignKey(Medication, on_delete=PROTECT, null=True) refill = models.IntegerField( null=True, blank=True, help_text="Number of times this prescription may be refilled", ) rando_sid = models.CharField(max_length=25, null=True, blank=True) randomizer_name = models.CharField(max_length=25, null=True, blank=True) weight_in_kgs = models.DecimalField(max_digits=6, decimal_places=1, null=True, blank=True) clinician_initials = models.CharField(max_length=3, null=True) notes = models.TextField( max_length=250, null=True, blank=True, help_text="Private notes for pharmacist only", ) on_site = CurrentSiteManager() def __str__(self): return ( f"{self.medication} " f"{self.registered_subject.subject_identifier} {self.registered_subject.initials} " f"{formatted_age(born=self.registered_subject.dob, reference_dt=get_utcnow())} " f"{self.registered_subject.gender} " f"Written: {self.rx_date}") def save(self, *args, **kwargs): self.registered_subject = RegisteredSubject.objects.get( subject_identifier=self.subject_identifier) if self.randomizer_name: randomizer = site_randomizers.get(self.randomizer_name) self.rando_sid = (randomizer.model_cls().objects.get( subject_identifier=self.subject_identifier).sid) super().save(*args, **kwargs) class Meta(edc_models.BaseUuidModel.Meta): verbose_name = "Prescription" verbose_name_plural = "Prescriptions"
class Box(SearchSlugModelMixin, VerifyBoxModelMixin, SiteModelMixin, edc_models.BaseUuidModel): search_slug_fields = [ "box_identifier", "human_readable_identifier", "name" ] box_identifier = models.CharField(max_length=25, editable=False, unique=True) name = models.CharField(max_length=25, null=True, blank=True) box_datetime = models.DateTimeField(default=timezone.now) box_type = models.ForeignKey(BoxType, on_delete=PROTECT) category = models.CharField(max_length=25, default=TESTING, choices=BOX_CATEGORY) category_other = models.CharField(max_length=25, null=True, blank=True) specimen_types = models.CharField( max_length=25, help_text=("List of specimen types in this box. Use two-digit numeric " "codes separated by commas."), ) status = models.CharField(max_length=15, default=OPEN, choices=STATUS) accept_primary = models.BooleanField( default=False, help_text="Tick to allow 'primary' specimens to be added to this box", ) comment = models.TextField(null=True, blank=True) on_site = CurrentSiteManager() objects = BoxManager() history = edc_models.HistoricalRecords() def save(self, *args, **kwargs): if not self.box_identifier: identifier = BoxIdentifier() self.box_identifier = identifier.identifier if not self.name: self.name = self.box_identifier self.update_verified() super().save(*args, **kwargs) def __str__(self): return self.name def natural_key(self): return (self.box_identifier, ) natural_key.dependencies = ["edc_lab.boxtype", "sites.Site"] @property def count(self): return self.boxitem_set.all().count() @property def items(self): return self.boxitem_set.all().order_by("position") @property def human_readable_identifier(self): x = self.box_identifier return "{}-{}-{}".format(x[0:4], x[4:8], x[8:12]) @property def next_position(self): """Returns an integer or None.""" last_obj = self.boxitem_set.all().order_by("position").last() if not last_obj: next_position = 1 else: next_position = last_obj.position + 1 if next_position > self.box_type.total: raise BoxIsFullError( f"Box is full. Box {self.human_readable_identifier} has " f"{self.box_type.total} specimens.") return next_position @property def max_position(self): return class Meta(edc_models.BaseUuidModel.Meta): verbose_name = "Box" ordering = ("-box_datetime", ) verbose_name_plural = "Boxes"
class ConsentModelMixin(VerificationFieldsMixin, models.Model): """Mixin for a Consent model class such as SubjectConsent. Declare with edc_identifier's NonUniqueSubjectIdentifierModelMixin """ consent_helper_cls = ConsentHelper consent_datetime = models.DateTimeField( verbose_name="Consent date and time", validators=[datetime_not_before_study_start, datetime_not_future], ) report_datetime = models.DateTimeField(null=True, editable=False) version = models.CharField( verbose_name="Consent version", max_length=10, help_text="See 'Consent Type' for consent versions by period.", editable=False, ) updates_versions = models.BooleanField(default=False) sid = models.CharField( verbose_name="SID", max_length=15, null=True, blank=True, editable=False, help_text="Used for randomization against a prepared rando-list.", ) comment = EncryptedTextField( verbose_name="Comment", max_length=250, blank=True, null=True ) dm_comment = models.CharField( verbose_name="Data Management comment", max_length=150, null=True, editable=False, help_text="see also edc.data manager.", ) consent_identifier = models.UUIDField( default=uuid4, editable=False, help_text="A unique identifier for this consent instance", ) objects = ObjectConsentManager() consent = ConsentManager() on_site = CurrentSiteManager() def __str__(self): return f"{self.subject_identifier} v{self.version}" def natural_key(self): return (self.subject_identifier_as_pk,) def save(self, *args, **kwargs): self.report_datetime = self.consent_datetime consent_helper = self.consent_helper_cls( model_cls=self.__class__, update_previous=True, **self.__dict__ ) self.version = consent_helper.version self.updates_versions = True if consent_helper.updates_versions else False super().save(*args, **kwargs) @property def age_at_consent(self): """Returns a relativedelta. """ return age(self.dob, self.consent_datetime) @property def formatted_age_at_consent(self): """Returns a string representation. """ return formatted_age(self.dob, self.consent_datetime) class Meta: abstract = True consent_group = None get_latest_by = "consent_datetime" unique_together = ( ("first_name", "dob", "initials", "version"), ("subject_identifier", "version"), ) ordering = ("created",) indexes = [ models.Index( fields=[ "subject_identifier", "first_name", "dob", "initials", "version", ] ) ]
class DataQuery(ActionModelMixin, SiteModelMixin, BaseUuidModel): tracking_identifier_prefix = "DQ" action_name = DATA_QUERY_ACTION report_datetime = models.DateTimeField(verbose_name="Query date", default=get_utcnow) subject_identifier = models.CharField(max_length=50, null=True, editable=False) title = models.CharField(max_length=150, null=True, blank=False) sender = models.ForeignKey( DataManagerUser, related_name="+", on_delete=PROTECT, verbose_name="Query raised by", help_text="Select a name from the list", ) recipients = models.ManyToManyField( QueryUser, related_name="+", verbose_name="Sent to", help_text= ("Select any additional recipients. Users in the `Site Data Manager` " "group are automatically included."), blank=True, ) registered_subject = models.ForeignKey( QuerySubject, verbose_name="Subject Identifier", on_delete=PROTECT, null=True, blank=False, ) query_priority = models.CharField(verbose_name="Priority", max_length=25, choices=QUERY_PRIORITY, default=NORMAL) visit_schedule = models.ForeignKey( QueryVisitSchedule, verbose_name="Visit", on_delete=PROTECT, null=True, blank=True, ) visit_code_sequence = models.IntegerField( verbose_name="Visit code sequence", default=0, validators=[MinValueValidator(0), MaxValueValidator(25)], null=True, blank=True, help_text=("Defaults to '0'. For example, when combined with the " "visit code `1000` would make `1000.0`."), ) timepoint = models.DecimalField(null=True, decimal_places=1, max_digits=6) data_dictionaries = models.ManyToManyField( DataDictionary, verbose_name="CRF question(s)", blank=True, help_text="select all that apply", ) requisition_panel = models.ForeignKey( RequisitionPanel, verbose_name="Are responses linked to a requisition? If so, which", related_name="+", on_delete=PROTECT, null=True, blank=True, help_text="Requisition will be expected on day of visit.", ) query_text = models.TextField(help_text="Describe the query in detail.") site_resolved_datetime = models.DateTimeField( verbose_name="Site resolved on", null=True, blank=True) site_response_text = models.TextField(null=True, blank=True) site_response_status = models.CharField(verbose_name="Site status", max_length=25, choices=RESPONSE_STATUS, default=NEW) status = models.CharField(verbose_name="DM status", max_length=25, choices=DM_STATUS, default=OPEN) dm_user = models.ForeignKey( DataManagerUser, verbose_name="DM resolved by", related_name="dm_user", on_delete=PROTECT, null=True, blank=True, help_text="select a name from the list", ) resolved_datetime = models.DateTimeField(verbose_name="DM resolved on", null=True, blank=True) auto_resolved = models.BooleanField(default=False) plan_of_action = models.TextField( null=True, blank=True, help_text="If required, provide a plan of action") missed_visit = models.BooleanField( verbose_name="Visit reported as missed", default=False, help_text="If visit/timepoint was missed, data is not expected", ) locked = models.BooleanField( default=False, help_text="If locked, this query will NEVER be reopened.", ) locked_reason = models.TextField( verbose_name="Reason query locked", null=True, blank=True, help_text="If required, the reason the query cannot be resolved.", ) rule_generated = models.BooleanField( default=False, help_text="This query was auto-generated by a query rule.") rule_reference = models.CharField(verbose_name="Query rule reference", max_length=150, null=True, default=uuid4) on_site = CurrentSiteManager() objects = models.Manager() def __str__(self): return f"{self.action_identifier[-9:]}, {self.status}" def save(self, *args, **kwargs): self.subject_identifier = self.registered_subject.subject_identifier self.try_to_resolve_auto_generated_query() super().save(*args, **kwargs) def try_to_resolve_auto_generated_query(self): if (not self.dm_resolved and self.rule_generated and self.site_response_status == RESOLVED and self.site_response_text and self.site_response_text.strip() == AUTO_RESOLVED): self.status = CLOSED self.resolved_datetime = get_utcnow() self.dm_user = self.sender @property def dm_resolved(self): return self.status in [CLOSED, CLOSED_WITH_ACTION] @property def site_resolved(self): return self.site_response_status == RESOLVED def form_and_numbers_to_string(self): ret = [] model_verbose_names = [ o.model_verbose_name for o in self.data_dictionaries.all().order_by("model") ] model_verbose_names = list(set(model_verbose_names)) for model_verbose_name in model_verbose_names: numbers = [ str(o.number) for o in self.data_dictionaries.filter( model_verbose_name=model_verbose_name).order_by("number") ] numbers = ", ".join(numbers) ret.append((model_verbose_name, numbers)) return ret def get_action_item_display_name(self): pass def get_action_item_reason(self): try: url = url_names.get("subject_dashboard_url") except InvalidUrlName: visit_href = "#" else: try: visit = get_subject_visit_model_cls().objects.get( subject_identifier=self.registered_subject. subject_identifier, visit_schedule_name=self.visit_schedule. visit_schedule_name, schedule_name=self.visit_schedule.schedule_name, visit_code=self.visit_schedule.visit_code, visit_code_sequence=self.visit_code_sequence, ) except ObjectDoesNotExist: visit_href = "#" except AttributeError as e: if ("visit_schedule_name" not in str(e) and "schedule_name" not in str(e) and "visit_code" not in str(e)): raise visit_href = "#" else: visit_href = reverse( url, kwargs=dict( appointment=str(visit.appointment.id), subject_identifier=self.registered_subject. subject_identifier, ), ) template_name = (f"edc_data_manager/bootstrap{settings.EDC_BOOTSTRAP}/" f"columns/query_text.html") context = dict( form_and_numbers=self.form_and_numbers_to_string(), query_priority=self.query_priority, query_priority_display=self.get_query_priority_display(), query_text=self.query_text, questions=self.data_dictionaries.all().order_by("model", "number"), report_datetime=self.report_datetime, requisition_panel=self.requisition_panel, resolved_datetime=self.resolved_datetime, site_resolved_datetime=self.site_resolved_datetime, site_response_text=self.site_response_text, site_response_status=self.get_site_response_status_display(), status=self.status, dm_user=self.dm_user, title=self.title, visit_schedule=self.visit_schedule, visit_href=visit_href, ) return render_to_string(template_name, context=context) @property def model_names(self): model_names = list( set([dd.model for dd in self.data_dictionaries.all()])) return "|".join(model_names) class Meta(BaseUuidModel.Meta): verbose_name = "Data Query" verbose_name_plural = "Data Queries" unique_together = [ "registered_subject", "rule_reference", "visit_schedule" ] indexes = [ models.Index(fields=[ "subject_identifier", "action_identifier", "title", "rule_reference", "registered_subject", "visit_schedule", ]) ]
class SubjectScreening( NonUniqueSubjectIdentifierModelMixin, MapitioAdditionalIdentifiersModelMixin, SearchSlugModelMixin, ScreeningMethodsModeMixin, ScreeningFieldsModeMixin, PersonalFieldsMixin, SiteModelMixin, edc_models.BaseUuidModel, ): screening_identifier = models.CharField( verbose_name="Enrollment ID", max_length=50, blank=True, unique=True, editable=False, ) initials = EncryptedCharField( validators=[ RegexValidator("[A-Z]{1,3}", "Invalid format"), MinLengthValidator(2), MaxLengthValidator(3), ], help_text="Use UPPERCASE letters only. May be 2 or 3 letters.", blank=False, ) report_datetime = models.DateTimeField( verbose_name="Report Date and Time", default=get_utcnow, help_text="Date and time of this report.", ) gender = models.CharField( verbose_name="Gender", choices=GENDER, max_length=1, null=True, blank=False, ) age_in_years = models.IntegerField( validators=[MinValueValidator(0), MaxValueValidator(110)], ) dob = models.DateField(verbose_name="Date of birth", null=True, blank=False) is_dob_estimated = edc_models.IsDateEstimatedField( verbose_name="Is date of birth estimated?", null=True, blank=False ) confirm_hospital_identifier = EncryptedCharField( verbose_name="Confirm HMS Identifier", null=True, help_text="Retype the Hindu Mandal Hospital Identifier", ) confirm_ctc_identifier = EncryptedCharField( verbose_name="Confirm CTC Identifier", null=True, blank=True, ) clinic_registration_date = models.DateField( verbose_name="Date patient was <u>first</u> enrolled to this clinic", validators=[date_is_past, date_is_not_now], ) last_clinic_date = models.DateField( verbose_name="Date patient was <u>last</u> seen at this clinic", validators=[date_is_past, date_is_not_now], help_text="Date last seen according to information on the patient chart.", ) # not used, keep for compatability screening_consent = models.CharField( verbose_name=( "Has the subject given his/her verbal consent " "to be screened for the Mapitio trial?" ), max_length=15, choices=YES_NO_NA, default=NOT_APPLICABLE, ) # not used, keep for compatability selection_method = models.CharField( verbose_name="How was the patient selected for screening?", max_length=25, choices=SELECTION_METHOD, default=INTEGRATED_CLINIC, ) # not used, keep for compatability clinic_type = models.CharField( verbose_name="From which type of clinic was the patient selected", max_length=25, choices=CLINIC_CHOICES, default=INTEGRATED_CLINIC, ) on_site = CurrentSiteManager() objects = SubjectScreeningModelManager() def save(self, *args, **kwargs): """Screening Identifier is always allocated. """ self.screening_identifier = self.hospital_identifier check_eligible_final(self) super().save(*args, **kwargs) class Meta: verbose_name = "Enrollment" verbose_name_plural = "Enrollment" unique_together = ( ("first_name", "dob", "initials", "last_name"), ("hospital_identifier", "ctc_identifier"), )
class MoccaRegister(SiteModelMixin, BaseUuidModel): report_datetime = models.DateTimeField(default=get_utcnow) screening_identifier = models.CharField( verbose_name="MOCCA (ext) screening identifier", max_length=15, null=True, ) mocca_screening_identifier = models.CharField( verbose_name="MOCCA (original) screening identifier", max_length=15, null=True, blank=True, help_text="If known", ) mocca_study_identifier = models.CharField( verbose_name="MOCCA (original) study identifier", max_length=25, validators=[ RegexValidator( r"0[0-9]{1}\-0[0-9]{3}|[0-9]{6}", "Invalid format. Expected 12-3456 for UG, 123456 for TZ", ) ], help_text="Format must match original identifier. e.g. 12-3456 for UG, 123456 for TZ", ) mocca_country = models.CharField( max_length=25, choices=(("uganda", "Uganda"), ("tanzania", "Tanzania")) ) mocca_site = models.ForeignKey( MoccaOriginalSites, on_delete=models.PROTECT, limit_choices_to=get_mocca_site_limited_to, ) first_name = FirstnameField(null=True) last_name = LastnameField(null=True) initials = EncryptedCharField( validators=[ RegexValidator("[A-Z]{1,3}", "Invalid format"), MinLengthValidator(2), MaxLengthValidator(3), ], help_text="Use UPPERCASE letters only. May be 2 or 3 letters.", null=True, blank=False, ) gender = models.CharField(max_length=10, choices=GENDER, null=True, blank=False) age_in_years = models.IntegerField( validators=[MinValueValidator(18), MaxValueValidator(110)], null=True, blank=False, ) birth_year = models.IntegerField( validators=[MinValueValidator(1900), MaxValueValidator(2002)], null=True, blank=False, ) dob = models.DateField(null=True, blank=True) survival_status = models.CharField( max_length=25, choices=ALIVE_DEAD_UNKNOWN, default=UNKNOWN ) contact_attempts = models.IntegerField(default=0, help_text="auto-updated") call = models.CharField(verbose_name="Call?", max_length=15, choices=YES_NO, default=YES) subject_present = models.CharField( verbose_name="Patient is present. Screen now instead of calling?", max_length=15, choices=YES_NO, default=NO, help_text="Only select 'yes' if the patient is present in the clinic now.", ) date_last_called = models.DateField(null=True, help_text="auto-updated") next_appt_date = models.DateField( verbose_name="Appt", null=True, blank=True, help_text="auto-updated" ) notes = EncryptedTextField(verbose_name="General notes", null=True, blank=True) tel_one = EncryptedCharField("Tel/Mobile(1)", max_length=15, null=True) tel_two = EncryptedCharField("Tel/Mobile(2)", max_length=15, null=True) tel_three = EncryptedCharField("Tel/Mobile(3)", max_length=15, null=True) best_tel = models.CharField( verbose_name="Prefered Telephone / Mobile", max_length=15, choices=TEL_CHOICES, null=True, blank=True, help_text="If any, select the best telephone/mobile from above", ) on_site = CurrentSiteManager() objects = Manager() history = HistoricalRecords() def __str__(self): return ( f"{self.mocca_study_identifier} {self.initials} {self.age_in_years} {self.gender}" ) def save(self, *args, **kwargs): if self.screening_identifier: self.call = NO super().save(*args, **kwargs) def natural_key(self): return (self.mocca_study_identifier,) natural_key.dependencies = [ "sites.Site", "mocca_lists.MoccaOriginalSites", ] class Meta(BaseUuidModel.Meta): verbose_name = "MOCCA Patient Register" verbose_name_plural = "MOCCA Patient Register" ordering = ["mocca_country", "mocca_site"] indexes = [ Index(fields=["mocca_country", "mocca_site"]), Index(fields=["mocca_study_identifier", "initials", "gender"]), ] constraints = [ UniqueConstraint( fields=["mocca_screening_identifier"], name="unique_mocca_screening_identifier", ), UniqueConstraint( fields=["mocca_study_identifier"], name="unique_mocca_study_identifier" ), ]