This is a snapshot of Indico's old Trac site. Any information contained herein is most probably outdated. Access our new GitHub site here.

Ticket #126: schedule.py

File schedule.py, 16.0 KB (added by sylvestre, 6 years ago)

The Wrong file

Line 
1# -*- coding: utf-8 -*-
2##
3## $Id: schedule.py,v 1.75 2009/06/19 14:51:33 pferreir Exp $
4##
5## This file is part of CDS Indico.
6## Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007 CERN.
7##
8## CDS Indico is free software; you can redistribute it and/or
9## modify it under the terms of the GNU General Public License as
10## published by the Free Software Foundation; either version 2 of the
11## License, or (at your option) any later version.
12##
13## CDS Indico is distributed in the hope that it will be useful, but
14## WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16## General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with CDS Indico; if not, write to the Free Software Foundation, Inc.,
20## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
21
22"""
23"""
24import copy
25from persistent import Persistent
26from datetime import datetime,timedelta
27from MaKaC.common.Counter import Counter
28from MaKaC.errors import MaKaCError, TimingError, ParentTimingError,\
29    EntryTimingError
30from MaKaC.common import utils
31from MaKaC.trashCan import TrashCanManager
32from MaKaC.i18n import _
33from pytz import timezone
34from MaKaC.common.utils import daysBetween
35from MaKaC.common.PickleJar import Retrieves
36from MaKaC.common.PickleJar import DictPickler
37from MaKaC.common.Conversion import Conversion
38from MaKaC.common.contextManager import ContextManager
39
40class Schedule:
41    """base schedule class. Do NOT instantiate
42    """
43
44    def __init__( self, owner ):
45        pass
46
47    def getEntries( self ):
48        return []
49
50    def addEntry( self, entry, position=None ):
51        return
52
53    def removeEntry( self, entry ):
54        return
55
56    def getEntryPosition( self, entry ):
57        return
58
59    def moveEntry( self, entry, newPosition, after=1 ):
60        return
61
62    def getEntryLocator( self, entry ):
63        return
64
65    def getOwner( self ):
66        return
67
68    def reSchedule( self ):
69        return
70
71    def getEntryInPos( self, pos ):
72        return None
73
74
75class TimeSchedule(Schedule, Persistent):
76    """
77    """
78
79    def __init__(self,owner):
80        self._entries=[]
81        self._owner=owner
82        self._entryGen=Counter()
83        self._v_allowReSchedule=True
84        self._allowParallel=True
85
86    def notifyModification(self):
87        self.getOwner().notifyModification()
88
89    @Retrieves(['MaKaC.schedule.TimeSchedule',
90                'MaKaC.schedule.ConferenceSchedule'], 'entries', isPicklableObject=True)
91    def getEntries( self ):
92        return self._entries
93
94    def hasEntriesBefore(self,d):
95        """Tells wether there is any entry before the specified date
96        """
97        entries=self.getEntries()
98        if len(entries)==0:
99            return False
100        return entries[0].getStartDate()<d
101
102    def hasEntriesAfter(self,d):
103        """Tells wether there is any entry after the specified date
104        """
105        entries=self.getEntries()
106        if len(entries)==0:
107            return False
108        return self.calculateEndDate()>d
109
110    def checkSanity( self ):
111        if self.hasEntriesBefore(self.getStartDate()) or self.hasEntriesAfter(self.getEndDate()):
112            raise TimingError("Sorry, cannot perform this date change: Some entries in the timetable would be outside the new dates [conference id => %s ]"%(self.getOwner().getConference().getId()))
113
114    def isOutside(self,entry):
115        """Tells whether an entry is outside the date boundaries of the schedule
116        """
117        ######################################
118        # Fermi timezone awareness           #
119        ######################################
120        if entry.getStartDate() is not None:
121            if entry.getStartDate()<self.getStartDate('UTC') or \
122                    entry.getStartDate()>self.getEndDate('UTC'):
123                return True
124        if entry.getEndDate() is not None:
125            if entry.getEndDate()<self.getStartDate('UTC') or \
126                    entry.getEndDate()>self.getEndDate('UTC'):
127                return True
128        return False
129
130    def hasEntry(self,entry):
131        return entry.isScheduled() and entry.getSchedule()==self and\
132            entry in self._entries
133
134    def _addEntry(self,entry,check=2):
135        """check parameter:
136            0: no check at all
137            1: check and raise error in case of problem
138            2: check and adapt the owner dates"""
139
140        if entry.isScheduled():
141            # remove it from the old schedule and add it to this one
142            entry.getSchedule().removeEntry(entry)
143
144        owner = self.getOwner()
145        tz = owner.getConference().getTimezone()
146
147        # If user has entered start date use these dates
148        # if the entry has not a pre-defined start date we try to find a place
149        # within the schedule to allocate it
150        if entry.getStartDate() is None:
151            sDate=self.findFirstFreeSlot(entry.getDuration())
152            if sDate is None:
153                if check==2:
154                    newEndDate = self.getEndDate() + entry.getDuration()
155
156                    ContextManager.get('autoOps').append((owner,
157                                                          "OWNER_END_DATE_EXTENDED",
158                                                          owner,
159                                                          newEndDate.astimezone(timezone(tz))))
160
161                    owner.setEndDate(newEndDate, check)
162                    sDate = self.findFirstFreeSlot(entry.getDuration())
163                if sDate is None:
164                    raise ParentTimingError( _("There is not enough time found to add this entry in the schedule (duration: %s)")%entry.getDuration(), _("Add Entry"))
165            entry.setStartDate(sDate)
166        #if the entry has a pre-defined start date we must make sure that it is
167        #   not outside the boundaries of the schedule
168        else:
169            if entry.getStartDate() < self.getStartDate('UTC'):
170                if check==1:
171                    raise TimingError( _("Cannot schedule this entry because its start date (%s) is before its parents (%s)")%(entry.getStartDate(),self.getStartDate('UTC')),_("Add Entry"))
172                elif check == 2:
173                    ContextManager.get('autoOps').append((owner,
174                                                          "OWNER_START_DATE_EXTENDED",
175                                                          owner,
176                                                          entry.getAdjustedStartDate(tz=tz)))
177                    owner.setStartDate(entry.getStartDate(),check)
178            elif entry.getEndDate()>self.getEndDate('UTC'):
179                if check==1:
180                    raise TimingError( _("Cannot schedule this entry because its end date (%s) is after its parents (%s)")%(entry.getEndDate(),self.getEndDate('UTC')),_("Add Entry"))
181                elif check == 2:
182                    ContextManager.get('autoOps').append((owner,
183                                                          "OWNER_END_DATE_EXTENDED",
184                                                          owner,
185                                                          entry.getAdjustedEndDate(tz=tz)))
186                    owner.setEndDate(entry.getEndDate(),check)
187        #we make sure the entry end date does not go outside the schedule
188        #   boundaries
189        if entry.getEndDate() is not None and \
190                (entry.getEndDate()<self.getStartDate('UTC') or \
191                entry.getEndDate()>self.getEndDate('UTC')):
192            raise TimingError( _("Cannot schedule this entry because its end date (%s) is after its parents (%s)")%(entry.getEndDate(),self.getEndDate()), _("Add Entry"))
193        self._entries.append(entry)
194        entry.setSchedule(self,self._getNewEntryId())
195        self.reSchedule()
196        self._p_changed = 1
197
198    def _setEntryDuration(self,entry):
199        if entry.getDuration() is None:
200            entry.setDuration(0,5)
201
202    def addEntry(self,entry):
203        if (entry is None) or self.hasEntry(entry):
204            return
205        self._setEntryDuration(entry)
206        result = self._addEntry(entry)
207        self._p_changed = 1
208        return result
209
210    def _removeEntry(self,entry):
211        self._entries.remove(entry)
212        entry.setSchedule(None,"")
213        entry.setStartDate(None)
214        entry.delete()
215        self._p_changed = 1
216
217    def removeEntry(self,entry):
218        if entry is None or not self.hasEntry(entry):
219            return
220        self._removeEntry(entry)
221
222    def getEntryPosition( self, entry ):
223        return self._entries.index( entry )
224
225    def getOwner( self ):
226        return self._owner
227
228    ####################################
229    # Fermi timezone awareness         #
230    ####################################
231
232    def getStartDate( self ,tz='UTC'):
233        return self.getOwner().getAdjustedStartDate(tz)
234
235    @Retrieves(['MaKaC.schedule.TimeSchedule',
236                'MaKaC.schedule.ConferenceSchedule'], 'startDate', Conversion.datetime)
237    def getAdjustedStartDate( self, tz=None ):
238        return self.getOwner().getAdjustedStartDate(tz)
239
240    def getEndDate( self, tz='UTC'):
241        return self.getOwner().getAdjustedEndDate(tz)
242
243    @Retrieves(['MaKaC.schedule.TimeSchedule',
244                'MaKaC.schedule.ConferenceSchedule'], 'endDate', Conversion.datetime)
245    def getAdjustedEndDate( self, tz=None):
246        return self.getOwner().getAdjustedEndDate(tz)
247
248    ####################################
249    # Fermi timezone awareness(end)    #
250    ####################################
251
252    def cmpEntries(self,e1,e2):
253        return cmp(e1.getStartDate(),e2.getStartDate())
254
255    def reSchedule(self):
256        try:
257            if self._allowParalell:
258                pass
259        except AttributeError:
260            self._allowParallel=True
261        try:
262            if self._v_allowReSchedule:
263                pass
264        except AttributeError:
265            self._v_allowReSchedule=True
266        if self._v_allowReSchedule:
267            self._v_allowReSchedule=False
268            self._entries.sort(self.cmpEntries)
269            lastEntry=None
270            for entry in self._entries:
271                if lastEntry is not None:
272                    if not self._allowParallel:
273                        if lastEntry.collides(entry):
274                            entry.setStartDate(lastEntry.getEndDate())
275                lastEntry=entry
276            self._v_allowReSchedule=True
277        self._p_changed = 1
278
279    def calculateEndDate( self ):
280        if len(self._entries) == 0:
281            return self.getStartDate()
282        eDate = self.getStartDate()
283        for entry in self._entries:
284            if entry.getEndDate()>eDate:
285                eDate = entry.getEndDate()
286        return eDate
287
288    def calculateStartDate( self ):
289        if len(self._entries) == 0:
290            return self.getStartDate()
291        else:
292            return self._entries[0].getStartDate()
293
294    def getTimezone( self ):
295        return self.getOwner().getConference().getTimezone()
296
297    def getFirstFreeSlotOnDay(self,day):
298        if not day.tzinfo:
299            day = timezone(self.getTimezone()).localize(day)
300        tz = day.tzinfo
301        entries = self.getEntriesOnDay(day)
302        if len(entries)==0:
303            if self.getStartDate().astimezone(tz).date() == day.date():
304                return self.getStartDate().astimezone(tz)
305            return day.astimezone(timezone(self.getTimezone())).replace(hour=8,minute=0).astimezone(tz)
306        else:
307            return self.calculateDayEndDate(day)
308
309    def calculateDayEndDate(self,day,hour=0,min=0):
310        if day is None:
311            return self.calculateEndDate()
312        if not day.tzinfo:
313            day = timezone(self.getTimezone()).localize(day)
314        tz = day.tzinfo
315        maxDate=day.replace(hour=hour,minute=min)
316        entries = self.getEntriesOnDay(day)
317        if hour != 0 or min != 0:
318            return maxDate
319        elif len(entries)==0:
320            confstime = self.getOwner().getAdjustedStartDate()
321            return day.astimezone(timezone(self.getTimezone())).replace(hour=confstime.hour,minute=confstime.minute).astimezone(tz)
322        else:
323            for entry in entries:
324                if entry.getEndDate()>maxDate:
325                    maxDate=entry.getEndDate().astimezone(tz)
326            if maxDate.date() != day.date():
327                maxDate = day.replace(hour=23,minute=59)
328            return maxDate
329
330    def calculateDayStartDate( self, day ):
331        #
332        # This determines where the times start on the time table.
333        # day is a tz aware datetime
334        if not day.tzinfo:
335            day = timezone(self.getTimezone()).localize(day)
336        tz = day.tzinfo
337        entries = self.getEntriesOnDay(day)
338        if len(entries) == 0:
339            return timezone(self.getTimezone()).localize(datetime(day.year,day.month,day.day,8,0)).astimezone(tz)
340        else:
341            for entry in entries:
342                if entry.getStartDate().astimezone(tz).date() >= day.date():
343                    return entry.getStartDate().astimezone(tz)
344                else:
345                    return day.replace(hour=0,minute=0)
346
347    def getEntryInPos( self, pos ):
348        try:
349            return self.getEntries()[int(pos)]
350        except IndexError:
351            return None
352
353    def getEntriesOnDay( self, day ):
354        """Returns a list containing all the entries which occur whithin the
355            specified day. These entries will be ordered descending.
356        """
357        if not day.tzinfo:
358            day = timezone(self.getTimezone()).localize(day)
359        res = []
360        for entry in self.getEntries():
361            if entry.inDay( day ):
362                res.append( entry )
363        return res
364
365    def getEntriesOnDate( self, date ):
366        """Returns a list containing all the entries which occur whithin the
367            specified day. These entries will be ordered descending.
368        """
369        res = []
370        for entry in self.getEntries():
371            if entry.onDate( date ):
372                res.append( entry )
373        return res
374
375    def _getNewEntryId(self):
376        try:
377            if self._entryGen:
378                pass
379        except AttributeError:
380            self._entryGen=Counter()
381        return str(self._entryGen.newCount())
382
383    def getEntryById(self,id):
384        for entry in self.getEntries():
385            if entry.getId()==str(id).strip():
386                return entry
387        return None
388
389    def hasGap(self):
390        """check if schedule has gap between two entries"""
391        entries = self.getEntries()
392        if len(entries) > 1:
393            sDate = self.getStartDate('UTC')
394            for entry in entries:
395                if entry.getStartDate()!=sDate:
396                    return True
397                sDate = entry.getEndDate()
398        return False
399
400    def compact(self):
401        """removes any overlaping among schedule entries and make them go one
402            after the other without any gap
403        """
404        self._v_allowReSchedule=False
405        refDate=self.getStartDate('UTC')
406        for entry in self._entries:
407            entry.setStartDate(refDate)
408            refDate=entry.getEndDate()
409        self._v_allowReSchedule=True
410
411    def moveUpEntry(self,entry,tz=None):
412        pass
413
414    def moveDownEntry(self,entry,tz=None):
415        pass
416
417    def rescheduleTimes(self, type, diff, tz, day=None):
418        pass
419
420    def clear(self):
421        while len(self._entries)>0:
422            self._removeEntry(self._entries[0])
423        self._p_changed = 1
424
425
426    def findFirstFreeSlot(self,reqDur=None):
427        """Tries to find the first free time slot available where an entry with
428            the specified duration could be placed
429        """
430        d=self.getStartDate('UTC')
431        for entry in self.getEntries():
432            availDur=entry.getStartDate()-d
433            if availDur!=0:
434                if reqDur is not None and reqDur!=0:
435                    if reqDur<=availDur:
436                        return d
437                else:
438                    return d
439            d=entry.getEndDate()
440        availDur=self.getEndDate()-d
441        if availDur!=0:
442            if reqDur is not None and reqDur!=0:
443                if reqDur<=availDur:
444                    return d
445            else:
446                return d
447