Revision 76974b6f
Added by Benjamin Dauvergne almost 12 years ago
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
agenda/actes/dossiers: move Occurence fields into Event, add recurring events support