[gen] Calendar field: allow to have several events at the same day via the concept of timeslots (ongoing work).

This commit is contained in:
Gaetan Delannay 2015-02-27 15:57:42 +01:00
parent 0c706c695e
commit da8f7a5bcd
10 changed files with 350 additions and 144 deletions

View file

@ -8,6 +8,24 @@ from appy.px import Px
from DateTime import DateTime from DateTime import DateTime
from BTrees.IOBTree import IOBTree from BTrees.IOBTree import IOBTree
from persistent.list import PersistentList from persistent.list import PersistentList
from persistent import Persistent
# ------------------------------------------------------------------------------
class Timeslot:
'''A timeslot defines a time range within a single day'''
def __init__(self, id, start=None, end=None, name=None, eventTypes=None):
# A short, human-readable string identifier, unique among all timeslots
# for a given Calendar. Id "main" is reserved for the main timeslot that
# represents the whole day.
self.id = id
# The time range can be defined by p_start ~(i_hour, i_minute)~ and
# p_end (idem), or by a simple name, like "AM" or "PM".
self.start = start
self.end = end
self.name = name or id
# The event types (among all event types defined at the Calendar level)
# that can be assigned to this slot.
self.eventTypes = eventTypes # "None" means "all"
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Other: class Other:
@ -49,16 +67,33 @@ class Other:
info.name = eventNames[eventType] info.name = eventNames[eventType]
res.append(info) res.append(info)
# ------------------------------------------------------------------------------
class Event(Persistent):
'''An event as will be stored in the database'''
def __init__(self, eventType, timeslot='main'):
self.eventType = eventType
self.timeslot = timeslot
def getName(self, allEventNames):
'''Gets the name for this event, that depends on it type and may include
the timeslot if not "main".'''
res = allEventNames[self.eventType]
if self.timeslot != 'main': res += ' ' + self.timeslot
return res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Calendar(Field): class Calendar(Field):
'''This field allows to produce an agenda (monthly view) and view/edit '''This field allows to produce an agenda (monthly view) and view/edit
events on it.''' events on it.'''
jsFiles = {'view': ('calendar.js',)} jsFiles = {'view': ('calendar.js',)}
DateTime = DateTime DateTime = DateTime
Other = Other # Access to the Other class via the Calendar class # Access to Calendar utility classes via the Calendar class
Timeslot = Timeslot
Other = Other
Event = Event
IterSub = IterSub IterSub = IterSub
timelineBgColors = {'Fri': '#a6a6a6', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'} timelineBgColors = {'Fri': '#dedede', 'Sat': '#c0c0c0', 'Sun': '#c0c0c0'}
# For timeline rendering, the row displaying month names # For timeline rendering, the row displaying month names
pxTimeLineMonths = Px(''' pxTimeLineMonths = Px('''
@ -120,7 +155,8 @@ class Calendar(Field):
<td></td> <td></td>
</tr> </tr>
<!-- Other calendars --> <!-- Other calendars -->
<tr for="other in field.IterSub(others)" <x for="otherGroup in others">
<tr for="other in otherGroup"
var2="tlName=field.getTimelineName(other)"> var2="tlName=field.getTimelineName(other)">
<td class="tlLeft">::tlName</td> <td class="tlLeft">::tlName</td>
<!-- A cell in this other calendar --> <!-- A cell in this other calendar -->
@ -135,82 +171,21 @@ class Calendar(Field):
</x> </x>
<td class="tlRight">::tlName</td> <td class="tlRight">::tlName</td>
</tr> </tr>
<!-- A separator between groups of other calendars -->
<tr if="not loop.otherGroup.last" height="5px">
<th colspan=":len(grid)+2"></th></tr>
</x>
<!-- Footer (repetition of months and days) --> <!-- Footer (repetition of months and days) -->
<x>:field.pxTimelineDayNumbers</x><x>:field.pxTimelineDayLetters</x> <x>:field.pxTimelineDayNumbers</x><x>:field.pxTimelineDayLetters</x>
<x>:field.pxTimeLineMonths</x> <x>:field.pxTimeLineMonths</x>
</table> </table>
<x>:field.pxTimelineLegend</x>''') <x>:field.pxTimelineLegend</x>''')
# Month view for a calendar # Popup for adding an event in the month view
pxViewMonth = Px(''' pxAddEvent = Px('''
<table cellpadding="0" cellspacing="0" width="100%" class="list" <div var="prefix='%s_newEvent' % field.name;
style="font-size: 95%" popupId=prefix + 'Popup';
var="rowHeight=int(field.height/float(len(grid)))"> showTimeslots=len(field.timeslots) &gt; 2"
<!-- 1st row: names of days -->
<tr height="22px">
<th for="dayId in field.weekDays"
width="14%">:namesOfDays[dayId].short</th>
</tr>
<!-- The calendar in itself -->
<tr for="row in grid" valign="top" height=":rowHeight">
<x for="date in row"
var2="inRange=field.dateInRange(date, startDate, endDate);
cssClasses=field.getCellClass(zobj, date, render, today)">
<!-- Dump an empty cell if we are out of the supported date range -->
<td if="not inRange" class=":cssClasses"></td>
<!-- Dump a normal cell if we are in range -->
<td if="inRange"
var2="events=field.getEventsAt(zobj, date);
spansDays=field.hasEventsAt(zobj, date+1, events);
mayCreate=mayEdit and not events;
mayDelete=mayEdit and events and field.mayDelete(obj,events);
day=date.day();
dayString=date.strftime('%Y/%m/%d');
js=mayEdit and 'toggleVisibility(this, %s)' % q('img') \
or ''"
style=":date.isCurrentDay() and 'font-weight:bold' or \
'font-weight:normal'"
class=":cssClasses" onmouseover=":js" onmouseout=":js">
<span>:day</span>
<span if="day == 1">:_('month_%s_short' % date.aMonth())</span>
<!-- Icon for adding an event -->
<x if="mayCreate">
<img class="clickable" style="visibility:hidden"
var="info=field.getApplicableEventsTypesAt(zobj, date, \
eventTypes, preComputed, True)"
if="info and info.eventTypes" src=":url('plus')"
onclick=":'openEventPopup(%s, %s, %s, null, %s, %s)' % \
(q('new'), q(field.name), q(dayString), q(info.eventTypes),\
q(info.message))"/>
</x>
<!-- Icon for deleting an event -->
<img if="mayDelete" class="clickable" style="visibility:hidden"
src=":url('delete')"
onclick=":'openEventPopup(%s, %s, %s, %s, null, null)' % \
(q('del'), q(field.name), q(dayString), q(spansDays))"/>
<!-- A single event is allowed for the moment -->
<div if="events" var2="eventType=events[0].eventType">
<span style="color: grey">:allEventNames[eventType]</span>
</div>
<!-- Events from other calendars -->
<x if="others"
var2="otherEvents=field.getOtherEventsAt(zobj, date, \
others, allEventNames, render, colors)">
<div style=":'color: %s; font-style: italic' % event.color"
for="event in otherEvents">:event.name</div>
</x>
<!-- Additional info -->
<x var="info=field.getAdditionalInfoAt(zobj, date, preComputed)"
if="info">::info</x>
</td>
</x>
</tr>
</table>
<!-- Popup for creating a calendar event -->
<div if="eventTypes"
var="prefix='%s_newEvent' % field.name;
popupId=prefix + 'Popup'"
id=":popupId" class="popup" align="center"> id=":popupId" class="popup" align="center">
<form id=":prefix + 'Form'" method="post"> <form id=":prefix + 'Form'" method="post">
<input type="hidden" name="fieldName" value=":field.name"/> <input type="hidden" name="fieldName" value=":field.name"/>
@ -222,16 +197,27 @@ class Calendar(Field):
<!-- Choose an event type --> <!-- Choose an event type -->
<div align="center" style="margin-bottom: 3px">:_('which_event')</div> <div align="center" style="margin-bottom: 3px">:_('which_event')</div>
<select name="eventType"> <select name="eventType" style="margin-bottom: 10px">
<option value="">:_('choose_a_value')</option> <option value="">:_('choose_a_value')</option>
<option for="eventType in eventTypes" <option for="eventType in eventTypes"
value=":eventType">:allEventNames[eventType]</option> value=":eventType">:allEventNames[eventType]</option>
</select><br/><br/> </select>
<!-- Choose a timeslot -->
<div if="showTimeslots" style="margin-bottom: 10px">
<span class="discreet">:_('timeslot')</span>
<select if="showTimeslots" name="timeslot">
<option value="main">:_('timeslot_main')</option>
<option for="timeslot in field.timeslots"
if="timeslot.id != 'main'">:timeslot.name</option>
</select>
</div>
<!-- Span the event on several days --> <!-- Span the event on several days -->
<x if="not showTimeslots">
<div align="center" class="discreet" style="margin-bottom: 3px"> <div align="center" class="discreet" style="margin-bottom: 3px">
<span>:_('event_span')</span> <span>:_('event_span')</span>
<input type="text" size="3" name="eventSpan"/> <input type="text" size="3" name="eventSpan"/>
</div> </div>
</x>
<input type="button" <input type="button"
value=":_('object_save')" value=":_('object_save')"
onclick=":'triggerCalendarEvent(%s, %s, %s, %s, \ onclick=":'triggerCalendarEvent(%s, %s, %s, %s, \
@ -241,9 +227,10 @@ class Calendar(Field):
value=":_('object_cancel')" value=":_('object_cancel')"
onclick=":'closePopup(%s)' % q(popupId)"/> onclick=":'closePopup(%s)' % q(popupId)"/>
</form> </form>
</div> </div>''')
<!-- Popup for deleting a calendar event --> # Popup for removing events in the month view
pxDelEvent = Px('''
<div var="prefix='%s_delEvent' % field.name; <div var="prefix='%s_delEvent' % field.name;
popupId=prefix + 'Popup'" popupId=prefix + 'Popup'"
id=":popupId" class="popup" align="center"> id=":popupId" class="popup" align="center">
@ -253,6 +240,7 @@ class Calendar(Field):
<input type="hidden" name="name" value=":field.name"/> <input type="hidden" name="name" value=":field.name"/>
<input type="hidden" name="action" value="process"/> <input type="hidden" name="action" value="process"/>
<input type="hidden" name="actionType" value="deleteEvent"/> <input type="hidden" name="actionType" value="deleteEvent"/>
<input type="hidden" name="timeslot" value="main"/>
<input type="hidden" name="day"/> <input type="hidden" name="day"/>
<div align="center" <div align="center"
style="margin-bottom: 5px">:_('action_confirm')</div> style="margin-bottom: 5px">:_('action_confirm')</div>
@ -275,6 +263,84 @@ class Calendar(Field):
</form> </form>
</div>''') </div>''')
# Month view for a calendar
pxViewMonth = Px('''
<table cellpadding="0" cellspacing="0" width="100%" class="list"
style="font-size: 95%"
var="rowHeight=int(field.height/float(len(grid)))">
<!-- 1st row: names of days -->
<tr height="22px">
<th for="dayId in field.weekDays"
width="14%">:namesOfDays[dayId].short</th>
</tr>
<!-- The calendar in itself -->
<tr for="row in grid" valign="top" height=":rowHeight">
<x for="date in row"
var2="inRange=field.dateInRange(date, startDate, endDate);
cssClasses=field.getCellClass(zobj, date, render, today)">
<!-- Dump an empty cell if we are out of the supported date range -->
<td if="not inRange" class=":cssClasses"></td>
<!-- Dump a normal cell if we are in range -->
<td if="inRange"
var2="events=field.getEventsAt(zobj, date);
single=events and (len(events) == 1);
spansDays=field.hasEventsAt(zobj, date+1, events);
mayCreate=mayEdit and not field.dayIsFull(date, events);
mayDelete=mayEdit and events and field.mayDelete(obj,events);
day=date.day();
dayString=date.strftime('%Y/%m/%d');
js=mayEdit and 'toggleVisibility(this, %s)' % q('img') \
or ''"
style=":date.isCurrentDay() and 'font-weight:bold' or \
'font-weight:normal'"
class=":cssClasses" onmouseover=":js" onmouseout=":js">
<span>:day</span>
<span if="day == 1">:_('month_%s_short' % date.aMonth())</span>
<!-- Icon for adding an event -->
<x if="mayCreate">
<img class="clickable" style="visibility:hidden"
var="info=field.getApplicableEventsTypesAt(zobj, date, \
eventTypes, preComputed, True)"
if="info and info.eventTypes" src=":url('plus')"
onclick=":'openEventPopup(%s, %s, %s, null, null, %s, %s)' % \
(q('new'), q(field.name), q(dayString), q(info.eventTypes),\
q(info.message))"/>
</x>
<!-- Icon for deleting event(s) -->
<img if="mayDelete" class="clickable" style="visibility:hidden"
src=":url(single and 'delete' or 'deleteMany')"
onclick=":'openEventPopup(%s, %s, %s, %s, %s, null, null)' % \
(q('del'), q(field.name), q(dayString), q('main'), q(spansDays))"/>
<!-- Events -->
<x if="events">
<div for="event in events" style="color: grey">
<x>:event.getName(allEventNames)</x>
<!-- Icon for delete this particular event -->
<img if="mayDelete and not single" class="clickable"
src=":url('delete')" style="visibility:hidden"
onclick=":'openEventPopup(%s, %s, %s, %s, null, null, null)'% \
(q('del'), q(field.name), q(dayString), q(event.timeslot))"/>
</div>
</x>
<!-- Events from other calendars -->
<x if="others"
var2="otherEvents=field.getOtherEventsAt(zobj, date, \
others, allEventNames, render, colors)">
<div style=":'color: %s; font-style: italic' % event.color"
for="event in otherEvents">:event.name</div>
</x>
<!-- Additional info -->
<x var="info=field.getAdditionalInfoAt(zobj, date, preComputed)"
if="info">::info</x>
</td>
</x>
</tr>
</table>
<!-- Popups for creating and deleting a calendar event -->
<x if="mayEdit and eventTypes">
<x>:field.pxAddEvent</x><x>:field.pxDelEvent</x></x>''')
pxView = pxCell = Px(''' pxView = pxCell = Px('''
<div var="defaultDate=field.getDefaultDate(zobj); <div var="defaultDate=field.getDefaultDate(zobj);
defaultDateMonth=defaultDate.strftime('%Y/%m'); defaultDateMonth=defaultDate.strftime('%Y/%m');
@ -341,9 +407,9 @@ class Calendar(Field):
colspan=1, master=None, masterValue=None, focus=False, colspan=1, master=None, masterValue=None, focus=False,
mapping=None, label=None, maxEventLength=50, render='month', mapping=None, label=None, maxEventLength=50, render='month',
others=None, timelineName=None, additionalInfo=None, others=None, timelineName=None, additionalInfo=None,
startDate=None, endDate=None, defaultDate=None, colors=None, startDate=None, endDate=None, defaultDate=None, timeslots=None,
showUncolored=False, preCompute=None, applicableEvents=None, colors=None, showUncolored=False, preCompute=None,
view=None, xml=None, delete=True): applicableEvents=None, view=None, xml=None, delete=True):
Field.__init__(self, validator, (0,1), default, show, page, group, Field.__init__(self, validator, (0,1), default, show, page, group,
layouts, move, False, True, False, specificReadPermission, layouts, move, False, True, False, specificReadPermission,
specificWritePermission, width, height, None, colspan, specificWritePermission, width, height, None, colspan,
@ -418,6 +484,14 @@ class Calendar(Field):
# 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
# "timeslots" are a way to define, within a single day, time ranges. It
# must be a list of Timeslot instances (see above). If you define
# timeslots, the first one must be the one representing the whole day
# and must have id "main".
if not timeslots: self.timeslots = [Timeslot('main')]
else:
self.timeslots = timeslots
self.checkTimeslots()
# "colors" must be or return a dict ~{s_eventType: s_color}~ giving a # "colors" must be or return a dict ~{s_eventType: s_color}~ giving a
# color to every event type defined in this calendar or in any calendar # color to every event type defined in this calendar or in any calendar
# from "others". In a timeline, cells are too small to display # from "others". In a timeline, cells are too small to display
@ -442,6 +516,13 @@ class Calendar(Field):
# it must accept an event type as single arg. # it must accept an event type as single arg.
self.delete = delete self.delete = delete
def checkTimeslots(self):
'''Checks whether self.timeslots defines corect timeslots.'''
# The first timeslot must be the global one, named 'main'
if self.timeslots[0].id != 'main':
raise Exception('The first timeslot must have id "main" and is ' \
'the one representing the whole day.')
def getPreComputedInfo(self, obj, monthDayOne, grid): def getPreComputedInfo(self, obj, monthDayOne, grid):
'''Returns the result of calling self.preComputed, or None if no such '''Returns the result of calling self.preComputed, or None if no such
method exists.''' method exists.'''
@ -561,6 +642,15 @@ class Calendar(Field):
if callable(self.colors): return self.colors(obj) if callable(self.colors): return self.colors(obj)
return self.colors return self.colors
def dayIsFull(self, date, events):
'''In the calendar full at p_date? Defined events at this p_date are in
p_events. We check here if the main timeslot is used or if all
others are used.'''
if not events: return
for e in events:
if e.timeslot == 'main': return True
return len(events) == len(self.timeslots)-1
def dateInRange(self, date, startDate, endDate): def dateInRange(self, date, startDate, endDate):
'''Is p_date within the range (possibly) defined for this calendar by '''Is p_date within the range (possibly) defined for this calendar by
p_startDate and p_endDate ?''' p_startDate and p_endDate ?'''
@ -613,6 +703,28 @@ class Calendar(Field):
if not events: return if not events: return
return events[0].eventType return events[0].eventType
def walkEvents(self, obj, callback):
'''Walks on p_obj, the calendar value for this field and calls
p_callback for every day containing events. The callback must accept
3 args: p_obj, the current day (as a DateTime instance) and the list
of events at that day (the database-stored PersistentList
instance). If the callback returns True we stop the walk.'''
obj = obj.o
if not hasattr(obj, self.name): return
# Browse years
years = getattr(obj, self.name)
if not years: return
for year in years.keys():
# Browse this year's months
months = years[year]
for month in months.keys():
# Browse this month's days
days = months[month]
for day in days.keys():
date = DateTime('%d/%d/%d UTC' % (year, month, day))
stop = callback(obj, date, days[day])
if stop: return
def getEventsByType(self, obj, eventType, minDate=None, maxDate=None, def getEventsByType(self, obj, eventType, minDate=None, maxDate=None,
sorted=True, groupSpanned=False): sorted=True, groupSpanned=False):
'''Returns all the events of a given p_eventType. If p_eventType is '''Returns all the events of a given p_eventType. If p_eventType is
@ -669,7 +781,7 @@ class Calendar(Field):
# Filter unwanted events # Filter unwanted events
if eventType and (event.eventType != eventType): if eventType and (event.eventType != eventType):
continue continue
# We have found a event. # We have found a event
date = DateTime('%d/%d/%d UTC' % (year, month, day)) date = DateTime('%d/%d/%d UTC' % (year, month, day))
if groupSpanned: if groupSpanned:
singleRes = [date, None, event] singleRes = [date, None, event]
@ -699,9 +811,9 @@ class Calendar(Field):
def hasEventsAt(self, obj, date, otherEvents): def hasEventsAt(self, obj, date, otherEvents):
'''Returns True if, at p_date, an event is found of the same type as '''Returns True if, at p_date, an event is found of the same type as
p_otherEvents.''' p_otherEvents.'''
if not otherEvents: return False if not otherEvents: return
events = self.getEventsAt(obj, date) events = self.getEventsAt(obj, date)
if not events: return False if not events: return
return events[0].eventType == otherEvents[0].eventType return events[0].eventType == otherEvents[0].eventType
def getOtherEventsAt(self, obj, date, others, eventNames, render, colors): def getOtherEventsAt(self, obj, date, others, eventNames, render, colors):
@ -756,7 +868,7 @@ class Calendar(Field):
'''Get the end date for this calendar if defined''' '''Get the end date for this calendar if defined'''
if self.endDate: if self.endDate:
d = self.endDate(obj.appy()) d = self.endDate(obj.appy())
# Return the end date without hour, in UTC. # Return the end date without hour, in UTC
return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day())) return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day()))
def getDefaultDate(self, obj): def getDefaultDate(self, obj):
@ -767,14 +879,14 @@ class Calendar(Field):
else: else:
return DateTime() # Now return DateTime() # Now
def createEvent(self, obj, date, eventType=None, eventSpan=None, def createEvent(self, obj, date, timeslot, eventType=None, eventSpan=None,
handleEventSpan=True): handleEventSpan=True):
'''Create a new event in the calendar, at some p_date (day). '''Create a new event in the calendar, at some p_date (day).
If p_eventType is given, it is used; else, rq['eventType'] is used. If p_eventType is given, it is used; else, rq['eventType'] is used.
If p_handleEventSpan is True, we will use p_eventSpan (or If p_handleEventSpan is True, we will use p_eventSpan (or
rq["eventSpan"] if p_eventSpan is not given) and also rq["eventSpan"] if p_eventSpan is not given) and also
create the same event for successive days.''' create the same event for successive days.'''
obj = obj.o # Ensure p_obj is not a wrapper. obj = obj.o # Ensure p_obj is not a wrapper
rq = obj.REQUEST rq = obj.REQUEST
# Get values from parameters # Get values from parameters
if not eventType: eventType = rq['eventType'] if not eventType: eventType = rq['eventType']
@ -802,16 +914,27 @@ class Calendar(Field):
events = daysDict[day] events = daysDict[day]
else: else:
daysDict[day] = events = PersistentList() daysDict[day] = events = PersistentList()
# Create and store the event, excepted if an event already exists # Return an error if the creation cannot occur
if not events: for e in events:
event = Object(eventType=eventType) if e.timeslot == timeslot:
events.append(event) return 'An event for this timeslot already exist'
elif e.timeslot == 'main':
return 'No more place for adding this event'
if events and (timeslot == 'main'):
return 'No more place (2) for adding this event'
# Create and store the event
events.append(Event(eventType, timeslot))
# Sort events in the order of timeslots
timeslots = [timeslot.id for timeslot in self.timeslots]
if len(events) > 1:
events.data.sort(key=lambda e: timeslots.index(e.timeslot))
events._p_changed = 1
# Span the event on the successive days if required # Span the event on the successive days if required
if handleEventSpan and eventSpan: if handleEventSpan and eventSpan and (timeslot != 'main'):
nbOfDays = min(int(eventSpan), self.maxEventLength) nbOfDays = min(int(eventSpan), self.maxEventLength)
for i in range(nbOfDays): for i in range(nbOfDays):
date = date + 1 date = date + 1
self.createEvent(obj, date, handleEventSpan=False) self.createEvent(obj, date, timeslot, handleEventSpan=False)
def mayDelete(self, obj, events): def mayDelete(self, obj, events):
'''May the user delete p_events?''' '''May the user delete p_events?'''
@ -819,15 +942,19 @@ class Calendar(Field):
if callable(self.delete): return self.delete(obj, events[0].eventType) if callable(self.delete): return self.delete(obj, events[0].eventType)
return True return True
def deleteEvent(self, obj, date, handleEventSpan=True): def deleteEvent(self, obj, date, timeslot, handleEventSpan=True):
'''Deletes an event. It actually deletes all events at p_date. '''Deletes an event. If t_timeslot is "main", it deletes all events at
If p_handleEventSpan is True, we will use rq["deleteNext"] to p_date, be there a single event on the main timeslot or several
delete successive events, too.''' events on other timeslots. Else, it only deletes the event at
obj = obj.o # Ensure p_obj is not a wrapper. p_timeslot. If p_handleEventSpan is True, we will use
rq["deleteNext"] to delete successive events, too.'''
obj = obj.o # Ensure p_obj is not a wrapper
if not self.getEventsAt(obj, date): return if not self.getEventsAt(obj, date): return
daysDict = getattr(obj, self.name)[date.year()][date.month()] 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) events = self.getEventsAt(obj, date)
if timeslot == 'main':
# Delete all events; delete them also in the following days when
# relevant.
del daysDict[date.day()] del daysDict[date.day()]
rq = obj.REQUEST rq = obj.REQUEST
if handleEventSpan and rq.has_key('deleteNext') and \ if handleEventSpan and rq.has_key('deleteNext') and \
@ -835,9 +962,18 @@ class Calendar(Field):
while True: while True:
date = date + 1 date = date + 1
if self.hasEventsAt(obj, date, events): if self.hasEventsAt(obj, date, events):
self.deleteEvent(obj, date, handleEventSpan=False) self.deleteEvent(obj, date, timeslot,
handleEventSpan=False)
else: else:
break break
else:
# Delete the event at p_timeslot
i = len(events) - 1
while i >= 0:
if events[i].timeslot == timeslot:
del events[i]
break
i -= 1
def process(self, obj): def process(self, obj):
'''Processes an action coming from the calendar widget, ie, the creation '''Processes an action coming from the calendar widget, ie, the creation
@ -846,11 +982,13 @@ class Calendar(Field):
action = rq['actionType'] action = rq['actionType']
# Security check # Security check
obj.mayEdit(self.writePermission, raiseError=True) obj.mayEdit(self.writePermission, raiseError=True)
# Get the date for this action # Get the date and timeslot for this action
date = DateTime(rq['day'])
timeslot = rq.get('timeslot', 'main')
if action == 'createEvent': if action == 'createEvent':
return self.createEvent(obj, DateTime(rq['day'])) return self.createEvent(obj, date, timeslot)
elif action == 'deleteEvent': elif action == 'deleteEvent':
return self.deleteEvent(obj, DateTime(rq['day'])) return self.deleteEvent(obj, date, timeslot)
def getColumnStyle(self, obj, date, render, today): def getColumnStyle(self, obj, date, render, today):
'''What style(s) must apply to the table column representing p_date '''What style(s) must apply to the table column representing p_date

