Project

General

Profile

Download (20.8 KB) Statistics
| Branch: | Tag: | Revision:
96f613c6 Benjamin Dauvergne
# -*- coding: utf-8 -*-
ba34dc24 Jérôme Schneider
76974b6f Benjamin Dauvergne
from datetime import datetime, date, timedelta
from copy import copy
ba34dc24 Jérôme Schneider
from django.utils.translation import ugettext_lazy as _
76974b6f Benjamin Dauvergne
from django.contrib.auth.models import User
ba34dc24 Jérôme Schneider
from django.db import models
4ce756c8 Benjamin Dauvergne
from django import forms
ba34dc24 Jérôme Schneider
11984ab3 Jérôme Schneider
from calebasse.agenda import managers
4ce756c8 Benjamin Dauvergne
from calebasse.utils import weeks_since_epoch, weekday_ranks
7a0a30d7 Benjamin Dauvergne
from interval import Interval
ba34dc24 Jérôme Schneider
__all__ = (
'EventType',
'Event',
76974b6f Benjamin Dauvergne
'EventWithAct',
ba34dc24 Jérôme Schneider
)

class EventType(models.Model):
'''
Simple ``Event`` classifcation.
'''
class Meta:
e39efb4a Jérôme Schneider
verbose_name = u'Type d\'événement'
verbose_name_plural = u'Types d\'événement'
ba34dc24 Jérôme Schneider
def __unicode__(self):
return self.label

e39efb4a Jérôme Schneider
label = models.CharField(_('label'), max_length=50)
68e9f8c1 Frédéric Péters
rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
e39efb4a Jérôme Schneider
84091f2c Jérôme Schneider
class Event(models.Model):
ba34dc24 Jérôme Schneider
'''
76974b6f Benjamin Dauvergne
Container model for general agenda events
ba34dc24 Jérôme Schneider
'''
e39efb4a Jérôme Schneider
objects = managers.EventManager()
ba34dc24 Jérôme Schneider
69e79c82 Jérôme Schneider
title = models.CharField(_('Title'), max_length=60, blank=True)
a334f1e6 Mikaël Ates
description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
69a9009e Jérôme Schneider
event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
76974b6f Benjamin Dauvergne
creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
ba34dc24 Jérôme Schneider
84091f2c Jérôme Schneider
services = models.ManyToManyField('ressources.Service',
null=True, blank=True, default=None)
e39efb4a Jérôme Schneider
participants = models.ManyToManyField('personnes.People',
null=True, blank=True, default=None)
1da8cee7 Benjamin Dauvergne
room = models.ForeignKey('ressources.Room', blank=True, null=True,
verbose_name=u'Salle')
e39efb4a Jérôme Schneider
5c93596e Benjamin Dauvergne
start_datetime = models.DateTimeField(_('Début'), db_index=True)
76974b6f Benjamin Dauvergne
end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
74102956 Benjamin Dauvergne
old_ev_id = models.CharField(max_length=8, blank=True, null=True)
old_rr_id = models.CharField(max_length=8, blank=True, null=True)
44482a62 Benjamin Dauvergne
# only used when there is no rr id
74102956 Benjamin Dauvergne
old_rs_id = models.CharField(max_length=8, blank=True, null=True)
a04356e2 Benjamin Dauvergne
# exception to is mutually exclusive with recurrence_periodicity
# an exception cannot be periodic
exception_to = models.ForeignKey('self', related_name='exceptions',
blank=True, null=True,
verbose_name=u'Exception à')
exception_date = models.DateField(blank=True, null=True,
e9fb7b88 Benjamin Dauvergne
verbose_name=u'Reporté du', db_index=True)
a04356e2 Benjamin Dauvergne
# canceled can only be used with exception to
e9fb7b88 Benjamin Dauvergne
canceled = models.BooleanField(_('Annulé'), db_index=True)
76974b6f Benjamin Dauvergne
PERIODS = (
(1, u'Toutes les semaines'),
(2, u'Une semaine sur deux'),
(3, u'Une semaine sur trois'),
(4, 'Une semaine sur quatre'),
(5, 'Une semaine sur cinq')
)
OFFSET = range(0,4)
4ce756c8 Benjamin Dauvergne
PERIODICITIES = (
(1, u'Toutes les semaines'),
(2, u'Une semaine sur deux'),
(3, u'Une semaine sur trois'),
(4, u'Une semaine sur quatre'),
(5, u'Une semaine sur cinq'),
(6, u'La première semaine du mois'),
(7, u'La deuxième semaine du mois'),
(8, u'La troisième semaine du mois'),
(9, u'La quatrième semaine du mois'),
(10, u'La dernière semaine du mois'),
(11, u'Les semaines paires'),
(12, u'Les semaines impaires')
)
WEEK_RANKS = (
(0, u'La première semaine du mois'),
(1, u'La deuxième semaine du mois'),
(2, u'La troisième semaine du mois'),
(3, u'La quatrième semaine du mois'),
(4, u'La dernière semaine du mois')
)
PARITIES = (
(0, u'Les semaines paires'),
(1, u'Les semaines impaires')
)
recurrence_periodicity = models.PositiveIntegerField(
choices=PERIODICITIES,
verbose_name=u"Périodicité",
default=None,
blank=True,
e9fb7b88 Benjamin Dauvergne
null=True,
db_index=True)
recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
76974b6f Benjamin Dauvergne
recurrence_week_offset = models.PositiveIntegerField(
choices=zip(OFFSET, OFFSET),
verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
default=0,
db_index=True)
recurrence_week_period = models.PositiveIntegerField(
choices=PERIODS,
verbose_name=u"Période en semaines",
default=None,
blank=True,
null=True,
db_index=True)
4ce756c8 Benjamin Dauvergne
recurrence_week_rank = models.PositiveIntegerField(
verbose_name=u"Rang de la semaine dans le mois",
choices=WEEK_RANKS,
e9fb7b88 Benjamin Dauvergne
blank=True, null=True, db_index=True)
4ce756c8 Benjamin Dauvergne
recurrence_week_parity = models.PositiveIntegerField(
choices=PARITIES,
verbose_name=u"Parité des semaines",
blank=True,
e9fb7b88 Benjamin Dauvergne
null=True,
db_index=True)
76974b6f Benjamin Dauvergne
recurrence_end_date = models.DateField(
verbose_name=_(u'Fin de la récurrence'),
blank=True, null=True,
db_index=True)

