Project

General

Profile

« Previous | Next » 

Revision 76974b6f

Added by Benjamin Dauvergne almost 12 years ago

agenda/actes/dossiers: move Occurence fields into Event, add recurring events support

View differences:

calebasse/agenda/models.py
1 1
# -*- coding: utf-8 -*-
2 2

  
3
from datetime import datetime
4

  
5
from dateutil import rrule
3
from datetime import datetime, date, timedelta
4
from copy import copy
6 5

  
7 6
from django.utils.translation import ugettext_lazy as _
8
from django.contrib.contenttypes.models import ContentType
9
from django.contrib.contenttypes import generic
7
from django.contrib.auth.models import User
10 8
from django.db import models
11 9

  
10
from ..middleware.request import get_request
12 11
from calebasse.agenda import managers
12
from calebasse.utils import weeks_since_epoch
13 13
from interval import Interval
14 14

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

  
22
class Note(models.Model):
23
    '''
24
    A generic model for adding simple, arbitrary notes to other models such as
25
    ``Event`` or ``Occurrence``.
26
    '''
27

  
28
    class Meta:
29
        verbose_name = u'Note'
30
        verbose_name_plural = u'Notes'
31

  
32
    def __unicode__(self):
33
        return self.note
34

  
35
    note = models.TextField(_('note'))
36
    created = models.DateTimeField(_('created'), auto_now_add=True)
37
    content_type = models.ForeignKey(ContentType)
38
    object_id = models.IntegerField()
39

  
40

  
41 21
class EventType(models.Model):
42 22
    '''
43 23
    Simple ``Event`` classifcation.
......
55 35

  
56 36
class Event(models.Model):
57 37
    '''
58
    Container model for general metadata and associated ``Occurrence`` entries.
38
    Container model for general agenda events
59 39
    '''
60 40
    objects = managers.EventManager()
61 41

  
62 42
    title = models.CharField(_('Title'), max_length=32, blank=True)
63 43
    description = models.TextField(_('Description'), max_length=100)
64 44
    event_type = models.ForeignKey(EventType, verbose_name=u"Type d'événement")
65
    notes = generic.GenericRelation(Note, verbose_name=_('notes'))
45
    creator = models.ForeignKey(User, verbose_name=_(u'Créateur'), blank=True, null=True)
46
    create_date = models.DateTimeField(_(u'Date de création'), auto_now_add=True)
66 47

  
67 48
    services = models.ManyToManyField('ressources.Service',
68 49
            null=True, blank=True, default=None)
......
71 52
    room = models.ForeignKey('ressources.Room', blank=True, null=True,
72 53
            verbose_name=u'Salle')
73 54

  
55
    start_datetime = models.DateTimeField(_('Début'), blank=True, null=True, db_index=True)
56
    end_datetime = models.DateTimeField(_('Fin'), blank=True, null=True)
57

  
58
    PERIODS = (
59
            (1, u'Toutes les semaines'),
60
            (2, u'Une semaine sur deux'),
61
            (3, u'Une semaine sur trois'),
62
            (4, 'Une semaine sur quatre'),
63
            (5, 'Une semaine sur cinq')
64
    )
65
    OFFSET = range(0,4)
66
    recurrence_week_day = models.PositiveIntegerField(default=0)
67
    recurrence_week_offset = models.PositiveIntegerField(
68
            choices=zip(OFFSET, OFFSET),
69
            verbose_name=u"Décalage en semaines par rapport au 1/1/1970 pour le calcul de période",
70
            default=0,
71
            db_index=True)
72
    recurrence_week_period = models.PositiveIntegerField(
73
            choices=PERIODS,
74
            verbose_name=u"Période en semaines",
75
            default=None,
76
            blank=True,
77
            null=True,
78
            db_index=True)
79
    recurrence_end_date = models.DateField(
80
            verbose_name=_(u'Fin de la récurrence'),
81
            blank=True, null=True,
82
            db_index=True)
83

  
74 84
    class Meta:
75 85
        verbose_name = u'Evénement'
76 86
        verbose_name_plural = u'Evénements'
77
        ordering = ('title', )
78

  
79

  
80
    def __unicode__(self):
81
        return self.title
82

  
83
    def add_occurrences(self, start_time, end_time, room=None, **rrule_params):
87
        ordering = ('start_datetime', 'end_datetime', 'title')
88

  
89
    def __init__(self, *args, **kwargs):
90
        if kwargs.get('start_datetime') and not kwargs.has_key('recurrence_end_date'):
91
            kwargs['recurrence_end_date'] = kwargs.get('start_datetime').date()
92
        super(Event, self).__init__(*args, **kwargs)
93

  
94
    def clean(self):
95
        '''Initialize recurrence fields if they are not.'''
96
        if self.start_datetime:
97
            if self.recurrence_week_period:
98
                if self.recurrence_end_date and self.recurrence_end_date < self.start_datetime.date():
99
                    self.recurrence_end_date = self.start_datetime.date()
100
                self.recurrence_week_day = self.start_datetime.weekday()
101
                self.recurrence_week_offset = weeks_since_epoch(self.start_datetime) % self.recurrence_week_period
102

  
103
    def timedelta(self):
104
        '''Distance between start and end of the event'''
105
        return self.end_datetime - self.start_datetime
106

  
107
    def today_occurence(self, today=None):
108
        '''For a recurring event compute the today 'Event'.
