Newer
Older

Carlos Vega
committed
from dateutil.relativedelta import relativedelta
from django.db.models.signals import post_save
from django.dispatch import receiver

Carlos Vega
committed
from django.db import transaction
from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES_CONTROL

Carlos Vega
committed
from web.models import Study
import logging
logger = logging.getLogger(__name__)
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
)

Carlos Vega
committed
@property
def next_visit(self):
return Visit.objects.filter(subject=self.subject, visit_number=self.visit_number+1).order_by('datetime_begin','datetime_end').first()
@property
def future_visits(self):
return Visit.objects.filter(subject=self.subject).filter(visit_number__gt=self.visit_number).order_by('datetime_begin','datetime_end')

Carlos Vega
committed
start = self.datetime_begin.strftime('%Y-%m-%d')
end = self.datetime_end.strftime('%Y-%m-%d')
finished = '✓' if self.is_finished else ''
return f'#{self.visit_number:02} | {start} / {end} | {self.subject.subject.first_name} {self.subject.subject.last_name} | {finished}'
def mark_as_finished(self):
self.is_finished = True
self.save()
Piotr Gawron
committed
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 self.subject.endpoint_reached:
create_follow_up = False
Piotr Gawron
committed
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

Carlos Vega
committed
study = self.subject.study
Piotr Gawron
committed
if self.subject.type == SUBJECT_TYPE_CHOICES_CONTROL:

Carlos Vega
committed
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
Piotr Gawron
committed
logger.warning('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)

Carlos Vega
committed
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def unfinish(self):
#if ValueError messages are changed, change test/view/test_visit.py
#check visit is indeed finished
if not self.is_finished:
raise ValueError('The visit is not finished.')
#check that there is only one future visit
future_visits = self.future_visits
if len(future_visits) > 1:
raise ValueError("Visit can't be unfinished. Only visits with one inmediate future visit (without appointments) can be unfinished.")
elif len(future_visits) == 1:
#check that the future visit has no appointments
#remove visit if it has no appointments
next_visit = future_visits[0]
if len(next_visit.appointment_set.all()) == 0:
next_visit.delete()
else:
raise ValueError("Visit can't be unfinished. The next visit has appointments.")
else:
#this can happen when there is no auto follow up visit
pass
self.is_finished = False
self.save()
return

Carlos Vega
committed
def check_visit_number(sender, instance, created, **kwargs):
# no other solution to ensure the visit_number is in chronological order than to sort the whole list if there are future visits
if visit.subject is not None: #not sure if select_for_update has an effect, the tests work as well without it

Carlos Vega
committed
#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','id')

Carlos Vega
committed
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

Carlos Vega
committed
else:
#if visits are modified, then, check everything
visits = Visit.objects.select_for_update().filter(subject=visit.subject).order_by('datetime_begin','id')

Carlos Vega
committed
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)