Project

General

Profile

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

calebasse / calebasse / agenda / models.py @ b16fae0e

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
    def delete(self, *args, **kwargs):
398
        if self.recurrence_periodicity is None:
399
            # clear all linked exceptions
400
            pass
401
        elif self.exception_to is not None:
402
            pass
403
        elif hasattr(self, 'parent'):
404
            pass
405
        else:
406
            # clear all linked exceptions
407
            pass
408
        super(EventWithAct, self).delete(*args, **kwargs)
409

    
410
    @property
411
    def act(self):
412
        return self.get_or_create_act()
413

    
414
    def get_or_create_act(self, today=None):
415
        from ..actes.models import Act, ActValidationState
416
        today = today or self.start_datetime.date()
417
        act, created = Act.objects.get_or_create(patient=self.patient,
418
                time=self.start_datetime.time(),
419
                _duration=self.timedelta().seconds // 60,
420
                parent_event=getattr(self, 'parent', self),
421
                date=today,
422
                act_type=self.act_type)
423
        self.update_act(act)
424
        if created:
425
            ActValidationState.objects.create(act=act, state_name='NON_VALIDE',
426
                author=self.creator, previous_state=None)
427
        return act
428

    
429
    def update_act(self, act):
430
        '''Update an act to match details of the meeting'''
431
        delta = self.timedelta()
432
        duration = delta.seconds // 60
433
        act._duration = duration
434
        act.doctors = self.participants.select_subclasses()
435
        act.act_type = self.act_type
436
        act.patient = self.patient
437
        act.date = self.start_datetime.date()
438
        act.save()
439

    
440
    def save(self, *args, **kwargs):
441
        '''Force event_type to be patient meeting.'''
442
        self.event_type = EventType(id=1)
443
        super(EventWithAct, self).save(*args, **kwargs)
444
        # list of occurences may have changed
445
        from ..actes.models import Act
446
        occurences = list(self.all_occurences())
447
        acts = Act.objects.filter(parent_event=self)
448
        occurences_by_date = dict((o.start_datetime.date(), o) for o in occurences)
449
        acts_by_date = dict()
450
        for a in acts:
451
            # sanity check
452
            assert a.date not in acts_by_date
453
            acts_by_date[a.date] = a
454
        for a in acts:
455
            o = occurences_by_date.get(a.date)
456
            if o:
457
                o.update_act(a)
458
            else:
459
                if len(a.actvalidationstate_set.all()) > 1:
460
                    a.parent_event = None
461
                    a.save()
462
                else:
463
                    a.delete()
464
        for o in occurences:
465
            if o.start_datetime.date() in acts_by_date:
466
                continue
467
            o.get_or_create_act()
468

    
469
    def today_occurrence(self, today=None, match=False):
470
        '''For virtual occurrences reset the event_ptr_id'''
471
        occurrence = super(EventWithAct, self).today_occurrence(today, match)
472
        if hasattr(occurrence, 'parent'):
473
            old_save = occurrence.save
474
            def save(*args, **kwargs):
475
                occurrence.event_ptr_id = None
476
                old_save(*args, **kwargs)
477
            occurrence.save = save
478
        return occurrence
479

    
480
    def is_event_absence(self):
481
        return self.act.is_absent()
482

    
483
    def __unicode__(self):
484
        kwargs = {
485
                'patient': self.patient,
486
                'start_datetime': self.start_datetime,
487
                'act_type': self.act_type
488
        }
489
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
490
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
491
            '{act_type} ({act_type.id})'.format(**kwargs)
492

    
493

    
494
from django.db.models.signals import m2m_changed
495
from django.dispatch import receiver
496

    
497

    
498
@receiver(m2m_changed, sender=Event.participants.through)
499
def participants_changed(sender, instance, action, **kwargs):
500
    if action.startswith('post'):
501
        workers = [ p.worker for p in instance.participants.prefetch_related('worker') ]
502
        for act in instance.act_set.all():
503
            act.doctors = workers
(7-7/10)