View file

@ -715,6 +715,14 @@ msgstr ""
msgid "del_next_events" msgid "del_next_events"
msgstr "" msgstr ""
#. Default: "Timeslot"
msgid "timeslot"
msgstr ""
#. Default: "All day"
msgid "timeslot_main"
msgstr ""
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "" msgstr ""

View file

@ -715,6 +715,14 @@ msgstr ""
msgid "del_next_events" msgid "del_next_events"
msgstr "" msgstr ""
#. Default: "Timeslot"
msgid "timeslot"
msgstr ""
#. Default: "All day"
msgid "timeslot_main"
msgstr ""
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "" msgstr ""

View file

@ -715,6 +715,14 @@ msgstr ""
msgid "del_next_events" msgid "del_next_events"
msgstr "" msgstr ""
#. Default: "Timeslot"
msgid "timeslot"
msgstr ""
#. Default: "All day"
msgid "timeslot_main"
msgstr ""
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "" msgstr ""

View file

@ -716,6 +716,14 @@ msgstr "Extend the event on the following number of days (leave blank to create
msgid "del_next_events" msgid "del_next_events"
msgstr "Also delete successive events of the same type." msgstr "Also delete successive events of the same type."
#. Default: "Timeslot"
msgid "timeslot"
msgstr "Timeslot"
#. Default: "All day"
msgid "timeslot_main"
msgstr "All day"
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "Inserted by ${userName}" msgstr "Inserted by ${userName}"