4ce756c8 Benjamin Dauvergne
PERIOD_LIST_TO_FIELDS = [(1, None, None),
(2, None, None),
(3, None, None),
(4, None, None),
(5, None, None),
(None, 0, None),
(None, 1, None),
(None, 2, None),
(None, 3, None),
(None, 4, None),
(None, None, 0),
(None, None, 1)
]

ba34dc24 Jérôme Schneider
class Meta:
e39efb4a Jérôme Schneider
verbose_name = u'Evénement'
verbose_name_plural = u'Evénements'
76974b6f Benjamin Dauvergne
ordering = ('start_datetime', 'end_datetime', 'title')
a04356e2 Benjamin Dauvergne
unique_together = (('exception_to', 'exception_date'),)
76974b6f Benjamin Dauvergne
def __init__(self, *args, **kwargs):
if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
super(Event, self).__init__(*args, **kwargs)

def clean(self):
'''Initialize recurrence fields if they are not.'''
f16de572 Benjamin Dauvergne
self.sanitize()
4ce756c8 Benjamin Dauvergne
if self.recurrence_periodicity:
f16de572 Benjamin Dauvergne
if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
4ce756c8 Benjamin Dauvergne
raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
if self.recurrence_week_parity is not None:
if self.start_datetime:
week = self.start_datetime.date().isocalendar()[1]
start_week_parity = week % 2
if start_week_parity != self.recurrence_week_parity:
raise forms.ValidationError(u'Le date de départ de la périodicité est en semaine {week}.'.format(week=week))
if self.recurrence_week_rank is not None and self.start_datetime:
start_week_ranks = weekday_ranks(self.start_datetime.date())
if self.recurrence_week_rank not in start_week_ranks:
raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
76974b6f Benjamin Dauvergne
f16de572 Benjamin Dauvergne
def sanitize(self):
if self.recurrence_periodicity:
l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
else:
l = None, None, None
self.recurrence_week_period = l[0]
self.recurrence_week_rank = l[1]
self.recurrence_week_parity = l[2]
if self.start_datetime:
if self.recurrence_periodicity:
self.recurrence_week_day = self.start_datetime.weekday()
if self.recurrence_week_period is not None:
self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period

76974b6f Benjamin Dauvergne
def timedelta(self):
'''Distance between start and end of the event'''
return self.end_datetime - self.start_datetime

a04356e2 Benjamin Dauvergne
def match_date(self, date):
if self.is_recurring():
# consider exceptions
exception = self.get_exceptions_dict().get(date)
if exception is not None:
return exception if exception.match_date(date) else None
if self.canceled:
return None
if date.weekday() != self.recurrence_week_day:
return None
if self.start_datetime.date() > date:
return None
if self.recurrence_end_date and self.recurrence_end_date < date:
return None
if self.recurrence_week_period is not None:
if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
return None
elif self.recurrence_week_parity is not None:
if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
return None
elif self.recurrence_week_rank is not None:
if self.recurrence_week_rank not in weekday_ranks(date):
return None
else:
raise NotImplemented
return self
else:
ced57915 Benjamin Dauvergne
return self if date == self.start_datetime.date() else None
a04356e2 Benjamin Dauvergne

b8a9f824 Benjamin Dauvergne
def today_occurrence(self, today=None, match=False, upgrade=True):
76974b6f Benjamin Dauvergne
'''For a recurring event compute the today 'Event'.

The computed event is the fake one that you cannot save to the database.
ba34dc24 Jérôme Schneider
'''
76974b6f Benjamin Dauvergne
today = today or date.today()
a04356e2 Benjamin Dauvergne
if self.canceled:
76974b6f Benjamin Dauvergne
return None
a04356e2 Benjamin Dauvergne
if match:
exception = self.get_exceptions_dict().get(today)
b16fae0e Benjamin Dauvergne
if exception:
if exception.start_datetime.date() == today:
return exception.today_occurrence(today)
else:
return None
4ce756c8 Benjamin Dauvergne
else:
a04356e2 Benjamin Dauvergne
exception_or_self = self.match_date(today)
if exception_or_self is None:
return None
if exception_or_self != self:
5c2d0ea2 Benjamin Dauvergne
return exception_or_self.today_occurrence(today)
b8a9f824 Benjamin Dauvergne
if self.event_type_id == 1 and type(self) != EventWithAct and upgrade:
self = self.eventwithact
59136255 Benjamin Dauvergne
if self.recurrence_periodicity is None:
return self
76974b6f Benjamin Dauvergne
start_datetime = datetime.combine(today, self.start_datetime.timetz())
end_datetime = start_datetime + self.timedelta()
event = copy(self)
a04356e2 Benjamin Dauvergne
event.exception_to = self
event.exception_date = today
76974b6f Benjamin Dauvergne
event.start_datetime = start_datetime
event.end_datetime = end_datetime
a04356e2 Benjamin Dauvergne
event.recurrence_periodicity = None
event.recurrence_week_offset = 0
76974b6f Benjamin Dauvergne
event.recurrence_week_period = None
a04356e2 Benjamin Dauvergne
event.recurrence_week_parity = None
event.recurrence_week_rank = None
event.recurrence_end_date = None
76974b6f Benjamin Dauvergne
event.parent = self
4ce756c8 Benjamin Dauvergne
# the returned event is "virtual", it must not be saved
a04356e2 Benjamin Dauvergne
old_save = event.save
old_participants = list(self.participants.all())
a334f1e6 Mikaël Ates
def save(*args, **kwargs):
a04356e2 Benjamin Dauvergne
event.id = None
86d19877 Benjamin Dauvergne
event.event_ptr_id = None
a04356e2 Benjamin Dauvergne
old_save(*args, **kwargs)
dbe774de Benjamin Dauvergne
if hasattr(self, 'exceptions_dict'):
self.exceptions_dict[event.start_datetime.date()] = event
a04356e2 Benjamin Dauvergne
event.participants = old_participants
76974b6f Benjamin Dauvergne
event.save = save
return event

def next_occurence(self, today=None):
'''Returns the next occurence after today.'''
today = today or date.today()
for occurence in self.all_occurences():
if occurence.start_datetime.date() > today:
return occurence

def is_recurring(self):
'''Is this event multiple ?'''
4ce756c8 Benjamin Dauvergne
return self.recurrence_periodicity is not None
76974b6f Benjamin Dauvergne
a04356e2 Benjamin Dauvergne
def get_exceptions_dict(self):
if not hasattr(self, 'exceptions_dict'):
self.exceptions_dict = dict()
508c3726 Benjamin Dauvergne
if self.exception_to_id is None:
b8a9f824 Benjamin Dauvergne
for exception in self.exceptions.all():
508c3726 Benjamin Dauvergne
self.exceptions_dict[exception.exception_date] = exception
a04356e2 Benjamin Dauvergne
return self.exceptions_dict

76974b6f Benjamin Dauvergne
def all_occurences(self, limit=90):
4ce756c8 Benjamin Dauvergne
'''Returns all occurences of this event as virtual Event objects
76974b6f Benjamin Dauvergne
limit - compute occurrences until limit days in the future

Default is to limit to 90 days.
ba34dc24 Jérôme Schneider
'''
4ce756c8 Benjamin Dauvergne
if self.recurrence_periodicity is not None:
76974b6f Benjamin Dauvergne
day = self.start_datetime.date()
max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
a04356e2 Benjamin Dauvergne
occurrences = []
4ce756c8 Benjamin Dauvergne
if self.recurrence_week_period is not None:
delta = timedelta(days=self.recurrence_week_period*7)
while day <= end_date:
790fd669 Benjamin Dauvergne
occurrence = self.today_occurrence(day, True)
a04356e2 Benjamin Dauvergne
if occurrence is not None:
occurrences.append(occurrence)
4ce756c8 Benjamin Dauvergne
day += delta
elif self.recurrence_week_parity is not None:
delta = timedelta(days=7)
while day <= end_date:
if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
790fd669 Benjamin Dauvergne
occurrence = self.today_occurrence(day, True)
a04356e2 Benjamin Dauvergne
if occurrence is not None:
occurrences.append(occurrence)
4ce756c8 Benjamin Dauvergne
day += delta
elif self.recurrence_week_rank is not None:
delta = timedelta(days=7)
while day <= end_date:
if self.recurrence_week_rank in weekday_ranks(day):
790fd669 Benjamin Dauvergne
occurrence = self.today_occurrence(day, True)
a04356e2 Benjamin Dauvergne
if occurrence is not None:
occurrences.append(occurrence)
4ce756c8 Benjamin Dauvergne
day += delta
a04356e2 Benjamin Dauvergne
for exception in self.exceptions.all():
if exception.exception_date != exception.start_datetime.date():
b8a9f824 Benjamin Dauvergne
occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
a04356e2 Benjamin Dauvergne
return sorted(occurrences, key=lambda o: o.start_datetime)
ba34dc24 Jérôme Schneider
else:
a04356e2 Benjamin Dauvergne
return [self]
ba34dc24 Jérôme Schneider
4ce756c8 Benjamin Dauvergne
def save(self, *args, **kwargs):
0acdd88a Benjamin Dauvergne
assert self.recurrence_periodicity is None or self.exception_to is None
12a8ef2b Benjamin Dauvergne
assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
assert self.start_datetime is not None
f16de572 Benjamin Dauvergne
self.sanitize() # init periodicity fields
4ce756c8 Benjamin Dauvergne
super(Event, self).save(*args, **kwargs)
5f3ce603 Benjamin Dauvergne
self.acts_cleaning()
4ce756c8 Benjamin Dauvergne
fcd786f5 Benjamin Dauvergne
def delete(self, *args, **kwargs):
a04356e2 Benjamin Dauvergne
self.canceled = True
5f3ce603 Benjamin Dauvergne
# save will clean acts
self.save(*args, **kwargs)

def acts_cleaning(self):
# list of occurences may have changed
from ..actes.models import Act
if self.exception_to:
# maybe a new exception, so look for parent acts with same date
# as exception date
acts = Act.objects.filter(models.Q(parent_event=self)
|models.Q(parent_event=self.exception_to,
date=self.exception_date))
else:
acts = Act.objects.filter(parent_event=self)
acts = acts.prefetch_related('actvalidationstate_set')
if acts:
eventwithact = self.eventwithact
for act in acts:
if act.is_new():
if self.match_date(act.date):
if self.canceled:
act.delete()
else:
eventwithact.update_act(act)
else:
act.delete()
fcd786f5 Benjamin Dauvergne
76974b6f Benjamin Dauvergne
def to_interval(self):
return Interval(self.start_datetime, self.end_datetime)
ba34dc24 Jérôme Schneider
76974b6f Benjamin Dauvergne
def is_event_absence(self):
return False
ba34dc24 Jérôme Schneider
3e9b47e9 Benjamin Dauvergne
RECURRENCE_DESCRIPTION = [
u'Tous les %s', #(1, None, None),
u'Un %s sur deux', #(2, None, None),
u'Un %s sur trois', #(3, None, None),
u'Un %s sur quatre', #(4, None, None),
u'Un %s sur cinq', #(5, None, None),
u'Le premier %s du mois', #(None, 0, None),
u'Le deuxième %s du mois', #(None, 1, None),
u'Le troisième %s du mois', #(None, 2, None),
u'Le quatrième %s du mois', #(None, 3, None),
u'Le dernier %s du mois', #(None, 4, None),
u'Les %s les semaines paires', #(None, None, 0),
u'Les %s les semaines impaires', #(None, None, 1)
]

WEEKDAY_DESRIPTION = [
u'lundi',
u'mardi',
u'mercredi',
u'jeudi',
u'vendredi',
u'samedi',
u'dimanche'
]

