1 |
96f613c6
|
Benjamin Dauvergne
|
# -*- coding: utf-8 -*-
|
2 |
ba34dc24
|
Jérôme Schneider
|
|
3 |
76974b6f
|
Benjamin Dauvergne
|
from datetime import datetime, date, timedelta
|
4 |
|
|
from copy import copy
|
5 |
ba34dc24
|
Jérôme Schneider
|
|
6 |
|
|
from django.utils.translation import ugettext_lazy as _
|
7 |
76974b6f
|
Benjamin Dauvergne
|
from django.contrib.auth.models import User
|
8 |
ba34dc24
|
Jérôme Schneider
|
from django.db import models
|
9 |
4ce756c8
|
Benjamin Dauvergne
|
from django import forms
|
10 |
ba34dc24
|
Jérôme Schneider
|
|
11 |
11984ab3
|
Jérôme Schneider
|
from calebasse.agenda import managers
|
12 |
4ce756c8
|
Benjamin Dauvergne
|
from calebasse.utils import weeks_since_epoch, weekday_ranks
|
13 |
e6b125bd
|
Mikaël Ates
|
from calebasse.personnes.models import Holiday
|
14 |
a01d85be
|
Serghei MIHAI
|
|
15 |
|
|
from ..middleware.request import get_request
|
16 |
|
|
|
17 |
7a0a30d7
|
Benjamin Dauvergne
|
from interval import Interval
|
18 |
ba34dc24
|
Jérôme Schneider
|
|
19 |
|
|
__all__ = (
|
20 |
|
|
'EventType',
|
21 |
|
|
'Event',
|
22 |
76974b6f
|
Benjamin Dauvergne
|
'EventWithAct',
|
23 |
ba34dc24
|
Jérôme Schneider
|
)
|
24 |
|
|
|
25 |
|
|
class EventType(models.Model):
|
26 |
|
|
'''
|
27 |
|
|
Simple ``Event`` classifcation.
|
28 |
|
|
'''
|
29 |
|
|
class Meta:
|
30 |
e39efb4a
|
Jérôme Schneider
|
verbose_name = u'Type d\'événement'
|
31 |
|
|
verbose_name_plural = u'Types d\'événement'
|
32 |
ba34dc24
|
Jérôme Schneider
|
|
33 |
|
|
def __unicode__(self):
|
34 |
|
|
return self.label
|
35 |
|
|
|
36 |
e39efb4a
|
Jérôme Schneider
|
label = models.CharField(_('label'), max_length=50)
|
37 |
68e9f8c1
|
Frédéric Péters
|
rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
|
38 |
e39efb4a
|
Jérôme Schneider
|
|
39 |
84091f2c
|
Jérôme Schneider
|
class Event(models.Model):
|
40 |
ba34dc24
|
Jérôme Schneider
|
'''
|
41 |
76974b6f
|
Benjamin Dauvergne
|
Container model for general agenda events
|
42 |
ba34dc24
|
Jérôme Schneider
|
'''
|
43 |
e39efb4a
|
Jérôme Schneider
|
objects = managers.EventManager()
|
44 |
ba34dc24
|
Jérôme Schneider
|
|
45 |
e90f4c73
|
Jérôme Schneider
|
title = models.CharField(_('Title'), max_length=60, blank=True, default="")
|
46 |
a334f1e6
|
Mikaël Ates
|
description = models.TextField(_('Description'), max_length=100, blank=True, null=True)
|
47 |
69a9009e
|
Jérôme Schneider
|
event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
|
48 |
76974b6f
|
Benjamin Dauvergne
|
creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
|
49 |
|
|
create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
|
50 |
ba34dc24
|
Jérôme Schneider
|
|
51 |
84091f2c
|
Jérôme Schneider
|
services = models.ManyToManyField('ressources.Service',
|
52 |
|
|
null=True, blank=True, default=None)
|
53 |
e39efb4a
|
Jérôme Schneider
|
participants = models.ManyToManyField('personnes.People',
|
54 |
|
|
null=True, blank=True, default=None)
|
55 |
dd986559
|
Serghei MIHAI
|
ressource = models.ForeignKey('ressources.Ressource', blank=True, null=True,
|
56 |
|
|
verbose_name=u'Ressource')
|
57 |
e39efb4a
|
Jérôme Schneider
|
|
58 |
5c93596e
|
Benjamin Dauvergne
|
start_datetime = models.DateTimeField(_('Début'), db_index=True)
|
59 |
76974b6f
|
Benjamin Dauvergne
|
end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
|
60 |
74102956
|
Benjamin Dauvergne
|
old_ev_id = models.CharField(max_length=8, blank=True, null=True)
|
61 |
|
|
old_rr_id = models.CharField(max_length=8, blank=True, null=True)
|
62 |
44482a62
|
Benjamin Dauvergne
|
# only used when there is no rr id
|
63 |
74102956
|
Benjamin Dauvergne
|
old_rs_id = models.CharField(max_length=8, blank=True, null=True)
|
64 |
a04356e2
|
Benjamin Dauvergne
|
# exception to is mutually exclusive with recurrence_periodicity
|
65 |
|
|
# an exception cannot be periodic
|
66 |
|
|
exception_to = models.ForeignKey('self', related_name='exceptions',
|
67 |
|
|
blank=True, null=True,
|
68 |
|
|
verbose_name=u'Exception à')
|
69 |
|
|
exception_date = models.DateField(blank=True, null=True,
|
70 |
e9fb7b88
|
Benjamin Dauvergne
|
verbose_name=u'Reporté du', db_index=True)
|
71 |
a04356e2
|
Benjamin Dauvergne
|
# canceled can only be used with exception to
|
72 |
1164bc89
|
Mikaël Ates
|
canceled = models.BooleanField(_('Annulé'), db_index=True, default=False)
|
73 |
76974b6f
|
Benjamin Dauvergne
|
|
74 |
|
|
PERIODS = (
|
75 |
|
|
(1, u'Toutes les semaines'),
|
76 |
|
|
(2, u'Une semaine sur deux'),
|
77 |
|
|
(3, u'Une semaine sur trois'),
|
78 |
|
|
(4, 'Une semaine sur quatre'),
|
79 |
|
|
(5, 'Une semaine sur cinq')
|
80 |
|
|
)
|
81 |
3c5df84d
|
Jérôme Schneider
|
OFFSET = range(0, 4)
|
82 |
4ce756c8
|
Benjamin Dauvergne
|
PERIODICITIES = (
|
83 |
|
|
(1, u'Toutes les semaines'),
|
84 |
|
|
(2, u'Une semaine sur deux'),
|
85 |
|
|
(3, u'Une semaine sur trois'),
|
86 |
|
|
(4, u'Une semaine sur quatre'),
|
87 |
|
|
(5, u'Une semaine sur cinq'),
|
88 |
|
|
(6, u'La première semaine du mois'),
|
89 |
|
|
(7, u'La deuxième semaine du mois'),
|
90 |
|
|
(8, u'La troisième semaine du mois'),
|
91 |
|
|
(9, u'La quatrième semaine du mois'),
|
92 |
9de67cc5
|
Benjamin Dauvergne
|
(13, u'La cinquième semaine du mois'),
|
93 |
4ce756c8
|
Benjamin Dauvergne
|
(10, u'La dernière semaine du mois'),
|
94 |
|
|
(11, u'Les semaines paires'),
|
95 |
|
|
(12, u'Les semaines impaires')
|
96 |
|
|
)
|
97 |
|
|
WEEK_RANKS = (
|
98 |
|
|
(0, u'La première semaine du mois'),
|
99 |
|
|
(1, u'La deuxième semaine du mois'),
|
100 |
|
|
(2, u'La troisième semaine du mois'),
|
101 |
|
|
(3, u'La quatrième semaine du mois'),
|
102 |
9433e992
|
Benjamin Dauvergne
|
(4, u'La cinquième semaine du mois'),
|
103 |
|
|
(-1, u'La dernière semaine du mois'),
|
104 |
|
|
# (-2, u'L\'avant dernière semaine du mois'),
|
105 |
|
|
# (-3, u'L\'antépénultième semaine du mois'),
|
106 |
|
|
# (-4, u'L\'anté-antépénultième semaine du mois'),
|
107 |
|
|
# (-5, u'L\'anté-anté-antépénultième semaine du mois')
|
108 |
|
|
|
109 |
4ce756c8
|
Benjamin Dauvergne
|
)
|
110 |
|
|
PARITIES = (
|
111 |
|
|
(0, u'Les semaines paires'),
|
112 |
|
|
(1, u'Les semaines impaires')
|
113 |
|
|
)
|
114 |
|
|
recurrence_periodicity = models.PositiveIntegerField(
|
115 |
|
|
choices=PERIODICITIES,
|
116 |
|
|
verbose_name=u"Périodicité",
|
117 |
|
|
default=None,
|
118 |
|
|
blank=True,
|
119 |
e9fb7b88
|
Benjamin Dauvergne
|
null=True,
|
120 |
|
|
db_index=True)
|
121 |
|
|
recurrence_week_day = models.PositiveIntegerField(default=0, db_index=True)
|
122 |
76974b6f
|
Benjamin Dauvergne
|
recurrence_week_offset = models.PositiveIntegerField(
|
123 |
|
|
choices=zip(OFFSET, OFFSET),
|
124 |
|
|
verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
|
125 |
|
|
default=0,
|
126 |
|
|
db_index=True)
|
127 |
|
|
recurrence_week_period = models.PositiveIntegerField(
|
128 |
|
|
choices=PERIODS,
|
129 |
|
|
verbose_name=u"Période en semaines",
|
130 |
|
|
default=None,
|
131 |
|
|
blank=True,
|
132 |
|
|
null=True,
|
133 |
|
|
db_index=True)
|
134 |
9433e992
|
Benjamin Dauvergne
|
recurrence_week_rank = models.IntegerField(
|
135 |
4ce756c8
|
Benjamin Dauvergne
|
verbose_name=u"Rang de la semaine dans le mois",
|
136 |
|
|
choices=WEEK_RANKS,
|
137 |
e9fb7b88
|
Benjamin Dauvergne
|
blank=True, null=True, db_index=True)
|
138 |
4ce756c8
|
Benjamin Dauvergne
|
recurrence_week_parity = models.PositiveIntegerField(
|
139 |
|
|
choices=PARITIES,
|
140 |
|
|
verbose_name=u"Parité des semaines",
|
141 |
|
|
blank=True,
|
142 |
e9fb7b88
|
Benjamin Dauvergne
|
null=True,
|
143 |
|
|
db_index=True)
|
144 |
76974b6f
|
Benjamin Dauvergne
|
recurrence_end_date = models.DateField(
|
145 |
|
|
verbose_name=_(u'Fin de la récurrence'),
|
146 |
|
|
blank=True, null=True,
|
147 |
|
|
db_index=True)
|
148 |
|
|
|
149 |
4ce756c8
|
Benjamin Dauvergne
|
PERIOD_LIST_TO_FIELDS = [(1, None, None),
|
150 |
|
|
(2, None, None),
|
151 |
|
|
(3, None, None),
|
152 |
|
|
(4, None, None),
|
153 |
|
|
(5, None, None),
|
154 |
|
|
(None, 0, None),
|
155 |
|
|
(None, 1, None),
|
156 |
|
|
(None, 2, None),
|
157 |
|
|
(None, 3, None),
|
158 |
9de67cc5
|
Benjamin Dauvergne
|
(None, -1, None),
|
159 |
4ce756c8
|
Benjamin Dauvergne
|
(None, None, 0),
|
160 |
9de67cc5
|
Benjamin Dauvergne
|
(None, None, 1),
|
161 |
|
|
(None, 4, None),
|
162 |
4ce756c8
|
Benjamin Dauvergne
|
]
|
163 |
|
|
|
164 |
ba34dc24
|
Jérôme Schneider
|
class Meta:
|
165 |
e39efb4a
|
Jérôme Schneider
|
verbose_name = u'Evénement'
|
166 |
|
|
verbose_name_plural = u'Evénements'
|
167 |
76974b6f
|
Benjamin Dauvergne
|
ordering = ('start_datetime', 'end_datetime', 'title')
|
168 |
a04356e2
|
Benjamin Dauvergne
|
unique_together = (('exception_to', 'exception_date'),)
|
169 |
76974b6f
|
Benjamin Dauvergne
|
|
170 |
|
|
def __init__(self, *args, **kwargs):
|
171 |
|
|
if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
|
172 |
|
|
kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
|
173 |
|
|
super(Event, self).__init__(*args, **kwargs)
|
174 |
|
|
|
175 |
|
|
def clean(self):
|
176 |
|
|
'''Initialize recurrence fields if they are not.'''
|
177 |
f16de572
|
Benjamin Dauvergne
|
self.sanitize()
|
178 |
4ce756c8
|
Benjamin Dauvergne
|
if self.recurrence_periodicity:
|
179 |
f16de572
|
Benjamin Dauvergne
|
if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
|
180 |
4ce756c8
|
Benjamin Dauvergne
|
raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
|
181 |
|
|
if self.recurrence_week_parity is not None:
|
182 |
|
|
if self.start_datetime:
|
183 |
|
|
week = self.start_datetime.date().isocalendar()[1]
|
184 |
|
|
start_week_parity = week % 2
|
185 |
|
|
if start_week_parity != self.recurrence_week_parity:
|
186 |
05cf3715
|
Frédéric Péters
|
raise forms.ValidationError(u'La date de départ de la périodicité est en semaine {week}.'.format(week=week))
|
187 |
4ce756c8
|
Benjamin Dauvergne
|
if self.recurrence_week_rank is not None and self.start_datetime:
|
188 |
|
|
start_week_ranks = weekday_ranks(self.start_datetime.date())
|
189 |
|
|
if self.recurrence_week_rank not in start_week_ranks:
|
190 |
|
|
raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
|
191 |
76974b6f
|
Benjamin Dauvergne
|
|
192 |
f16de572
|
Benjamin Dauvergne
|
def sanitize(self):
|
193 |
|
|
if self.recurrence_periodicity:
|
194 |
|
|
l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
|
195 |
|
|
else:
|
196 |
|
|
l = None, None, None
|
197 |
|
|
self.recurrence_week_period = l[0]
|
198 |
|
|
self.recurrence_week_rank = l[1]
|
199 |
|
|
self.recurrence_week_parity = l[2]
|
200 |
|
|
if self.start_datetime:
|
201 |
|
|
if self.recurrence_periodicity:
|
202 |
|
|
self.recurrence_week_day = self.start_datetime.weekday()
|
203 |
|
|
if self.recurrence_week_period is not None:
|
204 |
|
|
self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
|
205 |
|
|
|
206 |
76974b6f
|
Benjamin Dauvergne
|
def timedelta(self):
|
207 |
|
|
'''Distance between start and end of the event'''
|
208 |
|
|
return self.end_datetime - self.start_datetime
|
209 |
|
|
|
210 |
b2ab5f4c
|
Mikaël Ates
|
@property
|
211 |
|
|
def time(self):
|
212 |
|
|
return self.start_datetime.time()
|
213 |
|
|
|
214 |
|
|
@property
|
215 |
|
|
def date(self):
|
216 |
|
|
return self.start_datetime.date()
|
217 |
|
|
|
218 |
|
|
@property
|
219 |
|
|
def duration(self):
|
220 |
|
|
if self.timedelta():
|
221 |
|
|
return self.timedelta().seconds / 60
|
222 |
|
|
return 0
|
223 |
|
|
|
224 |
a04356e2
|
Benjamin Dauvergne
|
def match_date(self, date):
|
225 |
|
|
if self.is_recurring():
|
226 |
|
|
# consider exceptions
|
227 |
|
|
exception = self.get_exceptions_dict().get(date)
|
228 |
|
|
if exception is not None:
|
229 |
|
|
return exception if exception.match_date(date) else None
|
230 |
|
|
if self.canceled:
|
231 |
|
|
return None
|
232 |
|
|
if date.weekday() != self.recurrence_week_day:
|
233 |
|
|
return None
|
234 |
|
|
if self.start_datetime.date() > date:
|
235 |
|
|
return None
|
236 |
|
|
if self.recurrence_end_date and self.recurrence_end_date < date:
|
237 |
|
|
return None
|
238 |
|
|
if self.recurrence_week_period is not None:
|
239 |
|
|
if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
|
240 |
|
|
return None
|
241 |
|
|
elif self.recurrence_week_parity is not None:
|
242 |
|
|
if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
|
243 |
|
|
return None
|
244 |
|
|
elif self.recurrence_week_rank is not None:
|
245 |
|
|
if self.recurrence_week_rank not in weekday_ranks(date):
|
246 |
|
|
return None
|
247 |
|
|
else:
|
248 |
|
|
raise NotImplemented
|
249 |
|
|
return self
|
250 |
|
|
else:
|
251 |
ced57915
|
Benjamin Dauvergne
|
return self if date == self.start_datetime.date() else None
|
252 |
a04356e2
|
Benjamin Dauvergne
|
|
253 |
|
|
|
254 |
b8a9f824
|
Benjamin Dauvergne
|
def today_occurrence(self, today=None, match=False, upgrade=True):
|
255 |
76974b6f
|
Benjamin Dauvergne
|
'''For a recurring event compute the today 'Event'.
|
256 |
|
|
|
257 |
|
|
The computed event is the fake one that you cannot save to the database.
|
258 |
ba34dc24
|
Jérôme Schneider
|
'''
|
259 |
76974b6f
|
Benjamin Dauvergne
|
today = today or date.today()
|
260 |
a04356e2
|
Benjamin Dauvergne
|
if self.canceled:
|
261 |
76974b6f
|
Benjamin Dauvergne
|
return None
|
262 |
a04356e2
|
Benjamin Dauvergne
|
if match:
|
263 |
|
|
exception = self.get_exceptions_dict().get(today)
|
264 |
b16fae0e
|
Benjamin Dauvergne
|
if exception:
|
265 |
|
|
if exception.start_datetime.date() == today:
|
266 |
|
|
return exception.today_occurrence(today)
|
267 |
|
|
else:
|
268 |
|
|
return None
|
269 |
4ce756c8
|
Benjamin Dauvergne
|
else:
|
270 |
a04356e2
|
Benjamin Dauvergne
|
exception_or_self = self.match_date(today)
|
271 |
|
|
if exception_or_self is None:
|
272 |
|
|
return None
|
273 |
|
|
if exception_or_self != self:
|
274 |
5c2d0ea2
|
Benjamin Dauvergne
|
return exception_or_self.today_occurrence(today)
|
275 |
b8a9f824
|
Benjamin Dauvergne
|
if self.event_type_id == 1 and type(self) != EventWithAct and upgrade:
|
276 |
|
|
self = self.eventwithact
|
277 |
59136255
|
Benjamin Dauvergne
|
if self.recurrence_periodicity is None:
|
278 |
|
|
return self
|
279 |
76974b6f
|
Benjamin Dauvergne
|
start_datetime = datetime.combine(today, self.start_datetime.timetz())
|
280 |
|
|
end_datetime = start_datetime + self.timedelta()
|
281 |
|
|
event = copy(self)
|
282 |
a04356e2
|
Benjamin Dauvergne
|
event.exception_to = self
|
283 |
|
|
event.exception_date = today
|
284 |
76974b6f
|
Benjamin Dauvergne
|
event.start_datetime = start_datetime
|
285 |
|
|
event.end_datetime = end_datetime
|
286 |
a04356e2
|
Benjamin Dauvergne
|
event.recurrence_periodicity = None
|
287 |
|
|
event.recurrence_week_offset = 0
|
288 |
76974b6f
|
Benjamin Dauvergne
|
event.recurrence_week_period = None
|
289 |
a04356e2
|
Benjamin Dauvergne
|
event.recurrence_week_parity = None
|
290 |
|
|
event.recurrence_week_rank = None
|
291 |
|
|
event.recurrence_end_date = None
|
292 |
76974b6f
|
Benjamin Dauvergne
|
event.parent = self
|
293 |
4ce756c8
|
Benjamin Dauvergne
|
# the returned event is "virtual", it must not be saved
|
294 |
a04356e2
|
Benjamin Dauvergne
|
old_save = event.save
|
295 |
|
|
old_participants = list(self.participants.all())
|
296 |
33522bec
|
Benjamin Dauvergne
|
old_services = list(self.services.all())
|
297 |
a334f1e6
|
Mikaël Ates
|
def save(*args, **kwargs):
|
298 |
a04356e2
|
Benjamin Dauvergne
|
event.id = None
|
299 |
86d19877
|
Benjamin Dauvergne
|
event.event_ptr_id = None
|
300 |
a04356e2
|
Benjamin Dauvergne
|
old_save(*args, **kwargs)
|
301 |
dbe774de
|
Benjamin Dauvergne
|
if hasattr(self, 'exceptions_dict'):
|
302 |
|
|
self.exceptions_dict[event.start_datetime.date()] = event
|
303 |
33522bec
|
Benjamin Dauvergne
|
event.services = old_services
|
304 |
a04356e2
|
Benjamin Dauvergne
|
event.participants = old_participants
|
305 |
083e9b50
|
Benjamin Dauvergne
|
event.save = old_save
|
306 |
76974b6f
|
Benjamin Dauvergne
|
event.save = save
|
307 |
|
|
return event
|
308 |
|
|
|
309 |
|
|
def next_occurence(self, today=None):
|
310 |
|
|
'''Returns the next occurence after today.'''
|
311 |
|
|
today = today or date.today()
|
312 |
|
|
for occurence in self.all_occurences():
|
313 |
|
|
if occurence.start_datetime.date() > today:
|
314 |
|
|
return occurence
|
315 |
|
|
|
316 |
|
|
def is_recurring(self):
|
317 |
|
|
'''Is this event multiple ?'''
|
318 |
4ce756c8
|
Benjamin Dauvergne
|
return self.recurrence_periodicity is not None
|
319 |
76974b6f
|
Benjamin Dauvergne
|
|
320 |
a04356e2
|
Benjamin Dauvergne
|
def get_exceptions_dict(self):
|
321 |
|
|
if not hasattr(self, 'exceptions_dict'):
|
322 |
|
|
self.exceptions_dict = dict()
|
323 |
508c3726
|
Benjamin Dauvergne
|
if self.exception_to_id is None:
|
324 |
b8a9f824
|
Benjamin Dauvergne
|
for exception in self.exceptions.all():
|
325 |
508c3726
|
Benjamin Dauvergne
|
self.exceptions_dict[exception.exception_date] = exception
|
326 |
a04356e2
|
Benjamin Dauvergne
|
return self.exceptions_dict
|
327 |
|
|
|
328 |
76974b6f
|
Benjamin Dauvergne
|
def all_occurences(self, limit=90):
|
329 |
4ce756c8
|
Benjamin Dauvergne
|
'''Returns all occurences of this event as virtual Event objects
|
330 |
76974b6f
|
Benjamin Dauvergne
|
|
331 |
|
|
limit - compute occurrences until limit days in the future
|
332 |
|
|
|
333 |
|
|
Default is to limit to 90 days.
|
334 |
ba34dc24
|
Jérôme Schneider
|
'''
|
335 |
4ce756c8
|
Benjamin Dauvergne
|
if self.recurrence_periodicity is not None:
|
336 |
76974b6f
|
Benjamin Dauvergne
|
day = self.start_datetime.date()
|
337 |
|
|
max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
|
338 |
|
|
end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
|
339 |
a04356e2
|
Benjamin Dauvergne
|
occurrences = []
|
340 |
4ce756c8
|
Benjamin Dauvergne
|
if self.recurrence_week_period is not None:
|
341 |
|
|
delta = timedelta(days=self.recurrence_week_period*7)
|
342 |
|
|
while day <= end_date:
|
343 |
790fd669
|
Benjamin Dauvergne
|
occurrence = self.today_occurrence(day, True)
|
344 |
a04356e2
|
Benjamin Dauvergne
|
if occurrence is not None:
|
345 |
|
|
occurrences.append(occurrence)
|
346 |
4ce756c8
|
Benjamin Dauvergne
|
day += delta
|
347 |
|
|
elif self.recurrence_week_parity is not None:
|
348 |
|
|
delta = timedelta(days=7)
|
349 |
|
|
while day <= end_date:
|
350 |
|
|
if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
|
351 |
790fd669
|
Benjamin Dauvergne
|
occurrence = self.today_occurrence(day, True)
|
352 |
a04356e2
|
Benjamin Dauvergne
|
if occurrence is not None:
|
353 |
|
|
occurrences.append(occurrence)
|
354 |
4ce756c8
|
Benjamin Dauvergne
|
day += delta
|
355 |
|
|
elif self.recurrence_week_rank is not None:
|
356 |
|
|
delta = timedelta(days=7)
|
357 |
|
|
while day <= end_date:
|
358 |
|
|
if self.recurrence_week_rank in weekday_ranks(day):
|
359 |
790fd669
|
Benjamin Dauvergne
|
occurrence = self.today_occurrence(day, True)
|
360 |
a04356e2
|
Benjamin Dauvergne
|
if occurrence is not None:
|
361 |
|
|
occurrences.append(occurrence)
|
362 |
4ce756c8
|
Benjamin Dauvergne
|
day += delta
|
363 |
a04356e2
|
Benjamin Dauvergne
|
for exception in self.exceptions.all():
|
364 |
fcc58fae
|
Jérôme Schneider
|
if not exception.canceled:
|
365 |
|
|
if exception.exception_date != exception.start_datetime.date() or exception.exception_date > end_date:
|
366 |
|
|
occurrences.append(exception.eventwithact if exception.event_type_id == 1 else exception)
|
367 |
a04356e2
|
Benjamin Dauvergne
|
return sorted(occurrences, key=lambda o: o.start_datetime)
|
368 |
ba34dc24
|
Jérôme Schneider
|
else:
|
369 |
a04356e2
|
Benjamin Dauvergne
|
return [self]
|
370 |
ba34dc24
|
Jérôme Schneider
|
|
371 |
4ce756c8
|
Benjamin Dauvergne
|
def save(self, *args, **kwargs):
|
372 |
0acdd88a
|
Benjamin Dauvergne
|
assert self.recurrence_periodicity is None or self.exception_to is None
|
373 |
12a8ef2b
|
Benjamin Dauvergne
|
assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
|
374 |
|
|
assert self.start_datetime is not None
|
375 |
f16de572
|
Benjamin Dauvergne
|
self.sanitize() # init periodicity fields
|
376 |
4ce756c8
|
Benjamin Dauvergne
|
super(Event, self).save(*args, **kwargs)
|
377 |
b2ab5f4c
|
Mikaël Ates
|
self.events_cleaning()
|
378 |
5f3ce603
|
Benjamin Dauvergne
|
self.acts_cleaning()
|
379 |
4ce756c8
|
Benjamin Dauvergne
|
|
380 |
fcd786f5
|
Benjamin Dauvergne
|
def delete(self, *args, **kwargs):
|
381 |
b2ab5f4c
|
Mikaël Ates
|
if not self.one_act_already_billed():
|
382 |
|
|
for exception in self.exceptions.all():
|
383 |
|
|
exception.delete()
|
384 |
|
|
self.canceled = True
|
385 |
|
|
if hasattr(self, 'eventwithact'):
|
386 |
|
|
# Needed
|
387 |
|
|
self.eventwithact.canceled = True
|
388 |
|
|
# save will clean acts
|
389 |
|
|
self.save(*args, **kwargs)
|
390 |
|
|
|
391 |
|
|
@property
|
392 |
|
|
def acts(self):
|
393 |
|
|
from ..actes.models import Act
|
394 |
|
|
return Act.objects.filter(
|
395 |
|
|
models.Q(parent_event=self) | \
|
396 |
|
|
models.Q(parent_event__exception_to=self))
|
397 |
|
|
|
398 |
a2e49cca
|
Mikaël Ates
|
def one_act_not_new(self):
|
399 |
|
|
'''
|
400 |
|
|
Return True if at least one act of the present event or an act
|
401 |
|
|
of one of its exceptions is not new.
|
402 |
|
|
'''
|
403 |
|
|
return True if [a for a in self.acts if not a.is_new()] else False
|
404 |
|
|
|
405 |
|
|
def last_act_not_new(self):
|
406 |
|
|
'''
|
407 |
|
|
Return True if at least one act of the present event or an act
|
408 |
|
|
of one of its exceptions is not new.
|
409 |
|
|
'''
|
410 |
|
|
act = None
|
411 |
|
|
for a in self.acts:
|
412 |
|
|
if not a.is_new():
|
413 |
|
|
if not act or a.date > act.date:
|
414 |
|
|
act = a
|
415 |
|
|
return act
|
416 |
|
|
|
417 |
b2ab5f4c
|
Mikaël Ates
|
def one_act_already_billed(self):
|
418 |
|
|
'''
|
419 |
|
|
Return True if at least one act of the present event or an act
|
420 |
|
|
of one of its exceptions is already billed.
|
421 |
|
|
'''
|
422 |
|
|
return self.acts.filter(already_billed=True).exists()
|
423 |
|
|
|
424 |
|
|
def one_act_is_billed(self):
|
425 |
|
|
'''
|
426 |
|
|
Return True if at least one act of the present event or an act
|
427 |
|
|
of one of its exceptions is billed.
|
428 |
|
|
'''
|
429 |
|
|
return self.acts.filter(is_billed=True).exists()
|
430 |
|
|
|
431 |
|
|
def events_cleaning(self):
|
432 |
|
|
"""
|
433 |
|
|
Delete exceptions out of bounds if not with an associated act
|
434 |
|
|
already billed.
|
435 |
|
|
"""
|
436 |
|
|
events = None
|
437 |
|
|
if self.recurrence_end_date:
|
438 |
|
|
events = Event.objects.filter(
|
439 |
|
|
models.Q(exception_to=self,
|
440 |
|
|
exception_date__lt=self.start_datetime.date()) | \
|
441 |
|
|
models.Q(exception_to=self,
|
442 |
|
|
exception_date__gt=self.recurrence_end_date))
|
443 |
|
|
else:
|
444 |
|
|
events = Event.objects.filter(exception_to=self,
|
445 |
|
|
exception_date__lt=self.start_datetime.date())
|
446 |
|
|
for event in events:
|
447 |
|
|
if hasattr(event, 'eventwithact'):
|
448 |
|
|
if not event.eventwithact.act.already_billed:
|
449 |
|
|
event.delete()
|
450 |
|
|
else:
|
451 |
|
|
event.delete()
|
452 |
5f3ce603
|
Benjamin Dauvergne
|
|
453 |
|
|
def acts_cleaning(self):
|
454 |
|
|
# list of occurences may have changed
|
455 |
|
|
from ..actes.models import Act
|
456 |
|
|
if self.exception_to:
|
457 |
|
|
# maybe a new exception, so look for parent acts with same date
|
458 |
|
|
# as exception date
|
459 |
|
|
acts = Act.objects.filter(models.Q(parent_event=self)
|
460 |
|
|
|models.Q(parent_event=self.exception_to,
|
461 |
|
|
date=self.exception_date))
|
462 |
|
|
else:
|
463 |
|
|
acts = Act.objects.filter(parent_event=self)
|
464 |
|
|
acts = acts.prefetch_related('actvalidationstate_set')
|
465 |
|
|
if acts:
|
466 |
|
|
eventwithact = self.eventwithact
|
467 |
|
|
for act in acts:
|
468 |
1d10b2c6
|
Benjamin Dauvergne
|
occurrence = eventwithact.today_occurrence(act.date)
|
469 |
|
|
if occurrence:
|
470 |
|
|
occurrence.update_act(act)
|
471 |
|
|
else:
|
472 |
b2ab5f4c
|
Mikaël Ates
|
if not act.already_billed:
|
473 |
|
|
act.delete()
|
474 |
fcd786f5
|
Benjamin Dauvergne
|
|
475 |
76974b6f
|
Benjamin Dauvergne
|
def to_interval(self):
|
476 |
|
|
return Interval(self.start_datetime, self.end_datetime)
|
477 |
ba34dc24
|
Jérôme Schneider
|
|
478 |
76974b6f
|
Benjamin Dauvergne
|
def is_event_absence(self):
|
479 |
|
|
return False
|
480 |
ba34dc24
|
Jérôme Schneider
|
|
481 |
bba85766
|
Serghei MIHAI
|
def get_inactive_participants(self):
|
482 |
|
|
return self.participants.filter(worker__enabled=False)
|
483 |
|
|
|
484 |
e6b125bd
|
Mikaël Ates
|
def get_missing_participants(self):
|
485 |
|
|
missing_participants = []
|
486 |
|
|
for participant in self.participants.all():
|
487 |
|
|
holidays = None
|
488 |
|
|
worker = participant.worker
|
489 |
|
|
holidays = Holiday.objects.for_worker(worker) \
|
490 |
|
|
.for_timed_period(self.start_datetime.date(), self.start_datetime.time(), self.end_datetime.time())
|
491 |
|
|
if holidays:
|
492 |
|
|
missing_participants.append(participant)
|
493 |
|
|
return missing_participants
|
494 |
|
|
|
495 |
3e9b47e9
|
Benjamin Dauvergne
|
RECURRENCE_DESCRIPTION = [
|
496 |
|
|
u'Tous les %s', #(1, None, None),
|
497 |
|
|
u'Un %s sur deux', #(2, None, None),
|
498 |
|
|
u'Un %s sur trois', #(3, None, None),
|
499 |
|
|
u'Un %s sur quatre', #(4, None, None),
|
500 |
|
|
u'Un %s sur cinq', #(5, None, None),
|
501 |
|
|
u'Le premier %s du mois', #(None, 0, None),
|
502 |
|
|
u'Le deuxième %s du mois', #(None, 1, None),
|
503 |
|
|
u'Le troisième %s du mois', #(None, 2, None),
|
504 |
|
|
u'Le quatrième %s du mois', #(None, 3, None),
|
505 |
|
|
u'Le dernier %s du mois', #(None, 4, None),
|
506 |
|
|
u'Les %s les semaines paires', #(None, None, 0),
|
507 |
|
|
u'Les %s les semaines impaires', #(None, None, 1)
|
508 |
|
|
]
|
509 |
|
|
|
510 |
|
|
WEEKDAY_DESRIPTION = [
|
511 |
|
|
u'lundi',
|
512 |
|
|
u'mardi',
|
513 |
|
|
u'mercredi',
|
514 |
|
|
u'jeudi',
|
515 |
|
|
u'vendredi',
|
516 |
|
|
u'samedi',
|
517 |
|
|
u'dimanche'
|
518 |
|
|
]
|
519 |
|
|
|
520 |
|
|
def recurrence_description(self):
|
521 |
|
|
'''Self description of this recurring event'''
|
522 |
|
|
if not self.recurrence_periodicity:
|
523 |
|
|
return None
|
524 |
|
|
parts = []
|
525 |
9ccdb2fe
|
Benjamin Dauvergne
|
parts.append(self.RECURRENCE_DESCRIPTION[self.recurrence_periodicity-1] \
|
526 |
3e9b47e9
|
Benjamin Dauvergne
|
% self.WEEKDAY_DESRIPTION[self.recurrence_week_day])
|
527 |
|
|
if self.recurrence_end_date:
|
528 |
|
|
parts.append(u'du')
|
529 |
|
|
else:
|
530 |
|
|
parts.append(u'à partir du')
|
531 |
|
|
parts.append(self.start_datetime.strftime('%d/%m/%Y'))
|
532 |
|
|
if self.recurrence_end_date:
|
533 |
|
|
parts.append(u'au')
|
534 |
|
|
parts.append(self.recurrence_end_date.strftime('%d/%m/%Y'))
|
535 |
|
|
return u' '.join(parts)
|
536 |
|
|
|
537 |
7a2fc3bb
|
Serghei MIHAI
|
def is_absent(self):
|
538 |
|
|
try:
|
539 |
|
|
return self.eventwithact.is_absent()
|
540 |
|
|
except self.DoesNotExist:
|
541 |
|
|
return False
|
542 |
|
|
|
543 |
ba34dc24
|
Jérôme Schneider
|
def __unicode__(self):
|
544 |
76974b6f
|
Benjamin Dauvergne
|
return self.title
|
545 |
ba34dc24
|
Jérôme Schneider
|
|
546 |
6190a8ce
|
Benjamin Dauvergne
|
def __repr__(self):
|
547 |
|
|
return '<Event: on {start_datetime} with {participants}'.format(
|
548 |
|
|
start_datetime=self.start_datetime,
|
549 |
|
|
participants=self.participants.all() if self.id else '<un-saved>')
|
550 |
|
|
|
551 |
ba34dc24
|
Jérôme Schneider
|
|
552 |
76974b6f
|
Benjamin Dauvergne
|
class EventWithActManager(managers.EventManager):
|
553 |
|
|
def create_patient_appointment(self, creator, title, patient,
|
554 |
|
|
doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
|
555 |
dd986559
|
Serghei MIHAI
|
ressource=None, periodicity=1, until=False):
|
556 |
76974b6f
|
Benjamin Dauvergne
|
appointment = self.create_event(creator=creator,
|
557 |
|
|
title=title,
|
558 |
|
|
event_type=EventType(id=1),
|
559 |
|
|
participants=doctors,
|
560 |
|
|
services=[service],
|
561 |
|
|
start_datetime=start_datetime,
|
562 |
|
|
end_datetime=end_datetime,
|
563 |
dd986559
|
Serghei MIHAI
|
ressource=ressource,
|
564 |
76974b6f
|
Benjamin Dauvergne
|
periodicity=periodicity,
|
565 |
|
|
until=until,
|
566 |
|
|
act_type=act_type,
|
567 |
|
|
patient=patient)
|
568 |
|
|
return appointment
|
569 |
|
|
|
570 |
|
|
|
571 |
|
|
class EventWithAct(Event):
|
572 |
|
|
'''An event corresponding to an act.'''
|
573 |
|
|
objects = EventWithActManager()
|
574 |
|
|
act_type = models.ForeignKey('ressources.ActType',
|
575 |
|
|
verbose_name=u'Type d\'acte')
|
576 |
|
|
patient = models.ForeignKey('dossiers.PatientRecord')
|
577 |
ddcbee95
|
Benjamin Dauvergne
|
convocation_sent = models.BooleanField(blank=True,
|
578 |
1164bc89
|
Mikaël Ates
|
verbose_name=u'Convoqué', db_index=True, default=False)
|
579 |
ba34dc24
|
Jérôme Schneider
|
|
580 |
5c2d0ea2
|
Benjamin Dauvergne
|
|
581 |
ba34dc24
|
Jérôme Schneider
|
@property
|
582 |
76974b6f
|
Benjamin Dauvergne
|
def act(self):
|
583 |
6b0ae820
|
Benjamin Dauvergne
|
for act in self.act_set.all():
|
584 |
|
|
if act.date == self.start_datetime.date():
|
585 |
|
|
return act
|
586 |
433b67bb
|
Benjamin Dauvergne
|
return self.build_act()
|
587 |
76974b6f
|
Benjamin Dauvergne
|
|
588 |
433b67bb
|
Benjamin Dauvergne
|
def get_state(self):
|
589 |
|
|
act = self.act
|
590 |
|
|
if act.id:
|
591 |
|
|
return act.get_state()
|
592 |
|
|
return None
|
593 |
|
|
|
594 |
4b5bbcb1
|
Mikaël Ates
|
def is_absent(self):
|
595 |
|
|
act = self.act
|
596 |
|
|
if act.id:
|
597 |
|
|
return act.is_absent()
|
598 |
|
|
return False
|
599 |
|
|
|
600 |
433b67bb
|
Benjamin Dauvergne
|
def build_act(self):
|
601 |
76974b6f
|
Benjamin Dauvergne
|
from ..actes.models import Act, ActValidationState
|
602 |
433b67bb
|
Benjamin Dauvergne
|
act = Act()
|
603 |
|
|
self.init_act(act)
|
604 |
|
|
old_save = act.save
|
605 |
|
|
def save(*args, **kwargs):
|
606 |
8fa0047b
|
Benjamin Dauvergne
|
act.save = old_save
|
607 |
433b67bb
|
Benjamin Dauvergne
|
old_save(*args, **kwargs)
|
608 |
d3cfbd19
|
Serghei MIHAI
|
act.doctors = (participant.worker for participant in self.participants.all())
|
609 |
981abc28
|
Jérôme Schneider
|
last_validation_state = ActValidationState.objects.create(
|
610 |
|
|
act=act, state_name='NON_VALIDE',
|
611 |
|
|
author=self.creator, previous_state=None)
|
612 |
|
|
act.last_validation_state = last_validation_state
|
613 |
32551648
|
Jérôme Schneider
|
old_save(*args, **kwargs)
|
614 |
433b67bb
|
Benjamin Dauvergne
|
act.save = save
|
615 |
76974b6f
|
Benjamin Dauvergne
|
return act
|
616 |
|
|
|
617 |
|
|
def update_act(self, act):
|
618 |
|
|
'''Update an act to match details of the meeting'''
|
619 |
433b67bb
|
Benjamin Dauvergne
|
self.init_act(act)
|
620 |
|
|
act.save()
|
621 |
|
|
|
622 |
|
|
def init_act(self, act):
|
623 |
b2ab5f4c
|
Mikaël Ates
|
if not act.is_billed:
|
624 |
|
|
delta = self.timedelta()
|
625 |
|
|
duration = delta.seconds // 60
|
626 |
|
|
act._duration = duration
|
627 |
|
|
act.act_type = self.act_type
|
628 |
|
|
act.patient = self.patient
|
629 |
|
|
act.date = self.start_datetime.date()
|
630 |
|
|
act.time = self.start_datetime.time()
|
631 |
046a69ee
|
Benjamin Dauvergne
|
act.parent_event = self
|
632 |
76974b6f
|
Benjamin Dauvergne
|
|
633 |
|
|
def save(self, *args, **kwargs):
|
634 |
|
|
'''Force event_type to be patient meeting.'''
|
635 |
|
|
self.event_type = EventType(id=1)
|
636 |
|
|
super(EventWithAct, self).save(*args, **kwargs)
|
637 |
ba0e06ef
|
Benjamin Dauvergne
|
|
638 |
5f4c5dd2
|
Frédéric Péters
|
def is_event_absence(self):
|
639 |
76974b6f
|
Benjamin Dauvergne
|
return self.act.is_absent()
|
640 |
|
|
|
641 |
|
|
def __unicode__(self):
|
642 |
|
|
kwargs = {
|
643 |
|
|
'patient': self.patient,
|
644 |
|
|
'start_datetime': self.start_datetime,
|
645 |
|
|
'act_type': self.act_type
|
646 |
|
|
}
|
647 |
|
|
kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
|
648 |
|
|
return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
|
649 |
|
|
'{act_type} ({act_type.id})'.format(**kwargs)
|
650 |
6eff18f3
|
Benjamin Dauvergne
|
|
651 |
|
|
|
652 |
|
|
from django.db.models.signals import m2m_changed
|
653 |
|
|
from django.dispatch import receiver
|
654 |
|
|
|
655 |
|
|
|
656 |
|
|
@receiver(m2m_changed, sender=Event.participants.through)
|
657 |
|
|
def participants_changed(sender, instance, action, **kwargs):
|
658 |
|
|
if action.startswith('post'):
|
659 |
|
|
workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
|
660 |
|
|
for act in instance.act_set.all():
|
661 |
|
|
act.doctors = workers
|