Project

General

Profile

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

calebasse / calebasse / agenda / models.py @ ebddd10a

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 and exception.start_datetime.date() == today():
237
                return exception.today_occurrence(today)
238
            else:
239
                return None
240
        else:
241
            exception_or_self = self.match_date(today)
242
            if exception_or_self is None:
243
                return None
244
            if exception_or_self != self:
245
                return exception_or_self.today_occurrence(today)
246
        if self.recurrence_periodicity is None:
247
            return self
248
        start_datetime = datetime.combine(today, self.start_datetime.timetz())
249
        end_datetime = start_datetime + self.timedelta()
250
        event = copy(self)
251
        event.exception_to = self
252
        event.exception_date = today
253
        event.start_datetime = start_datetime
254
        event.end_datetime = end_datetime
255
        event.recurrence_periodicity = None
256
        event.recurrence_week_offset = 0
257
        event.recurrence_week_period = None
258
        event.recurrence_week_parity = None
259
        event.recurrence_week_rank = None
260
        event.recurrence_end_date = None
261
        event.parent = self
262
        # the returned event is "virtual", it must not be saved
263
        old_save = event.save
264
        old_participants = list(self.participants.all())
265
        def save(*args, **kwargs): 
266
            event.id = None
267
            old_save(*args, **kwargs)
268
            event.participants = old_participants
269
        event.save = save
270
        return event
271

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

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

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

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

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

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

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

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

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

    
355
    def is_event_absence(self):
356
        return False
357

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

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

    
366

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

    
385

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

    
395

    
396
    def delete(self, *args, **kwargs):
397
        if self.recurrence_periodicity is None:
398
            # clear all linked exceptions
399
            pass
400
        elif self.exception_to is not None:
401
            pass
402
        elif hasattr(self, 'parent'):
403
            pass
404
        else:
405
            # clear all linked exceptions
406
            pass
407
        super(EventWithAct, self).delete(*args, **kwargs)
408

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

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

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

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

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

    
477
    def is_event_absence(self):
478
        return self.act.is_absent()
479

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

    
490

    
491
from django.db.models.signals import m2m_changed
492
from django.dispatch import receiver
493

    
494

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