[gen] Calendar field: allow to have several events at the same day via the concept of timeslots (ongoing work).
This commit is contained in:
parent
0c706c695e
commit
da8f7a5bcd
|
@ -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) > 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
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue