Project

General

Profile

Download (19.2 KB) Statistics
| Branch: | Tag: | Revision:

calebasse / calebasse / agenda / models.py @ 3ca3d1ff

1
# -*- coding: utf-8 -*-
2

    
3
from datetime import datetime, date, timedelta
4
from copy import copy
5

    
6
from django.utils.translation import ugettext_lazy as _
7
from django.contrib.auth.models import User
8
from django.db import models
9
from django import forms
10

    
11
from calebasse.agenda import managers
12
from calebasse.utils import weeks_since_epoch, weekday_ranks
13
from interval import Interval
14

    
15
__all__ = (
16
    'EventType',
17
    'Event',
18
    'EventWithAct',
19
)
20

    
21
class EventType(models.Model):
22
    '''
23
    Simple ``Event`` classifcation.
24
    '''
25
    class Meta:
26
        verbose_name = u'Type d\'événement'
27
        verbose_name_plural = u'Types d\'événement'
28

    
29
    def __unicode__(self):
30
        return self.label
31

    
32
    label = models.CharField(_('label'), max_length=50)
33
    rank = models.IntegerField(_('Sorting Rank'), null=True, blank=True, default=0)
34

    
35
class Event(models.Model):
36
    '''
37
    Container model for general agenda events
38
    '''
39
    objects = managers.EventManager()
40

    
41
    title = models.CharField(_('Title'), max_length=60, blank=True)
42
    description = models.TextField(_('Description'), max_length=100)
43
    event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
44
    creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
45
    create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
46

    
47
    services = models.ManyToManyField('ressources.Service',
48
            null=True, blank=True, default=None)
49
    participants = models.ManyToManyField('personnes.People',
50
            null=True, blank=True, default=None)
51
    room = models.ForeignKey('ressources.Room', blank=True, null=True,
52
            verbose_name=u'Salle')
53

    
54
    start_datetime = models.DateTimeField(_('Début'), db_index=True)
55
    end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
56
    old_ev_id = models.CharField(max_length=8, blank=True, null=True)
57
    old_rr_id = models.CharField(max_length=8, blank=True, null=True)
58
    # only used when there is no rr id
59
    old_rs_id = models.CharField(max_length=8, blank=True, null=True)
60
    # exception to is mutually exclusive with recurrence_periodicity
61
    # an exception cannot be periodic
62
    exception_to = models.ForeignKey('self', related_name='exceptions',
63
            blank=True, null=True,
64
            verbose_name=u'Exception à')
65
    exception_date = models.DateField(blank=True, null=True,
66
            verbose_name=u'Reporté du')
67
    # canceled can only be used with exception to
68
    canceled = models.BooleanField(_('Annulé'))
69

    
70
    PERIODS = (
71
            (1, u'Toutes les semaines'),
72
            (2, u'Une semaine sur deux'),
73
            (3, u'Une semaine sur trois'),
74
            (4, 'Une semaine sur quatre'),
75
            (5, 'Une semaine sur cinq')
76
    )
77
    OFFSET = range(0,4)
78
    PERIODICITIES = (
79
            (1, u'Toutes les semaines'),
80
            (2, u'Une semaine sur deux'),
81
            (3, u'Une semaine sur trois'),
82
            (4, u'Une semaine sur quatre'),
83
            (5, u'Une semaine sur cinq'),
84
            (6, u'La première semaine du mois'),
85
            (7, u'La deuxième semaine du mois'),
86
            (8, u'La troisième semaine du mois'),
87
            (9, u'La quatrième semaine du mois'),
88
            (10, u'La dernière semaine du mois'),
89
            (11, u'Les semaines paires'),
90
            (12, u'Les semaines impaires')
91
    )
92
    WEEK_RANKS = (
93
            (0, u'La première semaine du mois'),
94
            (1, u'La deuxième semaine du mois'),
95
            (2, u'La troisième semaine du mois'),
96
            (3, u'La quatrième semaine du mois'),
97
            (4, u'La dernière semaine du mois')
98
    )
99
    PARITIES = (
100
            (0, u'Les semaines paires'),
101
            (1, u'Les semaines impaires')
102
    )
103
    recurrence_periodicity = models.PositiveIntegerField(
104
            choices=PERIODICITIES,
105
            verbose_name=u"Périodicité",
106
            default=None,
107
            blank=True,
108
            null=True)
109
    recurrence_week_day = models.PositiveIntegerField(default=0)
110
    recurrence_week_offset = models.PositiveIntegerField(
111
            choices=zip(OFFSET, OFFSET),
112
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
113
            default=0,
114
            db_index=True)
