[gen] Calendar field: bugfixes; added params 'topPx' and 'bottomPx' allowing to define custom content before and after rendering the calendar view.
This commit is contained in:
parent
e52fee658f
commit
3e6027c499
|
@ -26,6 +26,9 @@ class Timeslot:
|
|||
# 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"
|
||||
# "day part" is the part of the day (from 0 to 1.0) that is taken by
|
||||
# the timeslot.
|
||||
self.dayPart = 1.0
|
||||
|
||||
def allows(self, eventType):
|
||||
'''It is allowed to have an event of p_eventType in this timeslot?'''
|
||||
|
@ -162,6 +165,10 @@ class Event(Persistent):
|
|||
return (self.eventType == other.eventType) and \
|
||||
(self.timeslot == other.timeslot)
|
||||
|
||||
def getDayPart(self, field):
|
||||
'''What is the day part taken by this event ?'''
|
||||
return field.getTimeslot(self.timeslot).dayPart
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Calendar(Field):
|
||||
'''This field allows to produce an agenda (monthly view) and view/edit
|
||||
|
@ -506,17 +513,24 @@ class Calendar(Field):
|
|||
onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(nextMonth))"/>
|
||||
<span>:_('month_%s' % monthDayOne.aMonth())</span>
|
||||
<span>:month.split('/')[0]</span>
|
||||
<!-- Validate button -->
|
||||
<input if="mayValidate" type="button" value=":_('validate_events')"
|
||||
class="buttonSmall button" style=":url('validate', bg=True)"
|
||||
var2="js='validateEvents(%s,%s)' % (q(ajaxHookId), q(month))"
|
||||
onclick=":'askConfirm(%s,%s,%s)' % (q('script'), q(js, False), \
|
||||
q(_('validate_events_confirm')))"/>
|
||||
<input type="checkbox" checked="checked" id=":'%s_auto' % ajaxHookId"
|
||||
class="smallbox"/>
|
||||
<label lfor="selectAuto" class="simpleLabel">:_('select_auto')</label>
|
||||
<!-- Validate button, with checkbox for automatic checbox selection -->
|
||||
<x if="mayValidate">
|
||||
<input if="mayValidate" type="button" value=":_('validate_events')"
|
||||
class="buttonSmall button" style=":url('validate', bg=True)"
|
||||
var2="js='validateEvents(%s,%s)' % (q(ajaxHookId), q(month))"
|
||||
onclick=":'askConfirm(%s,%s,%s)' % (q('script'), q(js, False), \
|
||||
q(_('validate_events_confirm')))"/>
|
||||
<input type="checkbox" checked="checked" id=":'%s_auto' % ajaxHookId"
|
||||
class="smallbox"/>
|
||||
<label lfor="selectAuto" class="simpleLabel">:_('select_auto')</label>
|
||||
</x>
|
||||
</div>
|
||||
<!-- The top PX, if defined -->
|
||||
<x if="field.topPx">::field.topPx</x>
|
||||
<!-- The calendar in itself -->
|
||||
<x>:getattr(field, 'pxView%s' % render.capitalize())</x>
|
||||
<!-- The bottom PX, if defined -->
|
||||
<x if="field.bottomPx">::field.bottomPx</x>
|
||||
</div>''')
|
||||
|
||||
pxEdit = pxSearch = ''
|
||||
|
@ -531,7 +545,24 @@ class Calendar(Field):
|
|||
startDate=None, endDate=None, defaultDate=None, timeslots=None,
|
||||
colors=None, showUncolored=False, preCompute=None,
|
||||
applicableEvents=None, totalRows=None, validation=None,
|
||||
view=None, xml=None, delete=True):
|
||||
topPx=None, bottomPx=None, view=None, xml=None, delete=True):
|
||||
# The "validator" attribute, allowing field-specific validation, behaves
|
||||
# differently for the Calendar field. If specified, it must hold a
|
||||
# method that will be executed every time a user wants to create an
|
||||
# event (or series of events) in the calendar. This method must accept
|
||||
# those args:
|
||||
# - date the date of the event (as a DateTime instance);
|
||||
# - eventType the event type (one among p_eventTypes);
|
||||
# - timeslot the timeslot for the event (see param "timeslots"
|
||||
# below);
|
||||
# - span the number of additional days on wich the event will
|
||||
# span (will be 0 if the user wants to create an event
|
||||
# for a single day).
|
||||
# If validation succeeds (ie, the event creation can take place), the
|
||||
# method must return True (boolean). Else, it will be canceled and an
|
||||
# error message will be shown. If the method returns False (boolean), it
|
||||
# will be a standard error message. If the method returns a string, it
|
||||
# will be used as specific error message.
|
||||
Field.__init__(self, validator, (0,1), default, show, page, group,
|
||||
layouts, move, False, True, False, specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
|
@ -649,13 +680,22 @@ class Calendar(Field):
|
|||
# May the user delete events in this calendar? If "delete" is a method,
|
||||
# it must accept an event type as single arg.
|
||||
self.delete = delete
|
||||
# You may specify PXs that will show specific information, respectively,
|
||||
# before and after the calendar.
|
||||
self.topPx = topPx
|
||||
self.bottomPx = bottomPx
|
||||
|
||||
def checkTimeslots(self):
|
||||
'''Checks whether self.timeslots defines corect timeslots.'''
|
||||
'''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.')
|
||||
# Set the day parts for every timeslot
|
||||
count = len(self.timeslots) - 1 # Count the timeslots, main excepted
|
||||
for timeslot in self.timeslots:
|
||||
if timeslot.id == 'main': continue
|
||||
timeslot.dayPart = 1.0 / count
|
||||
|
||||
def log(self, obj, msg, date=None):
|
||||
'''Logs m_msg, field-specifically prefixed.'''
|
||||
|
@ -915,6 +955,11 @@ class Calendar(Field):
|
|||
if not forBrowser: return res
|
||||
return ','.join(res)
|
||||
|
||||
def getTimeslot(self, id):
|
||||
'''Get the timeslot corresponding to p_id'''
|
||||
for slot in self.timeslots:
|
||||
if slot.id == id: return slot
|
||||
|
||||
def getEventsAt(self, obj, date):
|
||||
'''Returns the list of events that exist at some p_date (=day). p_date
|
||||
can be:
|
||||
|
@ -947,38 +992,95 @@ class Calendar(Field):
|
|||
if not events: return
|
||||
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.'''
|
||||
def standardizeDateRange(self, range):
|
||||
'''p_range can have various formats (see m_walkEvents below). This
|
||||
method standardizes the date range as a 6-tuple
|
||||
(startYear, startMonth, startDay, endYear, endMonth, endDay).'''
|
||||
if not range: return
|
||||
if isinstance(range, int):
|
||||
# p_range represents a year
|
||||
return (range, 1, 1, range, 12, 31)
|
||||
elif isinstance(range[0], int):
|
||||
# p_range represents a month
|
||||
year, month = range
|
||||
return (year, month, 1, year, month, 31)
|
||||
else:
|
||||
# p_range is a tuple (start, end) of DateTime instances
|
||||
start, end = range
|
||||
return (start.year(), start.month(), start.day(),
|
||||
end.year(), end.month(), end.day())
|
||||
|
||||
def walkEvents(self, obj, callback, dateRange=None):
|
||||
'''Walks on p_obj, the calendar value in chronological order 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.
|
||||
|
||||
If p_dateRange is specified, it limits the walk to this range. It
|
||||
can be:
|
||||
* an integer, representing a year;
|
||||
* a tuple of integers (year, month) representing a given month
|
||||
(first month is numbered 1);
|
||||
* a tuple (start, end) of DateTime instances.
|
||||
'''
|
||||
obj = obj.o
|
||||
if not hasattr(obj, self.name): return
|
||||
yearsDict = getattr(obj, self.name)
|
||||
if not yearsDict: return
|
||||
# Standardize date range
|
||||
if dateRange:
|
||||
startYear, startMonth, startDay, endYear, endMonth, endDay = \
|
||||
self.standardizeDateRange(dateRange)
|
||||
# Browse years
|
||||
years = getattr(obj, self.name)
|
||||
if not years: return
|
||||
for year in years.keys():
|
||||
years = list(yearsDict.keys())
|
||||
years.sort()
|
||||
for year in years:
|
||||
# Ignore this year if out of range
|
||||
if dateRange:
|
||||
if (year < startYear) or (year > endYear): continue
|
||||
isStartYear = year == startYear
|
||||
isEndYear = year == endYear
|
||||
# Browse this year's months
|
||||
months = years[year]
|
||||
for month in months.keys():
|
||||
monthsDict = yearsDict[year]
|
||||
if not monthsDict: continue
|
||||
months = list(monthsDict.keys())
|
||||
months.sort()
|
||||
for month in months:
|
||||
# Ignore this month if out of range
|
||||
if dateRange:
|
||||
if (isStartYear and (month < startMonth)) or \
|
||||
(isEndYear and (month > endMonth)): continue
|
||||
isStartMonth = isStartYear and (month == startMonth)
|
||||
isEndMonth = isEndYear and (month == endMonth)
|
||||
# Browse this month's days
|
||||
days = months[month]
|
||||
for day in days.keys():
|
||||
daysDict = monthsDict[month]
|
||||
if not daysDict: continue
|
||||
days = list(daysDict.keys())
|
||||
days.sort()
|
||||
for day in days:
|
||||
# Ignore this day if out of range
|
||||
if dateRange:
|
||||
if (isStartMonth and (day < startDay)) or \
|
||||
(isEndMonth and (day > endDay)): continue
|
||||
date = DateTime('%d/%d/%d UTC' % (year, month, day))
|
||||
stop = callback(obj, date, days[day])
|
||||
stop = callback(obj, date, daysDict[day])
|
||||
if stop: return
|
||||
|
||||
def getEventsByType(self, obj, eventType, minDate=None, maxDate=None,
|
||||
sorted=True, groupSpanned=False):
|
||||
'''Returns all the events of a given p_eventType. If p_eventType is
|
||||
None, it returns events of all types. The return value is a list of
|
||||
2-tuples whose 1st elem is a DateTime instance and whose 2nd elem is
|
||||
the event.
|
||||
None, it returns events of all types. p_eventType can also be a
|
||||
list or tuple. The return value is a list of 2-tuples whose 1st elem
|
||||
is a DateTime instance and whose 2nd elem is the event.
|
||||
|
||||
If p_sorted is True, the list is sorted in chronological order. Else,
|
||||
the order is random, but the result is computed faster.
|
||||
|
||||
If p_minDate and/or p_maxDate is/are specified, it restricts the
|
||||
search interval accordingly.
|
||||
|
||||
If p_groupSpanned is True, events spanned on several days are
|
||||
grouped into a single event. In this case, tuples in the result
|
||||
are 3-tuples: (DateTime_startDate, DateTime_endDate, event).
|
||||
|
@ -987,7 +1089,7 @@ class Calendar(Field):
|
|||
if groupSpanned and not sorted:
|
||||
raise Exception('Events must be sorted if you want to get ' \
|
||||
'spanned events to be grouped.')
|
||||
obj = obj.o # Ensure p_obj is not a wrapper.
|
||||
obj = obj.o # Ensure p_obj is not a wrapper
|
||||
res = []
|
||||
if not hasattr(obj, self.name): return res
|
||||
# Compute "min" and "max" tuples
|
||||
|
@ -1023,8 +1125,12 @@ class Calendar(Field):
|
|||
# Browse this day's events
|
||||
for event in events:
|
||||
# Filter unwanted events
|
||||
if eventType and (event.eventType != eventType):
|
||||
continue
|
||||
if eventType:
|
||||
if isinstance(eventType, str):
|
||||
keepIt = (event.eventType == eventType)
|
||||
else:
|
||||
keepIt = (event.eventType in eventType)
|
||||
if not keepIt: continue
|
||||
# We have found a event
|
||||
date = DateTime('%d/%d/%d UTC' % (year, month, day))
|
||||
if groupSpanned:
|
||||
|
|
|
@ -31,7 +31,7 @@ class Dict(List):
|
|||
# PX for rendering a single row
|
||||
pxRow = Px('''
|
||||
<tr valign="top" class=":loop.row.odd and 'even' or 'odd'">
|
||||
<td>:row[1]</td>
|
||||
<td class="discreet">:row[1]</td>
|
||||
<td for="info in subFields" if="info[1]" align="center"
|
||||
var2="field=info[1];
|
||||
fieldName='%s*%d' % (field.name, rowIndex);
|
||||
|
@ -41,15 +41,15 @@ class Dict(List):
|
|||
# PX for rendering the dict (shared between pxView and pxEdit)
|
||||
pxTable = Px('''
|
||||
<table var="isEdit=layoutType == 'edit'" if="isEdit or value"
|
||||
id=":'list_%s' % name" class=":isEdit and 'grid' or 'compact list'"
|
||||
id=":'list_%s' % name" class=":isEdit and 'grid' or 'list'"
|
||||
width=":field.width"
|
||||
var2="keys=field.keys(obj);
|
||||
subFields=field.getSubFields(zobj, layoutType)">
|
||||
<!-- Header -->
|
||||
<tr valign="bottom">
|
||||
<th></th>
|
||||
<th width=":field.widths[0]"></th>
|
||||
<th for="info in subFields" if="info[1]"
|
||||
width=":field.widths[loop.info.nb]">::_(info[1].labelId)</th>
|
||||
width=":field.widths[loop.info.nb+1]">::_(info[1].labelId)</th>
|
||||
</tr>
|
||||
<!-- Rows of data -->
|
||||
<x for="row in keys" var2="rowIndex=loop.row.nb">:field.pxRow</x>
|
||||
|
@ -79,7 +79,7 @@ class Dict(List):
|
|||
'''Formats the dict value as a list of values'''
|
||||
res = []
|
||||
for key, title in self.keys(obj.appy()):
|
||||
if key in value:
|
||||
if value and (key in value):
|
||||
res.append(value[key])
|
||||
else:
|
||||
# There is no value for this key in the database p_value
|
||||
|
|
|
@ -267,8 +267,9 @@ class ToolMixin(BaseMixin):
|
|||
def showPortlet(self, obj, layoutType):
|
||||
'''When must the portlet be shown? p_obj and p_layoutType can be None
|
||||
if we are not browing any objet (ie, we are on the home page).'''
|
||||
# Not on 'edit' pages.
|
||||
if layoutType == 'edit': return
|
||||
# Not on 'edit' pages or if there is no root class
|
||||
classes = self.getProductConfig(True).rootClasses
|
||||
if not classes or (layoutType == 'edit'): return
|
||||
res = True
|
||||
if obj and hasattr(obj, 'showPortlet'):
|
||||
res = obj.showPortlet()
|
||||
|
|
Loading…
Reference in a new issue