View file

@ -715,6 +715,14 @@ msgstr ""
msgid "del_next_events" msgid "del_next_events"
msgstr "" msgstr ""
#. Default: "Timeslot"
msgid "timeslot"
msgstr ""
#. Default: "All day"
msgid "timeslot_main"
msgstr ""
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "" msgstr ""

View file

@ -716,6 +716,14 @@ msgstr "Étendre l'événement sur le nombre de jours suivants (laissez vide pou
msgid "del_next_events" msgid "del_next_events"
msgstr "Supprimer aussi les événements successifs de même type" msgstr "Supprimer aussi les événements successifs de même type"
#. Default: "Timeslot"
msgid "timeslot"
msgstr "Plage horaire"
#. Default: "All day"
msgid "timeslot_main"
msgstr "Toute la journée"
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "Inséré par ${userName}" msgstr "Inséré par ${userName}"

View file

@ -715,6 +715,14 @@ msgstr ""
msgid "del_next_events" msgid "del_next_events"
msgstr "" msgstr ""
#. Default: "Timeslot"
msgid "timeslot"
msgstr ""
#. Default: "All day"
msgid "timeslot_main"
msgstr ""
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "" msgstr ""

View file

@ -715,6 +715,14 @@ msgstr "Het event uitbreiden naar de volgende dagen (leeg laten om een event aan
msgid "del_next_events" msgid "del_next_events"
msgstr "Verwijder ook alle opeenvolgende events van hetzelfde type" msgstr "Verwijder ook alle opeenvolgende events van hetzelfde type"
#. Default: "Timeslot"
msgid "timeslot"
msgstr ""
#. Default: "All day"
msgid "timeslot_main"
msgstr ""
#. Default: "Inserted by ${userName}" #. Default: "Inserted by ${userName}"
msgid "history_insert" msgid "history_insert"
msgstr "Ingevuld door ${userName}" msgstr "Ingevuld door ${userName}"