115
    recurrence_week_period = models.PositiveIntegerField(
116
            choices=PERIODS,
117
            verbose_name=u"Période en semaines",
118
            default=None,
119
            blank=True,
120
            null=True,
121
            db_index=True)
122
    recurrence_week_rank = models.PositiveIntegerField(
123
            verbose_name=u"Rang de la semaine dans le mois",
124
            choices=WEEK_RANKS,
125
            blank=True, null=True)
126
    recurrence_week_parity = models.PositiveIntegerField(
127
            choices=PARITIES,
128
            verbose_name=u"Parité des semaines",
129
            blank=True,
130
            null=True)
131
    recurrence_end_date = models.DateField(
132
            verbose_name=_(u'Fin de la récurrence'),
133
            blank=True, null=True,
134
            db_index=True)
135

    
136
    PERIOD_LIST_TO_FIELDS = [(1, None, None),
137
        (2, None, None),
138
        (3, None, None),
139
        (4, None, None),
140
        (5, None, None),
141
        (None, 0, None),
142
        (None, 1, None),
143
        (None, 2, None),
144
        (None, 3, None),
145
        (None, 4, None),
146
        (None, None, 0),
147
        (None, None, 1)
148
    ]
149

    
150
    class Meta:
151
        verbose_name = u'Evénement'
152
        verbose_name_plural = u'Evénements'
153
        ordering = ('start_datetime', 'end_datetime', 'title')
154
        unique_together = (('exception_to', 'exception_date'),)
155

    
156
    def __init__(self, *args, **kwargs):
157
        if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
158
            kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
159
        super(Event, self).__init__(*args, **kwargs)
160

    
161
    def clean(self):
162
        '''Initialize recurrence fields if they are not.'''
163
        self.sanitize()
164
        if self.recurrence_periodicity:
165
            if self.recurrence_end_date and self.start_datetime and self.recurrence_end_date < self.start_datetime.date():
166
                raise forms.ValidationError(u'La date de fin de périodicité doit être postérieure à la date de début.')
167
        if self.recurrence_week_parity is not None:
168
            if self.start_datetime:
169
                week = self.start_datetime.date().isocalendar()[1]
170
                start_week_parity = week % 2
171
                if start_week_parity != self.recurrence_week_parity:
172
                    raise forms.ValidationError(u'Le date de départ de la périodicité est en semaine {week}.'.format(week=week))
173
        if self.recurrence_week_rank is not None and self.start_datetime:
174
            start_week_ranks = weekday_ranks(self.start_datetime.date())
175
            if self.recurrence_week_rank not in start_week_ranks:
176
                raise forms.ValidationError('La date de début de périodicité doit faire partie de la bonne semaine dans le mois.')
177

    
178
    def sanitize(self):
179
        if self.recurrence_periodicity:
180
            l = self.PERIOD_LIST_TO_FIELDS[self.recurrence_periodicity-1]
181
        else:
182
            l = None, None, None
183
        self.recurrence_week_period = l[0]
184
        self.recurrence_week_rank = l[1]
185
        self.recurrence_week_parity = l[2]
186
        if self.start_datetime:
187
            if self.recurrence_periodicity:
188
                self.recurrence_week_day = self.start_datetime.weekday()
189
            if self.recurrence_week_period is not None:
190
                self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
191

    
192
    def timedelta(self):
193
        '''Distance between start and end of the event'''
194
        return self.end_datetime - self.start_datetime
195

    
196
    def match_date(self, date):
197
        if self.is_recurring():
198
            # consider exceptions
199
            exception = self.get_exceptions_dict().get(date)
200
            if exception is not None:
201
                return exception if exception.match_date(date) else None
202
            if self.canceled:
203
                return None
204
            if date.weekday() != self.recurrence_week_day:
205
                return None
206
            if self.start_datetime.date() > date:
207
                return None
208
            if self.recurrence_end_date and self.recurrence_end_date < date:
209
                return None
210
            if self.recurrence_week_period is not None:
211
                if weeks_since_epoch(date) % self.recurrence_week_period != self.recurrence_week_offset:
212
                    return None
213
            elif self.recurrence_week_parity is not None:
214
                if date.isocalendar()[1] % 2 != self.recurrence_week_parity:
215
                    return None
216
            elif self.recurrence_week_rank is not None:
217
                if self.recurrence_week_rank not in weekday_ranks(date):
218
                    return None
219
            else:
220
                raise NotImplemented
221
            return self
222
        else:
223
            return self if date == self.start_datetime.date() else None
224

    
225

    
226
    def today_occurrence(self, today=None, match=False):
227
        '''For a recurring event compute the today 'Event'.
228

    
229
           The computed event is the fake one that you cannot save to the database.
230
        '''
231
        today = today or date.today()
232
        if self.canceled:
233
            return None