109

  
110
           The computed event is the fake one that you cannot save to the database.
84 111
        '''
85
        Add one or more occurences to the event using a comparable API to 
86
        ``dateutil.rrule``. 
87

  
88
        If ``rrule_params`` does not contain a ``freq``, one will be defaulted
89
        to ``rrule.DAILY``.
90

  
91
        Because ``rrule.rrule`` returns an iterator that can essentially be
92
        unbounded, we need to slightly alter the expected behavior here in order
93
        to enforce a finite number of occurrence creation.
94

  
95
        If both ``count`` and ``until`` entries are missing from ``rrule_params``,
96
        only a single ``Occurrence`` instance will be created using the exact
97
        ``start_time`` and ``end_time`` values.
112
        today = today or date.today()
113
        if not self.is_recurring():
114
            if today == self.start_datetime.date():
115
                return self
116
            else:
117
                return None
118
        if today.weekday() != self.recurrence_week_day:
119
            return None
120
        if weeks_since_epoch(today) % self.recurrence_week_period != self.recurrence_week_offset:
121
            return None
122
        if not (self.start_datetime.date() <= today <= self.recurrence_end_date):
123
            return None
124
        start_datetime = datetime.combine(today, self.start_datetime.timetz())
125
        end_datetime = start_datetime + self.timedelta()
126
        event = copy(self)
127
        event.start_datetime = start_datetime
128
        event.end_datetime = end_datetime
129
        event.recurrence_end_date = None
130
        event.recurrence_week_offset = None
131
        event.recurrence_week_period = None
132
        event.parent = self
133
        def save(*args, **kwarks): 
134
            raise RuntimeError()
135
        event.save = save
136
        return event
137

  
138
    def next_occurence(self, today=None):
139
        '''Returns the next occurence after today.'''
140
        today = today or date.today()
141
        for occurence in self.all_occurences():
142
            if occurence.start_datetime.date() > today:
143
                return occurence
144

  
145
    def is_recurring(self):
146
        '''Is this event multiple ?'''
147
        return self.recurrence_week_period is not None
148

  
149
    def all_occurences(self, limit=90):
150
        '''Returns all occurences of this event as a list or pair (start_datetime,
151
           end_datetime).
152

  
153
           limit - compute occurrences until limit days in the future
154

  
155
           Default is to limit to 90 days.
98 156
        '''
99
        rrule_params.setdefault('freq', rrule.DAILY)
100

  
101
        if 'count' not in rrule_params and 'until' not in rrule_params:
102
            self.occurrence_set.create(start_time=start_time, end_time=end_time)
157
        if self.recurrence_week_period:
158
            day = self.start_datetime.date()
159
            max_end_date = max(date.today(), self.start_datetime.date()) + timedelta(days=limit)
160
            end_date = min(self.recurrence_end_date or max_end_date, max_end_date)
161
            delta = timedelta(days=self.recurrence_week_period*7)
162
            while day <= end_date:
163
                yield self.today_occurence(day)
164
                day += delta
103 165
        else:
104
            delta = end_time - start_time
105
            for ev in rrule.rrule(dtstart=start_time, **rrule_params):
106
                self.occurrence_set.create(start_time=ev, end_time=ev + delta)
166
            yield self
107 167

  
108
    def upcoming_occurrences(self):
109
        '''
110
        Return all occurrences that are set to start on or after the current
111
        time.
112
        '''
113
        return self.occurrence_set.filter(start_time__gte=datetime.now())
114

  
115
    def next_occurrence(self):
116
        '''
117
        Return the single occurrence set to start on or after the current time
118
        if available, otherwise ``None``.
119
        '''
120
        upcoming = self.upcoming_occurrences()
121
        return upcoming and upcoming[0] or None
122

  
123
    def daily_occurrences(self, dt=None):
124
        '''
125
        Convenience method wrapping ``Occurrence.objects.daily_occurrences``.
126
        '''
127
        return Occurrence.objects.daily_occurrences(dt=dt, event=self)
128

  
129

  
130
class Occurrence(models.Model):
131
    '''
132
    Represents the start end time for a specific occurrence of a master ``Event``
133
    object.
