[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:
Gaetan Delannay 2015-03-18 19:33:53 +01:00
parent e52fee658f
commit 3e6027c499
3 changed files with 145 additions and 38 deletions

View file

@ -26,6 +26,9 @@ class Timeslot:
# The event types (among all event types defined at the Calendar level) # The event types (among all event types defined at the Calendar level)
# that can be assigned to this slot. # that can be assigned to this slot.
self.eventTypes = eventTypes # "None" means "all" 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): def allows(self, eventType):
'''It is allowed to have an event of p_eventType in this timeslot?''' '''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 \ return (self.eventType == other.eventType) and \
(self.timeslot == other.timeslot) (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): 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
@ -506,17 +513,24 @@ class Calendar(Field):
onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(nextMonth))"/> onclick=":'askMonth(%s,%s)' % (q(ajaxHookId), q(nextMonth))"/>
<span>:_('month_%s' % monthDayOne.aMonth())</span> <span>:_('month_%s' % monthDayOne.aMonth())</span>
<span>:month.split('/')[0]</span> <span>:month.split('/')[0]</span>
<!-- Validate button --> <!-- Validate button, with checkbox for automatic checbox selection -->
<input if="mayValidate" type="button" value=":_('validate_events')" <x if="mayValidate">
class="buttonSmall button" style=":url('validate', bg=True)" <input if="mayValidate" type="button" value=":_('validate_events')"
var2="js='validateEvents(%s,%s)' % (q(ajaxHookId), q(month))" class="buttonSmall button" style=":url('validate', bg=True)"
onclick=":'askConfirm(%s,%s,%s)' % (q('script'), q(js, False), \ var2="js='validateEvents(%s,%s)' % (q(ajaxHookId), q(month))"
q(_('validate_events_confirm')))"/> onclick=":'askConfirm(%s,%s,%s)' % (q('script'), q(js, False), \
<input type="checkbox" checked="checked" id=":'%s_auto' % ajaxHookId" q(_('validate_events_confirm')))"/>
class="smallbox"/> <input type="checkbox" checked="checked" id=":'%s_auto' % ajaxHookId"
<label lfor="selectAuto" class="simpleLabel">:_('select_auto')</label> class="smallbox"/>
<label lfor="selectAuto" class="simpleLabel">:_('select_auto')</label>
</x>
</div> </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> <x>:getattr(field, 'pxView%s' % render.capitalize())</x>
<!-- The bottom PX, if defined -->
<x if="field.bottomPx">::field.bottomPx</x>
</div>''') </div>''')
pxEdit = pxSearch = '' pxEdit = pxSearch = ''
@ -531,7 +545,24 @@ class Calendar(Field):
startDate=None, endDate=None, defaultDate=None, timeslots=None, startDate=None, endDate=None, defaultDate=None, timeslots=None,
colors=None, showUncolored=False, preCompute=None, colors=None, showUncolored=False, preCompute=None,
applicableEvents=None, totalRows=None, validation=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, 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,
@ -649,13 +680,22 @@ class Calendar(Field):
# May the user delete events in this calendar? If "delete" is a method, # May the user delete events in this calendar? If "delete" is a method,
# it must accept an event type as single arg. # it must accept an event type as single arg.
self.delete = delete 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): 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' # The first timeslot must be the global one, named 'main'
if self.timeslots[0].id != 'main': if self.timeslots[0].id != 'main':
raise Exception('The first timeslot must have id "main" and is ' \ raise Exception('The first timeslot must have id "main" and is ' \
'the one representing the whole day.') '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): def log(self, obj, msg, date=None):
'''Logs m_msg, field-specifically prefixed.''' '''Logs m_msg, field-specifically prefixed.'''
@ -915,6 +955,11 @@ class Calendar(Field):
if not forBrowser: return res if not forBrowser: return res
return ','.join(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): def getEventsAt(self, obj, date):
'''Returns the list of events that exist at some p_date (=day). p_date '''Returns the list of events that exist at some p_date (=day). p_date
can be: can be:
@ -947,38 +992,95 @@ class Calendar(Field):
if not events: return if not events: return
return events[0].eventType return events[0].eventType
def walkEvents(self, obj, callback): def standardizeDateRange(self, range):
'''Walks on p_obj, the calendar value for this field and calls '''p_range can have various formats (see m_walkEvents below). This
p_callback for every day containing events. The callback must accept method standardizes the date range as a 6-tuple
3 args: p_obj, the current day (as a DateTime instance) and the list (startYear, startMonth, startDay, endYear, endMonth, endDay).'''
of events at that day (the database-stored PersistentList if not range: return
instance). If the callback returns True we stop the walk.''' 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 obj = obj.o
if not hasattr(obj, self.name): return 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 # Browse years
years = getattr(obj, self.name) years = list(yearsDict.keys())
if not years: return years.sort()
for year in years.keys(): 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 # Browse this year's months
months = years[year] monthsDict = yearsDict[year]
for month in months.keys(): 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 # Browse this month's days
days = months[month] daysDict = monthsDict[month]
for day in days.keys(): 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)) date = DateTime('%d/%d/%d UTC' % (year, month, day))
stop = callback(obj, date, days[day]) stop = callback(obj, date, daysDict[day])
if stop: return 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
None, it returns events of all types. The return value is a list of None, it returns events of all types. p_eventType can also be a
2-tuples whose 1st elem is a DateTime instance and whose 2nd elem is list or tuple. The return value is a list of 2-tuples whose 1st elem
the event. is a DateTime instance and whose 2nd elem is the event.
If p_sorted is True, the list is sorted in chronological order. Else, If p_sorted is True, the list is sorted in chronological order. Else,
the order is random, but the result is computed faster. the order is random, but the result is computed faster.
If p_minDate and/or p_maxDate is/are specified, it restricts the If p_minDate and/or p_maxDate is/are specified, it restricts the
search interval accordingly. search interval accordingly.
If p_groupSpanned is True, events spanned on several days are If p_groupSpanned is True, events spanned on several days are
grouped into a single event. In this case, tuples in the result grouped into a single event. In this case, tuples in the result
are 3-tuples: (DateTime_startDate, DateTime_endDate, event). are 3-tuples: (DateTime_startDate, DateTime_endDate, event).
@ -987,7 +1089,7 @@ class Calendar(Field):
if groupSpanned and not sorted: if groupSpanned and not sorted:
raise Exception('Events must be sorted if you want to get ' \ raise Exception('Events must be sorted if you want to get ' \
'spanned events to be grouped.') '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 = [] res = []
if not hasattr(obj, self.name): return res if not hasattr(obj, self.name): return res
# Compute "min" and "max" tuples # Compute "min" and "max" tuples
@ -1023,8 +1125,12 @@ class Calendar(Field):
# Browse this day's events # Browse this day's events
for event in events: for event in events:
# Filter unwanted events # Filter unwanted events
if eventType and (event.eventType != eventType): if eventType:
continue if isinstance(eventType, str):
keepIt = (event.eventType == eventType)
else:
keepIt = (event.eventType in eventType)
if not keepIt: 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:

View file

@ -31,7 +31,7 @@ class Dict(List):
# PX for rendering a single row # PX for rendering a single row
pxRow = Px(''' pxRow = Px('''
<tr valign="top" class=":loop.row.odd and 'even' or 'odd'"> <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" <td for="info in subFields" if="info[1]" align="center"
var2="field=info[1]; var2="field=info[1];
fieldName='%s*%d' % (field.name, rowIndex); fieldName='%s*%d' % (field.name, rowIndex);
@ -41,15 +41,15 @@ class Dict(List):
# PX for rendering the dict (shared between pxView and pxEdit) # PX for rendering the dict (shared between pxView and pxEdit)
pxTable = Px(''' pxTable = Px('''
<table var="isEdit=layoutType == 'edit'" if="isEdit or value" <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" width=":field.width"
var2="keys=field.keys(obj); var2="keys=field.keys(obj);
subFields=field.getSubFields(zobj, layoutType)"> subFields=field.getSubFields(zobj, layoutType)">
<!-- Header --> <!-- Header -->
<tr valign="bottom"> <tr valign="bottom">
<th></th> <th width=":field.widths[0]"></th>
<th for="info in subFields" if="info[1]" <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> </tr>
<!-- Rows of data --> <!-- Rows of data -->
<x for="row in keys" var2="rowIndex=loop.row.nb">:field.pxRow</x> <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''' '''Formats the dict value as a list of values'''
res = [] res = []
for key, title in self.keys(obj.appy()): for key, title in self.keys(obj.appy()):
if key in value: if value and (key in value):
res.append(value[key]) res.append(value[key])
else: else:
# There is no value for this key in the database p_value # There is no value for this key in the database p_value

View file

@ -267,8 +267,9 @@ class ToolMixin(BaseMixin):
def showPortlet(self, obj, layoutType): def showPortlet(self, obj, layoutType):
'''When must the portlet be shown? p_obj and p_layoutType can be None '''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).''' if we are not browing any objet (ie, we are on the home page).'''
# Not on 'edit' pages. # Not on 'edit' pages or if there is no root class
if layoutType == 'edit': return classes = self.getProductConfig(True).rootClasses
if not classes or (layoutType == 'edit'): return
res = True res = True
if obj and hasattr(obj, 'showPortlet'): if obj and hasattr(obj, 'showPortlet'):
res = obj.showPortlet() res = obj.showPortlet()