diff --git a/gen/calendar.py b/gen/calendar.py index 6b18c82..3f7e6b7 100644 --- a/gen/calendar.py +++ b/gen/calendar.py @@ -8,7 +8,8 @@ from persistent.list import PersistentList # ------------------------------------------------------------------------------ class Calendar(Type): - '''This field allows to produce an agenda and view/edit events on it.''' + '''This field allows to produce an agenda (monthly view) and view/edit + events on it.''' jsFiles = {'view': ('widgets/calendar.js',)} def __init__(self, eventTypes, eventNameMethod=None, validator=None, @@ -18,7 +19,8 @@ class Calendar(Type): colspan=1, master=None, masterValue=None, focus=False, mapping=None, label=None, maxEventLength=50, otherCalendars=None, additionalInfo=None, startDate=None, - endDate=None, defaultDate=None): + endDate=None, defaultDate=None, preCompute=None, + applicableEvents=None): Type.__init__(self, validator, (0,1), None, default, False, False, show, page, group, layouts, move, False, False, specificReadPermission, specificWritePermission, @@ -40,8 +42,23 @@ class Calendar(Type): # 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. + # When displaying a given month for this agenda, one may want to + # pre-compute, once for the whole month, some information that will then + # be given as arg for other methods specified in subsequent parameters. + # This mechanism exists for performance reasons, to avoid recomputing + # this global information several times. If you specify a method in + # p_preCompute, it will be called every time a given month is shown, and + # will receive 2 args: the first day of the currently shown month (as a + # DateTime instance) and the grid of all shown dates (as a list of lists + # of DateTime instances, one sub-list by row in the month view). This + # grid may hold a little more than dates of the current month. + # Subsequently, the return of your method will be given as arg to other + # methods that you may specify as args of other parameters of this + # Calendar class (see comments below). + self.preCompute = preCompute + # If a method is specified in the following parameters, it must accept + # a single arg (the result of self.preCompute) and 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 @@ -52,11 +69,11 @@ class Calendar(Type): # leading "#" when relevant) into which events of the calendar must # appear. self.otherCalendars = otherCalendars - # One may want to add custom information in the calendar. When a method - # is given in p_additionalInfo, for every cell of the month view, this - # method will be called with a single arg (the cell's date). The - # method's result (a string that can hold text or a chunk of XHTML) will - # be inserted in the cell. + # One may want to add, day by day, custom information in the calendar. + # When a method is given in p_additionalInfo, for every cell of the + # month view, this method will be called with 2 args: the cell's date + # and the result of self.preCompute. The method's result (a string that + # can hold text or a chunk of XHTML) will be inserted in the cell. self.additionalInfo = additionalInfo # 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 @@ -71,6 +88,23 @@ class Calendar(Type): # date is specified, it will be 'now' at the moment the calendar is # shown. self.defaultDate = defaultDate + # For a specific day, all event types may not be applicable. If this is + # the case, one may specify here a method that defines, for a given day, + # a sub-set of all event types. This method must accept 3 args: the day + # in question (as a DateTime instance), the list of all event types, + # which is a copy of the (possibly computed) self.eventTypes) and + # the result of calling self.preCompute. The method must modify + # the 2nd arg and remove from it potentially not applicable events. + # This method can also return a message, that will be shown to the user + # for explaining him why he can, for this day, only create events of a + # sub-set of the possible event types (or even no event at all). + self.applicableEvents = applicableEvents + + def getPreComputedInfo(self, obj, monthDayOne, grid): + '''Returns the result of calling self.preComputed, or None if no such + method exists.''' + if self.preCompute: + return self.preCompute(obj.appy(), monthDayOne, grid) def getSiblingMonth(self, month, prevNext): '''Gets the next or previous month (depending of p_prevNext) relative @@ -127,22 +161,22 @@ class Calendar(Type): currentDay = currentDay + 1 return res - def getOtherCalendars(self, obj): + def getOtherCalendars(self, obj, preComputed): '''Returns the list of other calendars whose events must also be shown on this calendar.''' if self.otherCalendars: - res = self.callMethod(obj, self.otherCalendars) + res = self.callMethod(obj, self.otherCalendars, preComputed) # 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 getAdditionalInfoAt(self, obj, date): + def getAdditionalInfoAt(self, obj, date, preComputed): '''If the user has specified a method in self.additionalInfo, we call it for displaying this additional info in the calendar, at some p_date.''' if not self.additionalInfo: return - return self.additionalInfo(obj.appy(), date) + return self.additionalInfo(obj.appy(), date, preComputed) def getEventTypes(self, obj): '''Returns the (dynamic or static) event types as defined in @@ -152,6 +186,32 @@ class Calendar(Type): else: return self.eventTypes + def getApplicableEventsTypesAt(self, obj, date, allEventTypes, preComputed, + forBrowser=False): + '''Returns the event types that are applicable at a given p_date. More + precisely, it returns an object with 2 attributes: + * "events" is the list of applicable event types; + * "message", not empty if some event types are not applicable, + contains a message explaining those event types are + not applicable. + ''' + if not self.applicableEvents: + eventTypes = allEventTypes + message = None + else: + eventTypes = allEventTypes[:] + message = self.applicableEvents(obj.appy(), date, eventTypes, + preComputed) + res = Object(eventTypes=eventTypes, message=message) + if forBrowser: + res.eventTypes = ','.join(res.eventTypes) + if not res.message: + res.message = '' + else: + res.message = obj.formatText(res.message, format='js') + return res.__dict__ + return res + def getEventsAt(self, obj, date, asDict=True): '''Returns the list of events that exist at some p_date (=day).''' obj = obj.o # Ensure p_obj is not a wrapper. diff --git a/gen/ui/appy.js b/gen/ui/appy.js index dbf856a..0dc007d 100644 --- a/gen/ui/appy.js +++ b/gen/ui/appy.js @@ -217,12 +217,14 @@ function getSlaveInfo(slave, infoType) { function getMasterValues(master) { // Returns the list of values that p_master currently has. + var res = null; if ((master.tagName == 'INPUT') && (master.type != 'checkbox')) { res = master.value; - if ((res[0] == '(') || (res[0] == '[')) { + if ((res.charAt(0) == '(') || (res.charAt(0) == '[')) { // There are multiple values, split it values = res.substring(1, res.length-1).split(','); res = []; + var v = null; for (var i=0; i < values.length; i++){ v = values[i].replace(' ', ''); res.push(v.substring(1, v.length-1)); diff --git a/gen/ui/widgets/calendar.js b/gen/ui/widgets/calendar.js index b4f655b..7d454b9 100644 --- a/gen/ui/widgets/calendar.js +++ b/gen/ui/widgets/calendar.js @@ -4,15 +4,20 @@ function askMonthView(hookId, objectUrl, fieldName, month) { askAjaxChunk(hookId,'GET',objectUrl,'widgets/calendar','viewMonth', params); } -function openEventPopup(action, fieldName, day, spansDays) { +function openEventPopup(action, fieldName, day, spansDays, + applicableEventTypes, message) { /* Opens the popup for creating (or deleting, depending on p_action) a calendar event at some p_day. When action is "del", we need to know (from p_spansDays) if the event spans more days, in order to propose a - checkbox allowing to delete events for those successive days. */ + checkbox allowing to delete events for those successive days. When action + is "new", a possibly restricted list of applicable event types for this + day is given in p_applicableEventTypes; p_message contains an optional + message explaining why not applicable types are not applicable. */ var prefix = fieldName + '_' + action + 'Event'; var f = document.getElementById(prefix + 'Form'); f.day.value = day; if (action == 'del') { + // Show or hide the checkbox for deleting the event for successive days. var elem = document.getElementById(prefix + 'DelNextEvent'); var cb = elem.getElementsByTagName('input'); cb[0].checked = false; @@ -20,6 +25,32 @@ function openEventPopup(action, fieldName, day, spansDays) { if (spansDays == 'True') { elem.style.display = 'block' } else { elem.style.display = 'none' } } + else if (action == 'new') { + // First: reinitialise input fields + f.eventType.style.background = ''; + var allOptions = f.eventType.options; + for (var i=0; i < allOptions.length; i++) { + allOptions[i].selected = false; + } + f.eventSpan.style.background = ''; + // Among all event types, show applicable ones and hide the others. + var applicable = applicableEventTypes.split(','); + var applicableDict = {}; + for (var i=0; i < applicable.length; i++) { + applicableDict[applicable[i]] = true; + } + for (var i=0; i < allOptions.length; i++) { + if (!allOptions[i].value) continue; + if (allOptions[i].value in applicableDict) { + allOptions[i].disabled = false; + allOptions[i].title = ''; + } + else { + allOptions[i].disabled = true; + allOptions[i].title = message; + } + } + } openPopup(prefix + 'Popup'); } @@ -29,6 +60,11 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, maxEventLeng var prefix = fieldName + '_' + action + 'Event'; var f = document.getElementById(prefix + 'Form'); if (action == 'new') { + // Check that an event span has been specified + if (f.eventType.selectedIndex == 0) { + f.eventType.style.background = wrongTextInput; + return; + } // Check that eventSpan is empty or contains a valid number var spanNumber = f.eventSpan.value.replace(' ', ''); if (spanNumber) { diff --git a/gen/ui/widgets/calendar.pt b/gen/ui/widgets/calendar.pt index d89c805..4825d1f 100644 --- a/gen/ui/widgets/calendar.pt +++ b/gen/ui/widgets/calendar.pt @@ -5,9 +5,11 @@ month request/month; monthDayOne python: DateTime('%s/01' % month); today python: DateTime('00:00'); + grid python: contextObj.callField(fieldName, 'getMonthGrid', month); + allEventTypes python: contextObj.callField(fieldName, 'getEventTypes', contextObj); + preComputed python: contextObj.callField(fieldName, 'getPreComputedInfo', contextObj, monthDayOne, grid); defaultDate python: contextObj.callField(fieldName, 'getDefaultDate', contextObj); defaultDateMonth python: defaultDate.strftime('%Y/%m'); - grid python: contextObj.callField(fieldName, 'getMonthGrid', month); previousMonth python: contextObj.callField(fieldName, 'getSiblingMonth', month, 'previous'); nextMonth python: contextObj.callField(fieldName, 'getSiblingMonth', month, 'next'); widget python: contextObj.getAppyType(fieldName, asDict=True); @@ -15,7 +17,7 @@ objUrl contextObj/absolute_url; startDate python: contextObj.callField(fieldName, 'getStartDate', contextObj); endDate python: contextObj.callField(fieldName, 'getEndDate', contextObj); - otherCalendars python: contextObj.callField(fieldName, 'getOtherCalendars', contextObj);" + otherCalendars python: contextObj.callField(fieldName, 'getOtherCalendars', contextObj, preComputed);" tal:attributes="id ajaxHookId">