234
        if match:
235
            exception = self.get_exceptions_dict().get(today)
236
            if exception:
237
                if exception.start_datetime.date() == today:
238
                    return exception.today_occurrence(today)
239
                else:
240
                    return None
241
        else:
242
            exception_or_self = self.match_date(today)
243
            if exception_or_self is None:
244
                return None
245
            if exception_or_self != self:
246
                return exception_or_self.today_occurrence(today)
247
        if self.recurrence_periodicity is None:
248
            return self
249
        start_datetime = datetime.combine(today, self.start_datetime.timetz())
250
        end_datetime = start_datetime + self.timedelta()
251
        event = copy(self)
252
        event.exception_to = self
253
        event.exception_date = today
254
        event.start_datetime = start_datetime
255
        event.end_datetime = end_datetime
256
        event.recurrence_periodicity = None
257
        event.recurrence_week_offset = 0
258
        event.recurrence_week_period = None
259
        event.recurrence_week_parity = None
260
        event.recurrence_week_rank = None
261
        event.recurrence_end_date = None
262
        event.parent = self
263
        # the returned event is "virtual", it must not be saved
264
        old_save = event.save
265
        old_participants = list(self.participants.all())
266
        def save(*args, **kwargs): 
267
            event.id = None
268
            old_save(*args, **kwargs)
269
            event.participants = old_participants
270
        event.save = save
271
        return event
272

    
273
    def next_occurence(self, today=None):
274
        '''Returns the next occurence after today.'''
275
        today = today or date.today()
276
        for occurence in self.all_occurences():
277
            if occurence.start_datetime.date() > today:
278
                return occurence
279

    
280
    def is_recurring(self):
281
        '''Is this event multiple ?'''
282
        return self.recurrence_periodicity is not None
283

    
284
    def get_exceptions_dict(self):
285
        if not hasattr(self, 'exceptions_dict'):
286
            self.exceptions_dict = dict()
287
            if self.exception_to_id is None:
288
                for exception in self.exceptions.select_subclasses():
289
                    self.exceptions_dict[exception.exception_date] = exception
290
        return self.exceptions_dict
291

    
292
    def all_occurences(self, limit=90):
293
        '''Returns all occurences of this event as virtual Event objects
294

    
295
           limit - compute occurrences until limit days in the future
296

    
297
           Default is to limit to 90 days.
298
        '''
299
        if self.recurrence_periodicity is not None:
300
            day = self.start_datetime.date()
301
            max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
302
            end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
303
            occurrences = []
304
            if self.recurrence_week_period is not None:
305
                delta = timedelta(days=self.recurrence_week_period*7)
306
                while day <= end_date:
307
                    occurrence = self.today_occurrence(day, True)
308
                    if occurrence is not None:
309
                        occurrences.append(occurrence)
310
                    day += delta
311
            elif self.recurrence_week_parity is not None:
312
                delta = timedelta(days=7)
313
                while day <= end_date:
314
                    if day.isocalendar()[1] % 2 == self.recurrence_week_parity:
315
                        occurrence = self.today_occurrence(day, True)
316
                        if occurrence is not None:
317
                            occurrences.append(occurrence)
318
                    day += delta
319
            elif self.recurrence_week_rank is not None:
320
                delta = timedelta(days=7)
321
                while day <= end_date:
322
                    if self.recurrence_week_rank in weekday_ranks(day):
323
                        occurrence = self.today_occurrence(day, True)
324
                        if occurrence is not None:
325
                            occurrences.append(occurrence)
326
                    day += delta
327
            for exception in self.exceptions.all():
328
                if exception.exception_date != exception.start_datetime.date():
329
                    occurrences.append(exception)
330
            return sorted(occurrences, key=lambda o: o.start_datetime)
331
        else:
332
            return [self]
333

    
334
    def save(self, *args, **kwargs):
335
        assert self.recurrence_periodicity is None or self.exception_to is None
336
        assert self.exception_to is None or self.exception_to.recurrence_periodicity is not None
337
        assert self.start_datetime is not None
338
        self.sanitize() # init periodicity fields
339
        super(Event, self).save(*args, **kwargs)
340

    
341
    def delete(self, *args, **kwargs):
342
        # never delete, only cancel
343
        from ..actes.models import Act
344
        for a in Act.objects.filter(parent_event=self):
345
            if len(a.actvalidationstate_set.all()) > 1:
346
                a.parent_event = None
347
                a.save()
348
            else:
349
                a.delete()
350
        self.canceled = True
351
        self.save()
352

    
353
    def to_interval(self):
354
        return Interval(self.start_datetime, self.end_datetime)
355

    
356
    def is_event_absence(self):
357
        return False