def recurrence_description(self):
'''Self description of this recurring event'''
if not self.recurrence_periodicity:
return None
parts = []
9ccdb2fe Benjamin Dauvergne
parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
3e9b47e9 Benjamin Dauvergne
% self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
if self.recurrence_end_date:
parts.append(u'du')
else:
parts.append(u'à partir du')
parts.append(self.start_datetime.strftime('%d/%m/%Y'))
if self.recurrence_end_date:
parts.append(u'au')
parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
return u' '.join(parts)





ba34dc24 Jérôme Schneider
def __unicode__(self):
76974b6f Benjamin Dauvergne
return self.title
ba34dc24 Jérôme Schneider
6190a8ce Benjamin Dauvergne
def __repr__(self):
return '<Event: on {start_datetime} with {participants}'.format(
start_datetime=self.start_datetime,
participants=self.participants.all() if self.id else '<un-saved>')

ba34dc24 Jérôme Schneider
76974b6f Benjamin Dauvergne
class EventWithActManager(managers.EventManager):
def create_patient_appointment(self, creator, title, patient,
doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
room=None, periodicity=1, until=False):
appointment = self.create_event(creator=creator,
title=title,
event_type=EventType(id=1),
participants=doctors,
services=[service],
start_datetime=start_datetime,
end_datetime=end_datetime,
room=room,
periodicity=periodicity,
until=until,
act_type=act_type,
patient=patient)
return appointment


class EventWithAct(Event):
'''An event corresponding to an act.'''
objects = EventWithActManager()
act_type = models.ForeignKey('ressources.ActType',
verbose_name=u'Type d\'acte')
patient = models.ForeignKey('dossiers.PatientRecord')
ddcbee95 Benjamin Dauvergne
convocation_sent = models.BooleanField(blank=True,
e9fb7b88 Benjamin Dauvergne
verbose_name=u'Convoqué', db_index=True)
ba34dc24 Jérôme Schneider
5c2d0ea2 Benjamin Dauvergne
ba34dc24 Jérôme Schneider
@property
76974b6f Benjamin Dauvergne
def act(self):
6b0ae820 Benjamin Dauvergne
for act in self.act_set.all():
if act.date == self.start_datetime.date():
return act
433b67bb Benjamin Dauvergne
return self.build_act()
76974b6f Benjamin Dauvergne
433b67bb Benjamin Dauvergne
def get_state(self):
act = self.act
if act.id:
return act.get_state()
return None

4b5bbcb1 Mikaël Ates
def is_absent(self):
act = self.act
if act.id:
return act.is_absent()
return False

433b67bb Benjamin Dauvergne
def build_act(self):
76974b6f Benjamin Dauvergne
from ..actes.models import Act, ActValidationState
433b67bb Benjamin Dauvergne
act = Act()
self.init_act(act)
old_save = act.save
def save(*args, **kwargs):
old_save(*args, **kwargs)
act.doctors = self.participants.select_subclasses()
76974b6f Benjamin Dauvergne
ActValidationState.objects.create(act=act, state_name='NON_VALIDE',
author=self.creator, previous_state=None)
433b67bb Benjamin Dauvergne
act.save = save
76974b6f Benjamin Dauvergne
return act

def update_act(self, act):
'''Update an act to match details of the meeting'''
433b67bb Benjamin Dauvergne
self.init_act(act)
act.save()

def init_act(self, act):
76974b6f Benjamin Dauvergne
delta = self.timedelta()
duration = delta.seconds // 60
act._duration = duration
act.act_type = self.act_type
act.patient = self.patient
046a69ee Benjamin Dauvergne
act.parent_event = self
76974b6f Benjamin Dauvergne
act.date = self.start_datetime.date()
dfea3c5e Benjamin Dauvergne
act.time = self.start_datetime.time()
76974b6f Benjamin Dauvergne
def save(self, *args, **kwargs):
'''Force event_type to be patient meeting.'''
self.event_type = EventType(id=1)
super(EventWithAct, self).save(*args, **kwargs)
ba0e06ef Benjamin Dauvergne
5f4c5dd2 Frédéric Péters
def is_event_absence(self):
76974b6f Benjamin Dauvergne
return self.act.is_absent()

def __unicode__(self):
kwargs = {
'patient': self.patient,
'start_datetime': self.start_datetime,
'act_type': self.act_type
}
kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
'{act_type} ({act_type.id})'.format(**kwargs)
6eff18f3 Benjamin Dauvergne

from django.db.models.signals import m2m_changed
from django.dispatch import receiver


@receiver(m2m_changed, sender=Event.participants.through)
def participants_changed(sender, instance, action, **kwargs):
if action.startswith('post'):
workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
for act in instance.act_set.all():
act.doctors = workers