# ------------------------------------------------------------------------------ import types from appy import Object from appy.gen import Type from DateTime import DateTime from BTrees.IOBTree import IOBTree from persistent.list import PersistentList # ------------------------------------------------------------------------------ class Calendar(Type): '''This field allows to produce an agenda and view/edit events on it.''' jsFiles = {'view': ('widgets/calendar.js',)} def __init__(self, eventTypes, eventNameMethod=None, validator=None, default=None, show='view', page='main', group=None, layouts=None, move=0, specificReadPermission=False, specificWritePermission=False, width=None, height=300, colspan=1, master=None, masterValue=None, focus=False, mapping=None, label=None, maxEventLength=50, otherCalendars=None, startDate=None, endDate=None, defaultDate=None): Type.__init__(self, validator, (0,1), None, default, False, False, show, page, group, layouts, move, False, False, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, False, True, mapping, label) # eventTypes can be a "static" list or tuple of strings that identify # the types of events that are supported by this calendar. It can also # be a method that computes such a "dynamic" list or tuple. When # specifying a static list, an i18n label will be generated for every # event type of the list. When specifying a dynamic list, you must also # give, in p_eventNameMethod, a method that will accept a single arg # (=one of the event types from your dynamic list) and return the "name" # of this event as it must be shown to the user. self.eventTypes = eventTypes self.eventNameMethod = eventNameMethod if (type(eventTypes) == types.FunctionType) and not eventNameMethod: raise Exception("When param 'eventTypes' is a method, you must " \ "give another method in param 'eventNameMethod'.") # It is not possible to create events that span more days than # maxEventLength. self.maxEventLength = maxEventLength # If a method is specified in the following parameters, it must return # a list of calendars whose events must be shown within this agenda. # Every element in this list must be a sub-list [object, name, color] # (not a tuple): # - object must refer to the other object on which the other calendar # field is defined; # - name is the name of the field on this object that stores the # calendar; # - color must be a string containing the HTML color (including the # leading "#" when relevant) into which events of the calendar must # appear. self.otherCalendars = otherCalendars # One may limit event encoding and viewing to a limited period of time, # via p_startDate and p_endDate. Those parameters, if given, must hold # methods accepting no arg and returning a Zope DateTime instance. self.startDate = startDate # Beware: specify an end date with an hour like # DateTime('2012/10/13 23:59:59') to avoid surprises. self.endDate = endDate # If a default date is specified, it must be a method accepting no arg # and returning a DateTime instance. As soon as the calendar is shown, # the month where this date is included will be shown. If not default # date is specified, it will be 'now' at the moment the calendar is # shown. self.defaultDate = defaultDate def getSiblingMonth(self, month, prevNext): '''Gets the next or previous month (depending of p_prevNext) relative to p_month.''' dayOne = DateTime('%s/01' % month) if prevNext == 'previous': refDate = dayOne - 1 elif prevNext == 'next': refDate = dayOne + 33 return refDate.strftime('%Y/%m') weekDays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') def getNamesOfDays(self, obj, short=True): res = [] for day in self.weekDays: if short: suffix = '_short' else: suffix = '' res.append(obj.translate('day_%s%s' % (day, suffix))) return res def getMonthGrid(self, month): '''Creates a list of lists of DateTime objects representing the calendar grid to render for a given p_month.''' # Month is a string "YYYY/mm". currentDay = DateTime('%s/01 12:00' % month) currentMonth = currentDay.month() res = [[]] dayOneNb = currentDay.dow() or 7 # This way, Sunday is 7 and not 0. if dayOneNb != 1: previousDate = DateTime(currentDay) # If the 1st day of the month is not a Monday, start the row with # the last days of the previous month. for i in range(1, dayOneNb): previousDate = previousDate - 1 res[0].insert(0, previousDate) finished = False while not finished: # Insert currentDay in the grid if len(res[-1]) == 7: # Create a new row res.append([currentDay]) else: res[-1].append(currentDay) currentDay = currentDay + 1 if currentDay.month() != currentMonth: finished = True # Complete, if needed, the last row with the first days of the next # month. if len(res[-1]) != 7: while len(res[-1]) != 7: res[-1].append(currentDay) currentDay = currentDay + 1 return res def getOtherCalendars(self, obj): '''Returns the list of other calendars whose events must also be shown on this calendar.''' if self.otherCalendars: res = self.callMethod(obj, self.otherCalendars) # Replace field names with field objects for i in range(len(res)): res[i][1] = res[i][0].getField(res[i][1]) return res def getEventTypes(self, obj): '''Returns the (dynamic or static) event types as defined in self.eventTypes.''' if type(self.eventTypes) == types.FunctionType: return self.eventTypes(obj.appy()) else: return self.eventTypes def getEventsAt(self, obj, date, asDict=True): '''Returns the list of events that exist at some p_date (=day).''' if not hasattr(obj, self.name): return years = getattr(obj, self.name) year = date.year() if year not in years: return months = years[year] month = date.month() if month not in months: return days = months[month] day = date.day() if day not in days: return if asDict: res = [e.__dict__ for e in days[day]] else: res = days[day] return res def hasEventsAt(self, obj, date, otherEvents): '''Returns True if, at p_date, an event is found of the same type as p_otherEvents.''' if not otherEvents: return False events = self.getEventsAt(obj, date, asDict=False) if not events: return False return events[0].eventType == otherEvents[0]['eventType'] def getOtherEventsAt(self, obj, date, otherCalendars): '''Gets events that are defined in p_otherCalendars at some p_date.''' res = [] for o, field, color in otherCalendars: events = field.getEventsAt(o.o, date, asDict=False) if events: eventType = events[0].eventType eventName = field.getEventName(o.o, eventType) info = Object(name=eventName, color=color) res.append(info.__dict__) return res def getEventName(self, obj, eventType): '''Gets the name of the event corresponding to p_eventType as it must appear to the user.''' if self.eventNameMethod: return self.eventNameMethod(obj.appy(), eventType) else: return obj.translate('%s_event_%s' % (self.labelId, eventType)) def getStartDate(self, obj): '''Get the start date for this calendar if defined.''' if self.startDate: return self.startDate(obj.appy()) def getEndDate(self, obj): '''Get the end date for this calendar if defined.''' if self.endDate: return self.endDate(obj.appy()) def getDefaultDate(self, obj): '''Get the default date that must appear as soon as the calendar is shown.''' if self.defaultDate: return self.defaultDate(obj.appy()) else: return DateTime() # Now def createEvent(self, obj, date, handleEventSpan=True): '''Create a new event in the calendar, at some p_date (day). If p_handleEventSpan is True, we will use rq["eventSpan"] and also create the same event for successive days.''' rq = obj.REQUEST year, month, day = date.year(), date.month(), date.day() # Check that the "preferences" dict exists or not. if not hasattr(obj.aq_base, self.name): # 1st level: create a IOBTree whose keys are years. setattr(obj, self.name, IOBTree()) yearsDict = getattr(obj, self.name) # Get the sub-dict storing months for a given year if year in yearsDict: monthsDict = yearsDict[year] else: yearsDict[year] = monthsDict = IOBTree() # Get the sub-dict storing days of a given month if month in monthsDict: daysDict = monthsDict[month] else: monthsDict[month] = daysDict = IOBTree() # Get the list of events for a given day if day in daysDict: events = daysDict[day] else: daysDict[day] = events = PersistentList() # Create and store the event, excepted if an event already exists. if not events: event = Object(eventType=rq['eventType']) events.append(event) # Span the event on the successive days if required if handleEventSpan and rq['eventSpan']: nbOfDays = min(int(rq['eventSpan']), self.maxEventLength) for i in range(nbOfDays): date = date + 1 self.createEvent(obj, date, handleEventSpan=False) def deleteEvent(self, obj, date, handleEventSpan=True): '''Deletes an event. It actually deletes all events at rq['day']. If p_handleEventSpan is True, we will use rq["deleteNext"] to delete successive events, too.''' rq = obj.REQUEST if not self.getEventsAt(obj, date): return daysDict = getattr(obj, self.name)[date.year()][date.month()] # Remember events, in case we must delete similar ones for next days. events = self.getEventsAt(obj, date) del daysDict[date.day()] if handleEventSpan and rq.has_key('deleteNext') and \ (rq['deleteNext'] == 'True'): while True: date = date + 1 if self.hasEventsAt(obj, date, events): self.deleteEvent(obj, date, handleEventSpan=False) else: break def process(self, obj): '''Processes an action coming from the calendar widget, ie, the creation or deletion of a calendar event.''' rq = obj.REQUEST action = rq['actionType'] # Get the date for this action if action == 'createEvent': return self.createEvent(obj, DateTime(rq['day'])) elif action == 'deleteEvent': return self.deleteEvent(obj, DateTime(rq['day'])) # ------------------------------------------------------------------------------