358

    
359
    def __unicode__(self):
360
        return self.title
361

    
362
    def __repr__(self):
363
        return '<Event: on {start_datetime} with {participants}'.format(
364
                start_datetime=self.start_datetime,
365
                participants=self.participants.all() if self.id else '<un-saved>')
366

    
367

    
368
class EventWithActManager(managers.EventManager):
369
    def create_patient_appointment(self, creator, title, patient,
370
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
371
            room=None, periodicity=1, until=False):
372
        appointment = self.create_event(creator=creator,
373
                title=title,
374
                event_type=EventType(id=1),
375
                participants=doctors,
376
                services=[service],
377
                start_datetime=start_datetime,
378
                end_datetime=end_datetime,
379
                room=room,
380
                periodicity=periodicity,
381
                until=until,
382
                act_type=act_type,
383
                patient=patient)
384
        return appointment
385

    
386

    
387
class EventWithAct(Event):
388
    '''An event corresponding to an act.'''
389
    objects = EventWithActManager()
390
    act_type = models.ForeignKey('ressources.ActType',
391
        verbose_name=u'Type d\'acte')
392
    patient = models.ForeignKey('dossiers.PatientRecord')
393
    convocation_sent = models.BooleanField(blank=True,
394
        verbose_name=u'Convoqué')
395

    
396

    
397
    @property
398
    def act(self):
399
        return self.get_or_create_act()
400

    
401
    def get_or_create_act(self, today=None):
402
        from ..actes.models import Act, ActValidationState
403
        today = today or self.start_datetime.date()
404
        act, created = Act.objects.get_or_create(patient=self.patient,
405
                time=self.start_datetime.time(),
406
                _duration=self.timedelta().seconds // 60,
407
                parent_event=getattr(self, 'parent', self),
408
                date=today,
409
                act_type=self.act_type)
410
        self.update_act(act)
411
        if created:
412
            ActValidationState.objects.create(act=act, state_name='NON_VALIDE',
413
                author=self.creator, previous_state=None)
414
        return act
415

    
416
    def update_act(self, act):
417
        '''Update an act to match details of the meeting'''
418
        delta = self.timedelta()
419
        duration = delta.seconds // 60
420
        act._duration = duration
421
        act.doctors = self.participants.select_subclasses()
422
        act.act_type = self.act_type
423
        act.patient = self.patient
424
        act.date = self.start_datetime.date()
425
        act.save()
426

    
427
    def save(self, *args, **kwargs):
428
        '''Force event_type to be patient meeting.'''
429
        self.event_type = EventType(id=1)
430
        super(EventWithAct, self).save(*args, **kwargs)
431
        # list of occurences may have changed
432
        from ..actes.models import Act
433
        occurences = list(self.all_occurences())
434
        acts = Act.objects.filter(parent_event=self)
435
        occurences_by_date = dict((o.start_datetime.date(), o) for o in occurences)
436
        acts_by_date = dict()
437
        for a in acts:
438
            # sanity check
439
            assert a.date not in acts_by_date
440
            acts_by_date[a.date] = a
441
        for a in acts:
442
            o = occurences_by_date.get(a.date)
443
            if o:
444
                o.update_act(a)
445
            else:
446
                if len(a.actvalidationstate_set.all()) > 1:
447
                    a.parent_event = None
448
                    a.save()
449
                else:
450
                    a.delete()
451
        for o in occurences:
452
            if o.start_datetime.date() in acts_by_date:
453
                continue
454
            o.get_or_create_act()
455

    
456
    def today_occurrence(self, today=None, match=False):
457
        '''For virtual occurrences reset the event_ptr_id'''
458
        occurrence = super(EventWithAct, self).today_occurrence(today, match)
459
        if hasattr(occurrence, 'parent'):
460
            old_save = occurrence.save
461
            def save(*args, **kwargs):
462
                occurrence.event_ptr_id = None
463
                old_save(*args, **kwargs)
464
            occurrence.save = save
465
        return occurrence
466

    
467
    def is_event_absence(self):
468
        return self.act.is_absent()
469

    
470
    def __unicode__(self):
471
        kwargs = {
472
                'patient': self.patient,
473
                'start_datetime': self.start_datetime,
474
                'act_type': self.act_type
475
        }
476
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
477
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
478
            '{act_type} ({act_type.id})'.format(**kwargs)
479

    
480

    
481
from django.db.models.signals import m2m_changed
482
from django.dispatch import receiver
483

    
484

    
485
@receiver(m2m_changed, sender=Event.participants.through)
486
def participants_changed(sender, instance, action, **kwargs):
487
    if action.startswith('post'):
488
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
489
        for act in instance.act_set.all():
490
            act.doctors = workers
(7-7/10)