[gen] Calendar field: first version of the timeline rendering.

This commit is contained in:
Gaetan Delannay 2015-02-24 15:18:08 +01:00
parent 429eaf8abc
commit 1be8163c70
3 changed files with 142 additions and 33 deletions

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import types import types
from appy import Object from appy import Object
@ -19,18 +20,19 @@ class Calendar(Field):
pxTimeLineMonths = Px(''' pxTimeLineMonths = Px('''
<tr><th></th> <!-- Names of months --> <tr><th></th> <!-- Names of months -->
<th for="mInfo in monthsInfos" <th for="mInfo in monthsInfos"
colspan=":mInfo.colspan">::mInfo.month</th></tr>''') colspan=":mInfo.colspan">::mInfo.month</th><th></th></tr>''')
# For timeline rendering, the row displaying day letters # For timeline rendering, the row displaying day letters
pxTimelineDayLetters = Px(''' pxTimelineDayLetters = Px('''
<tr><td></td> <!-- Days (letters) --> <tr><td></td> <!-- Days (letters) -->
<td for="date in grid"><b>:namesOfDays[date.aDay()].name[0]</b></td> <td for="date in grid"><b>:namesOfDays[date.aDay()].name[0]</b></td>
</tr>''') <td></td></tr>''')
# For timeline rendering, the row displaying day numbers # For timeline rendering, the row displaying day numbers
pxTimelineDayNumbers = Px(''' pxTimelineDayNumbers = Px('''
<tr><td></td> <!-- Days (numbers) --> <tr><td></td> <!-- Days (numbers) -->
<td for="date in grid"><b>:str(date.day()).zfill(2)</b></td></tr>''') <td for="date in grid"><b>:str(date.day()).zfill(2)</b></td>
<td></td></tr>''')
# Timeline view for a calendar # Timeline view for a calendar
pxViewTimeline = Px(''' pxViewTimeline = Px('''
@ -39,22 +41,35 @@ class Calendar(Field):
<!-- Column specifiers --> <!-- Column specifiers -->
<colgroup> <colgroup>
<!-- Names of calendars --> <!-- Names of calendars -->
<col style="width: 140px"></col> <col></col>
<col for="date in grid" <col for="date in grid"
style=":field.getCellStyle(zobj, date, render, today)"></col> style=":field.getColumnStyle(zobj, date, render, today)"></col>
<col></col>
</colgroup> </colgroup>
<!-- Header rows (months and days) --> <!-- Header rows (months and days) -->
<x>:field.pxTimeLineMonths</x> <x>:field.pxTimeLineMonths</x>
<x>:field.pxTimelineDayLetters</x><x>:field.pxTimelineDayNumbers</x> <x>:field.pxTimelineDayLetters</x><x>:field.pxTimelineDayNumbers</x>
<!-- The calendar in itself --> <!-- The calendar in itself -->
<tr if="allEventTypes"> <tr if="allEventTypes">
<td class="tlName">Name</td> <td class="tlName"></td>
<td for="date in grid"></td> <td for="date in grid"></td>
<td></td>
</tr> </tr>
<!-- Other calendars --> <!-- Other calendars -->
<tr for="other in otherCalendars"> <tr for="other in otherCalendars"
<td class="tlName">:field.getTimelineName(*other)</td> var2="tlName=field.getTimelineName(*other)">
<td for="date in grid"></td> <td class="tlLeft">::tlName</td>
<!-- A cell in this other calendar -->
<x for="date in grid"
var2="inRange=field.dateInRange(date, startDate, endDate)">
<td if="not inRange"></td>
<td if="inRange"
var2="events=field.getOtherEventsAt(zobj, date, other, eventNames,\
render, colors, single=True)"
style=":field.getCellStyle(zobj, date, render, \
events)">::field.getTimelineCell(events)</td>
</x>
<td class="tlRight">::tlName</td>
</tr> </tr>
<!-- 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>
@ -74,9 +89,7 @@ class Calendar(Field):
<!-- The calendar in itself --> <!-- The calendar in itself -->
<tr for="row in grid" valign="top" height=":rowHeight"> <tr for="row in grid" valign="top" height=":rowHeight">
<x for="date in row" <x for="date in row"
var2="tooEarly=startDate and (date &lt; startDate); var2="inRange=field.dateInRange(date, startDate, endDate);
tooLate=endDate and not tooEarly and (date &gt; endDate);
inRange=not tooEarly and not tooLate;
cssClasses=field.getCellClass(zobj, date, render, today)"> cssClasses=field.getCellClass(zobj, date, render, today)">
<!-- Dump an empty cell if we are out of the supported date range --> <!-- Dump an empty cell if we are out of the supported date range -->
<td if="not inRange" class=":cssClasses"></td> <td if="not inRange" class=":cssClasses"></td>
@ -112,12 +125,12 @@ class Calendar(Field):
(q('del'), q(field.name), q(dayString), q(spansDays))"/> (q('del'), q(field.name), q(dayString), q(spansDays))"/>
<!-- A single event is allowed for the moment --> <!-- A single event is allowed for the moment -->
<div if="events" var2="eventType=events[0].eventType"> <div if="events" var2="eventType=events[0].eventType">
<span style="color: grey">:field.getEventName(zobj, eventType)</span> <span style="color: grey">:eventNames[eventType]</span>
</div> </div>
<!-- Events from other calendars --> <!-- Events from other calendars -->
<x if="otherCalendars" <x if="otherCalendars"
var2="otherEvents=field.getOtherEventsAt(zobj, date, \ var2="otherEvents=field.getOtherEventsAt(zobj, date, \
otherCalendars)"> otherCalendars, eventNames, render, colors)">
<div style=":'color: %s; font-style: italic' % event.color" <div style=":'color: %s; font-style: italic' % event.color"
for="event in otherEvents">:event.name</div> for="event in otherEvents">:event.name</div>
</x> </x>
@ -147,7 +160,7 @@ class Calendar(Field):
<select name="eventType"> <select name="eventType">
<option value="">:_('choose_a_value')</option> <option value="">:_('choose_a_value')</option>
<option for="eventType in allEventTypes" <option for="eventType in allEventTypes"
value=":eventType">:field.getEventName(zobj,eventType)</option> value=":eventType">:eventNames[eventType]</option>
</select><br/><br/> </select><br/><br/>
<!--Span the event on several days --> <!--Span the event on several days -->
<div align="center" class="discreet" style="margin-bottom: 3px"> <div align="center" class="discreet" style="margin-bottom: 3px">
@ -215,6 +228,9 @@ class Calendar(Field):
startDate=field.getStartDate(zobj); startDate=field.getStartDate(zobj);
endDate=field.getEndDate(zobj); endDate=field.getEndDate(zobj);
otherCalendars=field.getOtherCalendars(zobj, preComputed); otherCalendars=field.getOtherCalendars(zobj, preComputed);
eventNames=field.getEventNames(zobj, allEventTypes, \
otherCalendars);
colors=field.getColors(zobj);
namesOfDays=field.getNamesOfDays(_)" namesOfDays=field.getNamesOfDays(_)"
id=":ajaxHookId"> id=":ajaxHookId">
<script>:'var %s_maxEventLength = %d;' % \ <script>:'var %s_maxEventLength = %d;' % \
@ -259,9 +275,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',
otherCalendars=None, timelineName=None, additionalInfo=None, otherCalendars=None, timelineName=None, additionalInfo=None,
startDate=None, endDate=None, defaultDate=None, startDate=None, endDate=None, defaultDate=None, colors=None,
preCompute=None, applicableEvents=None, view=None, xml=None, showUncolored=False, preCompute=None, applicableEvents=None,
delete=True): 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,
@ -340,6 +356,15 @@ 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
# "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
# from "otherCalendars". In a timeline, cells are too small to display
# translated names for event types, so colors are used instead.
self.colors = colors or {}
# For event types that are not present in self.colors hereabove, must we
# still show them? If yes, they will be represented by a dot with a
# tooltip containing the event name.
self.showUncolored = showUncolored
# For a specific day, all event types may not be applicable. If this is # For a specific day, all event types may not be applicable. If this is
# the case, one may specify here a method that defines, for a given day, # the case, one may specify here a method that defines, for a given day,
# a sub-set of all event types. This method must accept 3 args: the day # a sub-set of all event types. This method must accept 3 args: the day
@ -441,9 +466,16 @@ class Calendar(Field):
def getTimelineName(self, obj, name, color): def getTimelineName(self, obj, name, color):
'''Returns the name of this calendar as must be shown in a timeline.''' '''Returns the name of this calendar as must be shown in a timeline.'''
if not self.timelineName: return obj.title if not self.timelineName:
return '<a href="%s">%s</a>' % (obj.url, obj.title)
return self.timelineName(self, obj, name, color) return self.timelineName(self, obj, name, color)
def getTimelineCell(self, events):
'''Gets the content of a cell in a timeline calendar.'''
# Currently a single event is allowed
if not events or not events[0].symbol: return ''
return events[0].symbol
def getAdditionalInfoAt(self, obj, date, preComputed): def getAdditionalInfoAt(self, obj, date, preComputed):
'''If the user has specified a method in self.additionalInfo, we call '''If the user has specified a method in self.additionalInfo, we call
it for displaying this additional info in the calendar, at some it for displaying this additional info in the calendar, at some
@ -457,6 +489,19 @@ class Calendar(Field):
if callable(self.eventTypes): return self.eventTypes(obj.appy()) if callable(self.eventTypes): return self.eventTypes(obj.appy())
return self.eventTypes return self.eventTypes
def getColors(self, obj):
'''Gets the colors for event types managed by this calendar and
otherCalendars (from self.colors).'''
if callable(self.colors): return self.colors(obj)
return self.colors
def dateInRange(self, date, startDate, endDate):
'''Is p_date within the range (possibly) defined for this calendar by
p_startDate and p_endDate ?'''
tooEarly = startDate and (date < startDate)
tooLate = endDate and not tooEarly and (date > endDate)
return not tooEarly and not tooLate
def getApplicableEventsTypesAt(self, obj, date, allEventTypes, preComputed, def getApplicableEventsTypesAt(self, obj, date, allEventTypes, preComputed,
forBrowser=False): forBrowser=False):
'''Returns the event types that are applicable at a given p_date. More '''Returns the event types that are applicable at a given p_date. More
@ -593,15 +638,36 @@ class Calendar(Field):
if not events: return False if not events: return False
return events[0].eventType == otherEvents[0].eventType return events[0].eventType == otherEvents[0].eventType
def getOtherEventsAt(self, obj, date, otherCalendars): def getOtherEventsAt(self, obj, date, otherCalendars, eventNames, render,
'''Gets events that are defined in p_otherCalendars at some p_date.''' colors, single=False):
'''Gets events that are defined in p_otherCalendars at some p_date.
If p_single is True, p_otherCalendars does not contain the list of
all other calendars, but information about a single calendar.'''
res = [] res = []
if single: otherCalendars = [otherCalendars]
isTimeline = render == 'timeline'
for o, field, color in otherCalendars: for o, field, color in otherCalendars:
events = field.getEventsAt(o.o, date) events = field.getEventsAt(o.o, date)
if events: if events:
eventType = events[0].eventType eventType = events[0].eventType
eventName = field.getEventName(o.o, eventType) info = Object(color=color)
info = Object(name=eventName, color=color) if isTimeline:
# Get the background color for this cell if it has been
# defined, or (a) nothing if showUncolored is False, (b) a
# tooltipped dot else.
if eventType in colors:
info.bgColor = colors[eventType]
info.symbol = None
else:
info.bgColor = None
if self.showUncolored:
info.symbol = '<acronym title="%s">▪</acronym>' % \
eventNames[eventType]
else:
info.symbol = None
else:
# Get the event name
info.name = eventNames[eventType]
res.append(info) res.append(info)
return res return res
@ -613,15 +679,31 @@ class Calendar(Field):
else: else:
return obj.translate('%s_event_%s' % (self.labelId, eventType)) return obj.translate('%s_event_%s' % (self.labelId, eventType))
def getEventNames(self, obj, eventTypes, otherCalendars):
'''Computes a dict of event names, keyed by event types, for all events
in this calendar and p_otherCalendars).'''
res = {}
if eventTypes:
for et in eventTypes:
res[et] = self.getEventName(obj, et)
if not otherCalendars: return res
for other, field, color in otherCalendars:
eventTypes = field.getEventTypes(other)
if eventTypes:
for et in eventTypes:
if et not in res:
res[et] = field.getEventName(other, et)
return res
def getStartDate(self, obj): def getStartDate(self, obj):
'''Get the start date for this calendar if defined.''' '''Get the start date for this calendar if defined'''
if self.startDate: if self.startDate:
d = self.startDate(obj.appy()) d = self.startDate(obj.appy())
# Return the start date without hour, in UTC. # Return the start 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 getEndDate(self, obj): def getEndDate(self, obj):
'''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.
@ -720,9 +802,9 @@ class Calendar(Field):
elif action == 'deleteEvent': elif action == 'deleteEvent':
return self.deleteEvent(obj, DateTime(rq['day'])) return self.deleteEvent(obj, DateTime(rq['day']))
def getCellStyle(self, obj, date, render, today): def getColumnStyle(self, obj, date, render, today):
'''What style(s) must apply to the table cell representing p_date '''What style(s) must apply to the table column representing p_date
in the calendar?''' in the calendar? For timelines only.'''
if render != 'timeline': return '' if render != 'timeline': return ''
# Cells representing specific days must have a specific background color # Cells representing specific days must have a specific background color
res = '' res = ''
@ -731,10 +813,18 @@ class Calendar(Field):
res = 'background-color: %s' % Calendar.timelineBgColors[day] res = 'background-color: %s' % Calendar.timelineBgColors[day]
return res return res
def getCellStyle(self, obj, date, render, events):
'''Gets the cell style to apply to the cell corresponding to p_date.'''
if render != 'timeline': return '' # Currently, for timelines only
if not events: return ''
# Currently, a single event is allowed
event = events[0]
return event.bgColor and ('background-color: %s' % event.bgColor) or ''
def getCellClass(self, obj, date, render, today): def getCellClass(self, obj, date, render, today):
'''What CSS class(es) must apply to the table cell representing p_date '''What CSS class(es) must apply to the table cell representing p_date
in the calendar?''' in the calendar?'''
if render != 'month': return '' if render != 'month': return '' # Currently, for month rendering only
res = [] res = []
# We must distinguish between past and future dates # We must distinguish between past and future dates
if date < today: if date < today:

View file

@ -209,7 +209,8 @@ class String(Field):
name=":name" id=":name" class=":masterCss" name=":name" id=":name" class=":masterCss"
multiple=":isMultiple and 'multiple' or ''" multiple=":isMultiple and 'multiple' or ''"
onchange=":field.getOnChange(zobj, layoutType)" onchange=":field.getOnChange(zobj, layoutType)"
size=":isMultiple and field.height or 1"> size=":field.getSelectSize(isMultiple)"
style=":field.getSelectStyle(isMultiple)">
<option for="val in possibleValues" value=":val[0]" <option for="val in possibleValues" value=":val[0]"
selected=":field.isSelected(zobj, name, val[0], rawValue)" selected=":field.isSelected(zobj, name, val[0], rawValue)"
title=":val[1]">:ztool.truncateValue(val[1],field.width)</option> title=":val[1]">:ztool.truncateValue(val[1],field.width)</option>
@ -479,6 +480,21 @@ class String(Field):
res = False res = False
return res return res
def getSelectSize(self, isMultiple):
'''When this field renders as a selection list, get the value of its
"size" attribute.'''
if not isMultiple: return 1
if isinstance(self.height, int): return self.height
# "height" can be defined as a string. In this case it is used to define
# height via a attribute "style", not "size".
return ''
def getSelectStyle(self, isMultiple):
'''When thiss field renders as a selection list, get the value of its
"style" attribute.'''
if not isMultiple or not isinstance(self.height, str): return ''
return 'height: %s' % self.height
def isMultilingual(self, obj, dontKnow=False): def isMultilingual(self, obj, dontKnow=False):
'''Is this field multilingual ? If we don't know, say p_dontKnow.''' '''Is this field multilingual ? If we don't know, say p_dontKnow.'''
# In the following case, impossible to know: we say no. # In the following case, impossible to know: we say no.