134
    '''
135
    start_time = models.DateTimeField()
136
    end_time = models.DateTimeField()
137
    event = models.ForeignKey('Event', verbose_name=_('event'), editable=False)
138
    notes = models.ManyToManyField('Note', verbose_name=_('notes'),
139
            null=True, blank=True, default=None)
140

  
141
    objects = managers.OccurrenceManager()
168
    def to_interval(self):
169
        return Interval(self.start_datetime, self.end_datetime)
142 170

  
143
    class Meta:
144
        verbose_name = u'Occurrence'
145
        verbose_name_plural = u'Occurrences'
146
        ordering = ('start_time', 'end_time')
171
    def is_event_absence(self):
172
        return False
147 173

  
148 174
    def __unicode__(self):
149
        return u'%s: %s' % (self.title, self.start_time.isoformat())
175
        return self.title
150 176

  
151
    def __cmp__(self, other):
152
        return cmp(self.start_time, other.start_time)
153 177

  
154
    @property
155
    def title(self):
156
        return self.event.title
178
class EventWithActManager(managers.EventManager):
179
    def create_patient_appointment(self, creator, title, patient,
180
            doctors=[], act_type=None, service=None, start_datetime=None, end_datetime=None,
181
            room=None, periodicity=1, until=False):
182
        appointment = self.create_event(creator=creator,
183
                title=title,
184
                event_type=EventType(id=1),
185
                participants=doctors,
186
                services=[service],
187
                start_datetime=start_datetime,
188
                end_datetime=end_datetime,
189
                room=room,
190
                periodicity=periodicity,
191
                until=until,
192
                act_type=act_type,
193
                patient=patient)
194
        return appointment
195

  
196

  
197
class EventWithAct(Event):
198
    '''An event corresponding to an act.'''
199
    objects = EventWithActManager()
200
    act_type = models.ForeignKey('ressources.ActType',
201
        verbose_name=u'Type d\'acte')
202
    patient = models.ForeignKey('dossiers.PatientRecord')
157 203

  
158 204
    @property
159
    def event_type(self):
160
        return self.event.event_type
161

  
162
    def to_interval(self):
163
        return Interval(self.start_time, self.end_time)
205
    def act(self):
206
        return self.get_or_create_act()
207

  
208
    def get_or_create_act(self, today=None):
209
        from ..actes.models import Act, ActValidationState
210
        today = today or self.start_datetime.date()
211
        act, created = Act.objects.get_or_create(patient=self.patient,
212
                parent_event=getattr(self, 'parent', self),
213
                date=today,
214
                act_type=self.act_type)
215
        self.update_act(act)
216
        if created:
217
            ActValidationState.objects.create(act=act, state_name='NON_VALIDE',
218
                author=self.creator, previous_state=None)
219
        return act
220

  
221
    def update_act(self, act):
222
        '''Update an act to match details of the meeting'''
223
        delta = self.timedelta()
224
        duration = delta.seconds // 60
225
        act._duration = duration
226
        act.doctors = self.participants.select_subclasses()
227
        act.act_type = self.act_type
228
        act.patient = self.patient
229
        act.date = self.start_datetime.date()
230
        act.save()
231

  
232
    def save(self, *args, **kwargs):
233
        '''Force event_type to be patient meeting.'''
234
        self.clean()
235
        self.event_type = EventType(id=1)
236
        super(EventWithAct, self).save(*args, **kwargs)
237
        # list of occurences may have changed
238
        from ..actes.models import Act
239
        occurences = list(self.all_occurences())
240
        acts = Act.objects.filter(parent_event=self)
241
        occurences_by_date = dict((o.start_datetime.date(), o) for o in occurences)
242
        acts_by_date = dict()
243
        for a in acts:
244
            # sanity check
245
            assert a.date not in acts_by_date
246
            acts_by_date[a.date] = a
247
        for a in acts:
248
            o = occurences_by_date.get(a.date)
249
            if o:
250
                o.update_act(a)
251
            else:
252
                if len(a.actvalidationstate_set.all()) > 1:
253
                    a.parent_event = None
254
                    a.save()
255
                else:
256
                    a.delete()
257
        participants = self.participants.select_subclasses()
258
        for o in occurences:
259
            if o.start_datetime.date() in acts_by_date:
260
                continue
261
            o.get_or_create_act()
164 262

  
165 263
    def is_event_absence(self):
166
        if self.event.event_type.id != 1:
167
            return False
168
        event_act = self.event.eventact
169
        return event_act.is_absent()
264
        return self.act.is_absent()
265

  
266
    def __unicode__(self):
267
        kwargs = {
268
                'patient': self.patient,
269
                'start_datetime': self.start_datetime,
270
                'act_type': self.act_type
271
        }
272
        kwargs['doctors'] = ', '.join(map(unicode, self.participants.all())) if self.id else ''
273
        return u'Rdv le {start_datetime} de {patient} avec {doctors} pour ' \
274
            '{act_type} ({act_type.id})'.format(**kwargs)

Also available in: Unified diff