View file

@ -14,26 +14,28 @@ function askCalendar(hookId, objectUrl, render, fieldName, month) {
askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params); askAjaxChunk(hookId, 'GET', objectUrl, fieldName+':pxView', params);
} }
function openEventPopup(action, fieldName, day, spansDays, function openEventPopup(action, fieldName, day, timeslot, spansDays,
applicableEventTypes, message) { 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 the
(from p_spansDays) if the event spans more days, in order to propose a p_timeslot where the event is assigned and if the event spans more days
checkbox allowing to delete events for those successive days. When action (from p_spansDays), in order to propose a checkbox allowing to delete
is "new", a possibly restricted list of applicable event types for this events for those successive days. When action is "new", a possibly
day is given in p_applicableEventTypes; p_message contains an optional restricted list of applicable event types for this day is given in
message explaining why not applicable types are not applicable. */ 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. f.timeslot.value = timeslot;
// 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;
cb[1].value = 'False'; cb[1].value = 'False';
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') { else if (action == 'new') {
// First: reinitialise input fields // First: reinitialise input fields
@ -42,8 +44,8 @@ function openEventPopup(action, fieldName, day, spansDays,
for (var i=0; i < allOptions.length; i++) { for (var i=0; i < allOptions.length; i++) {
allOptions[i].selected = false; allOptions[i].selected = false;
} }
f.eventSpan.style.background = ''; if (f.eventSpan) f.eventSpan.style.background = '';
// Among all event types, show applicable ones and hide the others. // Among all event types, show applicable ones and hide the others
var applicable = applicableEventTypes.split(','); var applicable = applicableEventTypes.split(',');
var applicableDict = {}; var applicableDict = {};
for (var i=0; i < applicable.length; i++) { for (var i=0; i < applicable.length; i++) {
@ -76,6 +78,7 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl,
f.eventType.style.background = wrongTextInput; f.eventType.style.background = wrongTextInput;
return; return;
} }
if (f.eventSpan) {
// 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) {
@ -86,6 +89,7 @@ function triggerCalendarEvent(action, hookId, fieldName, objectUrl,
} }
} }
} }
}
var elems = f.elements; var elems = f.elements;
var params = {}; var params = {};
// Put form elements into "params" // Put form elements into "params"