View file

@ -27,7 +27,8 @@ input[type=button] { border: 1px solid #d0d0d0; margin: 0 3px;
background-color: #f8f8f8; cursor: pointer } background-color: #f8f8f8; cursor: pointer }
input[type=submit] { border: 1px solid #d0d0d0; background-color: #f8f8f8; input[type=submit] { border: 1px solid #d0d0d0; background-color: #f8f8f8;
cursor: pointer } cursor: pointer }
input[type=password] { border: 1px solid #d0d0d0; input[type=password] { border: 1px solid #d0d0d0; padding: 0 2px;
margin-bottom:1px;
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif } font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif }
input[type=text] { border: 1px solid #d0d0d0; padding: 0 2px; margin-bottom:1px; input[type=text] { border: 1px solid #d0d0d0; padding: 0 2px; margin-bottom:1px;
font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif } font-family: "Lucida Grande","Lucida Sans Unicode",Helvetica,Arial,Verdana,sans-serif }
@ -128,7 +129,9 @@ td.search { padding-top: 8px }
.timeline { font-size: 90%; color: #555555 } .timeline { font-size: 90%; color: #555555 }
.timeline td { text-align: center; padding: 1px } .timeline td { text-align: center; padding: 1px }
.timeline th { padding: 1px } .timeline th { padding: 1px }
.tlName { text-align: left !important } .tlLeft { text-align: left !important }
.tlRight { text-align: right !important }
.tlRight { text-align: right !important }
.msgTable { margin: 6px 0; width: 100%; font-size: 93% } .msgTable { margin: 6px 0; width: 100%; font-size: 93% }
.msgTable tr { vertical-align: top } .msgTable tr { vertical-align: top }
.msgTable td, .msgTable th { border: 1px solid grey; color: #555555; .msgTable td, .msgTable th { border: 1px solid grey; color: #555555;