From 3e6027c499c304a13d0cb85d2c07517bec371e01 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 18 Mar 2015 19:33:53 +0100 Subject: [PATCH] [gen] Calendar field: bugfixes; added params 'topPx' and 'bottomPx' allowing to define custom content before and after rendering the calendar view. --- fields/calendar.py | 168 ++++++++++++++++++++++++++++++++-------- fields/dict.py | 10 +-- gen/mixins/ToolMixin.py | 5 +- 3 files changed, 145 insertions(+), 38 deletions(-) diff --git a/fields/calendar.py b/fields/calendar.py index 5761599..d5dd27c 100644 --- a/fields/calendar.py +++ b/fields/calendar.py @@ -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))"/> :_('month_%s' % monthDayOne.aMonth()) :month.split('/')[0] - - - - + + + + + + + + ::field.topPx + :getattr(field, 'pxView%s' % render.capitalize()) + + ::field.bottomPx ''') 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: diff --git a/fields/dict.py b/fields/dict.py index f010e70..ce554bd 100644 --- a/fields/dict.py +++ b/fields/dict.py @@ -31,7 +31,7 @@ class Dict(List): # PX for rendering a single row pxRow = Px(''' - :row[1] + :row[1] - + ::_(info[1].labelId) + width=":field.widths[loop.info.nb+1]">::_(info[1].labelId) :field.pxRow @@ -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 diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index b813c88..5b46e12 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -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()