# coding=utf-8 import datetime from dateutil.relativedelta import relativedelta from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.db import transaction from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES_CONTROL from web.models import Study import logging logger = logging.getLogger(__name__) class Visit(models.Model): class Meta: app_label = 'web' subject = models.ForeignKey("web.StudySubject", on_delete=models.CASCADE, verbose_name='Subject' ) datetime_begin = models.DateTimeField( verbose_name='Visit starts at' ) datetime_end = models.DateTimeField( verbose_name='Visit ends at' ) # Deadline before which all appointments need to be scheduled is_finished = models.BooleanField( verbose_name='Has ended', default=False ) post_mail_sent = models.BooleanField(choices=BOOL_CHOICES, verbose_name='Post mail sent', default=False ) appointment_types = models.ManyToManyField("web.AppointmentType", verbose_name='Requested appointments', blank=True, ) # this value is automatically computed by signal handled by # update_visit_number method visit_number = models.IntegerField( verbose_name='Visit number', default=1 ) def __unicode__(self): return "%s %s" % (self.subject.subject.first_name, self.subject.subject.last_name) def __str__(self): return "%s %s" % (self.subject.subject.first_name, self.subject.subject.last_name) def mark_as_finished(self): self.is_finished = True self.save() create_follow_up = True if self.subject.subject.dead: create_follow_up = False elif self.subject.resigned: create_follow_up = False elif self.subject.excluded: create_follow_up = False elif not self.subject.study.auto_create_follow_up: create_follow_up = False if create_follow_up: visit_started = Visit.objects.filter( subject=self.subject).filter(visit_number=1)[0].datetime_begin follow_up_number = Visit.objects.filter( subject=self.subject).count() + 1 study = self.subject.study if self.subject.type == SUBJECT_TYPE_CHOICES_CONTROL: args = {study.default_delta_time_for_follow_up_units: study.default_delta_time_for_control_follow_up} else: args = {study.default_delta_time_for_follow_up_units: study.default_delta_time_for_patient_follow_up} time_to_next_visit = relativedelta(**args) * (follow_up_number - 1) #calculated from first visit logger.warn('new visit: {} {} {}'.format(args, relativedelta(**args), time_to_next_visit)) Visit.objects.create( subject=self.subject, datetime_begin=visit_started + time_to_next_visit, datetime_end=visit_started + time_to_next_visit + relativedelta(months=study.default_visit_duration_in_months) ) @receiver(post_save, sender=Visit) def check_visit_number(sender, instance, created, **kwargs): # no other solution to ensure the visit_number is in cronological order than to sort the whole list if there are future visits visit = instance if visit.subject is not None: #not sure if select_for_update has an effect, the tests work as well without it #new visit, sort only future visit respect to the new one if created: visits_before = Visit.objects.select_for_update().filter(subject=visit.subject).filter(datetime_begin__lt=visit.datetime_begin).count() # we need to sort the future visits respect to the new one, if any visits = Visit.objects.select_for_update().filter(subject=visit.subject).filter(datetime_begin__gte=visit.datetime_begin).order_by('datetime_begin','datetime_end') with transaction.atomic(): #not sure if it has an effect, the tests work as well without it for i, v in enumerate(visits): expected_visit_number = (visits_before + i + 1) if v.visit_number != expected_visit_number: Visit.objects.filter(id=v.id).update(visit_number=expected_visit_number) # does not rise post_save, we avoid recursion if v.id == visit.id: #if the iteration visit is the same that the instance that produced the signal call #this ensures that the upper saved object is also updated, otherwise, refresh_from_db should be called visit.visit_number = v.visit_number else: #if visits are modified, then, check everything visits = Visit.objects.select_for_update().filter(subject=visit.subject).order_by('datetime_begin','datetime_end') with transaction.atomic(): for i, v in enumerate(visits): expected_visit_number = (i+1) if v.visit_number != expected_visit_number: #update only those with wrong numbers Visit.objects.filter(id=v.id).update(visit_number=expected_visit_number)