[gen] Bugfix for IE in master/slave relationships. Calendar field: added 2 params: 'preCompute' allows to specify a method that is called once every time a month is shown and whose result can be accessed by other methods; 'applicableEvents' allows to specify, for every day, a list of applicable events which can be a sub-set of all aplicable events (or event nothing).

This commit is contained in:
Gaetan Delannay 2012-10-30 05:53:45 +01:00
parent caca61516f
commit 1505264887
4 changed files with 130 additions and 24 deletions

View file

@ -8,7 +8,8 @@ from persistent.list import PersistentList
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Calendar(Type): 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',)} jsFiles = {'view': ('widgets/calendar.js',)}
def __init__(self, eventTypes, eventNameMethod=None, validator=None, def __init__(self, eventTypes, eventNameMethod=None, validator=None,
@ -18,7 +19,8 @@ class Calendar(Type):
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
mapping=None, label=None, maxEventLength=50, mapping=None, label=None, maxEventLength=50,
otherCalendars=None, additionalInfo=None, startDate=None, 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, Type.__init__(self, validator, (0,1), None, default, False, False,
show, page, group, layouts, move, False, False, show, page, group, layouts, move, False, False,
specificReadPermission, specificWritePermission, specificReadPermission, specificWritePermission,
@ -40,8 +42,23 @@ class Calendar(Type):
# It is not possible to create events that span more days than # It is not possible to create events that span more days than
# maxEventLength. # maxEventLength.
self.maxEventLength = maxEventLength self.maxEventLength = maxEventLength
# If a method is specified in the following parameters, it must return # When displaying a given month for this agenda, one may want to
# a list of calendars whose events must be shown within this agenda. # 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] # Every element in this list must be a sub-list [object, name, color]
# (not a tuple): # (not a tuple):
# - object must refer to the other object on which the other calendar # - 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 # leading "#" when relevant) into which events of the calendar must
# appear. # appear.
self.otherCalendars = otherCalendars self.otherCalendars = otherCalendars
# One may want to add custom information in the calendar. When a method # One may want to add, day by day, custom information in the calendar.
# is given in p_additionalInfo, for every cell of the month view, this # When a method is given in p_additionalInfo, for every cell of the
# method will be called with a single arg (the cell's date). The # month view, this method will be called with 2 args: the cell's date
# method's result (a string that can hold text or a chunk of XHTML) will # and the result of self.preCompute. The method's result (a string that
# be inserted in the cell. # can hold text or a chunk of XHTML) will be inserted in the cell.
self.additionalInfo = additionalInfo self.additionalInfo = additionalInfo
# One may limit event encoding and viewing to a limited period of time, # 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 # 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 # date is specified, it will be 'now' at the moment the calendar is
# shown. # shown.
self.defaultDate = defaultDate 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): def getSiblingMonth(self, month, prevNext):
'''Gets the next or previous month (depending of p_prevNext) relative '''Gets the next or previous month (depending of p_prevNext) relative
@ -127,22 +161,22 @@ class Calendar(Type):
currentDay = currentDay + 1 currentDay = currentDay + 1
return res return res
def getOtherCalendars(self, obj): def getOtherCalendars(self, obj, preComputed):
'''Returns the list of other calendars whose events must also be shown '''Returns the list of other calendars whose events must also be shown
on this calendar.''' on this calendar.'''
if self.otherCalendars: if self.otherCalendars:
res = self.callMethod(obj, self.otherCalendars) res = self.callMethod(obj, self.otherCalendars, preComputed)
# Replace field names with field objects # Replace field names with field objects
for i in range(len(res)): for i in range(len(res)):
res[i][1] = res[i][0].getField(res[i][1]) res[i][1] = res[i][0].getField(res[i][1])
return res 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 '''If the user has specified a method in self.additionalInfo, we call
it for displaying this additional info in the calendar, at some it for displaying this additional info in the calendar, at some
p_date.''' p_date.'''
if not self.additionalInfo: return if not self.additionalInfo: return
return self.additionalInfo(obj.appy(), date) return self.additionalInfo(obj.appy(), date, preComputed)
def getEventTypes(self, obj): def getEventTypes(self, obj):
'''Returns the (dynamic or static) event types as defined in '''Returns the (dynamic or static) event types as defined in
@ -152,6 +186,32 @@ class Calendar(Type):
else: else:
return self.eventTypes 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): def getEventsAt(self, obj, date, asDict=True):
'''Returns the list of events that exist at some p_date (=day).''' '''Returns the list of events that exist at some p_date (=day).'''
obj = obj.o # Ensure p_obj is not a wrapper. obj = obj.o # Ensure p_obj is not a wrapper.

View file

@ -217,12 +217,14 @@ function getSlaveInfo(slave, infoType) {
function getMasterValues(master) { function getMasterValues(master) {
// Returns the list of values that p_master currently has. // Returns the list of values that p_master currently has.
var res = null;
if ((master.tagName == 'INPUT') && (master.type != 'checkbox')) { if ((master.tagName == 'INPUT') && (master.type != 'checkbox')) {
res = master.value; res = master.value;
if ((res[0] == '(') || (res[0] == '[')) { if ((res.charAt(0) == '(') || (res.charAt(0) == '[')) {
// There are multiple values, split it // There are multiple values, split it
values = res.substring(1, res.length-1).split(','); values = res.substring(1, res.length-1).split(',');
res = []; res = [];
var v = null;
for (var i=0; i < values.length; i++){ for (var i=0; i < values.length; i++){
v = values[i].replace(' ', ''); v = values[i].replace(' ', '');
res.push(v.substring(1, v.length-1)); res.push(v.substring(1, v.length-1));

View file

@ -4,15 +4,20 @@ function askMonthView(hookId, objectUrl, fieldName, month) {
askAjaxChunk(hookId,'GET',objectUrl,'widgets/calendar','viewMonth', params); 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 /* 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 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 (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 prefix = fieldName + '_' + action + 'Event';
var f = document.getElementById(prefix + 'Form'); var f = document.getElementById(prefix + 'Form');
f.day.value = day; f.day.value = day;
if (action == 'del') { if (action == 'del') {
// Show or hide the checkbox for deleting the event for successive days.
var elem = document.getElementById(prefix + 'DelNextEvent'); var elem = document.getElementById(prefix + 'DelNextEvent');
var cb = elem.getElementsByTagName('input'); var cb = elem.getElementsByTagName('input');
cb[0].checked = false; cb[0].checked = false;
@ -20,6 +25,32 @@ function openEventPopup(action, fieldName, day, spansDays) {
if (spansDays == 'True') { elem.style.display = 'block' } if (spansDays == 'True') { elem.style.display = 'block' }
else { elem.style.display = 'none' } 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'); openPopup(prefix + 'Popup');
} }
@ -29,6 +60,11 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl, maxEventLeng
var prefix = fieldName + '_' + action + 'Event'; var prefix = fieldName + '_' + action + 'Event';
var f = document.getElementById(prefix + 'Form'); var f = document.getElementById(prefix + 'Form');
if (action == 'new') { 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 // Check that eventSpan is empty or contains a valid number
var spanNumber = f.eventSpan.value.replace(' ', ''); var spanNumber = f.eventSpan.value.replace(' ', '');
if (spanNumber) { if (spanNumber) {

View file

@ -5,9 +5,11 @@
month request/month; month request/month;
monthDayOne python: DateTime('%s/01' % month); monthDayOne python: DateTime('%s/01' % month);
today python: DateTime('00:00'); 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); defaultDate python: contextObj.callField(fieldName, 'getDefaultDate', contextObj);
defaultDateMonth python: defaultDate.strftime('%Y/%m'); defaultDateMonth python: defaultDate.strftime('%Y/%m');
grid python: contextObj.callField(fieldName, 'getMonthGrid', month);
previousMonth python: contextObj.callField(fieldName, 'getSiblingMonth', month, 'previous'); previousMonth python: contextObj.callField(fieldName, 'getSiblingMonth', month, 'previous');
nextMonth python: contextObj.callField(fieldName, 'getSiblingMonth', month, 'next'); nextMonth python: contextObj.callField(fieldName, 'getSiblingMonth', month, 'next');
widget python: contextObj.getAppyType(fieldName, asDict=True); widget python: contextObj.getAppyType(fieldName, asDict=True);
@ -15,7 +17,7 @@
objUrl contextObj/absolute_url; objUrl contextObj/absolute_url;
startDate python: contextObj.callField(fieldName, 'getStartDate', contextObj); startDate python: contextObj.callField(fieldName, 'getStartDate', contextObj);
endDate python: contextObj.callField(fieldName, 'getEndDate', 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"> tal:attributes="id ajaxHookId">
<script type="text/javascript" <script type="text/javascript"
@ -82,13 +84,17 @@
<span tal:condition="python: day == 1" <span tal:condition="python: day == 1"
tal:content="python: _('month_%s_short' % date.aMonth())"></span> tal:content="python: _('month_%s_short' % date.aMonth())"></span>
<tal:comment replace="nothing">Icon for adding an event</tal:comment> <tal:comment replace="nothing">Icon for adding an event</tal:comment>
<img tal:condition="mayCreate" style="visibility:hidden; cursor:pointer" <tal:create condition="mayCreate">
<img style="visibility:hidden; cursor:pointer"
tal:define="info python: contextObj.callField(fieldName, 'getApplicableEventsTypesAt', contextObj, date, allEventTypes, preComputed, True)"
tal:condition="info/eventTypes"
tal:attributes="src string: $appUrl/ui/plus.png; tal:attributes="src string: $appUrl/ui/plus.png;
onclick python: 'openEventPopup(\'new\',\'%s\',\'%s\')' % (fieldName, dayString)"/> onclick python: 'openEventPopup(\'new\',\'%s\',\'%s\',null,\'%s\',\'%s\')' % (fieldName, dayString, info['eventTypes'], info['message'])"/>
</tal:create>
<tal:comment replace="nothing">Icon for deleting an event</tal:comment> <tal:comment replace="nothing">Icon for deleting an event</tal:comment>
<img tal:condition="mayDelete" style="visibility:hidden; cursor:pointer" <img tal:condition="mayDelete" style="visibility:hidden; cursor:pointer"
tal:attributes="src string: $appUrl/ui/delete.png; tal:attributes="src string: $appUrl/ui/delete.png;
onclick python: 'openEventPopup(\'del\',\'%s\',\'%s\',\'%s\')' % (fieldName, dayString, str(spansDays))"/> onclick python: 'openEventPopup(\'del\',\'%s\',\'%s\',\'%s\',null,null)' % (fieldName, dayString, str(spansDays))"/>
<tal:events condition="events"> <tal:events condition="events">
<tal:comment replace="nothing">A single event is allowed for the moment</tal:comment> <tal:comment replace="nothing">A single event is allowed for the moment</tal:comment>
<div tal:define="eventType python: events[0]['eventType']"> <div tal:define="eventType python: events[0]['eventType']">
@ -105,7 +111,7 @@
</tal:e> </tal:e>
</tal:others> </tal:others>
<tal:comment replace="nothing">Additional info</tal:comment> <tal:comment replace="nothing">Additional info</tal:comment>
<tal:info define="info python: contextObj.callField(fieldName,'getAdditionalInfoAt', contextObj, date)" <tal:info define="info python: contextObj.callField(fieldName,'getAdditionalInfoAt', contextObj, date, preComputed)"
condition="info" replace="structure info"/> condition="info" replace="structure info"/>
</tal:day> </tal:day>
</td> </td>
@ -127,9 +133,11 @@
<input type="hidden" name="actionType" value="createEvent"/> <input type="hidden" name="actionType" value="createEvent"/>
<input type="hidden" name="day"/> <input type="hidden" name="day"/>
<tal:comment replace="nothing">Choose an event type</tal:comment>
<div align="center" style="margin-bottom: 3px" tal:content="python: _('which_event')"></div> <div align="center" style="margin-bottom: 3px" tal:content="python: _('which_event')"></div>
<select name="eventType"> <select name="eventType">
<option tal:repeat="eventType python: contextObj.callField(fieldName, 'getEventTypes', contextObj)" <option value="" tal:content="python: _('choose_a_value')"></option>
<option tal:repeat="eventType allEventTypes"
tal:content="python: contextObj.callField(fieldName, 'getEventName', contextObj, eventType)" tal:content="python: contextObj.callField(fieldName, 'getEventName', contextObj, eventType)"
tal:attributes="value eventType"> tal:attributes="value eventType">
</option> </option>