Personal tools
You are here: Home Products Plone Roadmap #133: Calendar Portlet Refactoring
Document Actions

#133: Calendar Portlet Refactoring

Contents
  1. Motivation
  2. Assumptions
  3. Proposal
  4. Implementation
by Maik Roeder last modified June 11, 2006 - 00:22
We propose a refactoring that makes the Calendar Portlet twice as fast by replacing the Python logic in the Page Template loops with Python code. It doesn't come as a surprise, that this new implementation also makes the calendar more flexible, and changing the starting day of the week becomes trivial, while this was hard in the old implementation.
Proposed by
Gilles Lenfant
Seconded by
Maik Röder
Proposal type
Architecture
State
rejected

Motivation

The calendar portlet is not only slow, but putting a lot of logic in the template makes it hard to change its behaviour.

For example, it should be trivial to change the starting day of the week.



Assumptions


This proposal only treat the calendar refactoring. Setting the day of the week in a portlet or in the ZMI is not covered by this PLIP, although this is easy to implement.

Proposal


The following code needs to be refactored:

<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
i18n:domain="plone">

<body>

<div metal:define-macro="portlet"
tal:omit-tag=""
tal:define="DateTime python:modules['DateTime'].DateTime;
current python:DateTime();
current_day current/day;
yearmonth here/getYearAndMonthToDisplay;
nextYearMax python: current+365;
prevYearMin python: current-365;
year python:yearmonth[0];
month python:yearmonth[1];
prevMonthTime python:here.getPreviousMonth(month, year);
nextMonthTime python:here.getNextMonth(month, year);
weeks python:here.portal_calendar.getEventsForCalendar(month, year);
anchor_url request/anchor_url | here_url;
query_string python:request.get('orig_query', None);
query_string python: (query_string is None and request.get('QUERY_STRING', None)) or query_string;
url_quote python:modules['Products.PythonScripts.standard'].url_quote;
anchor_method request/anchor_method | template/getId;
translation_service nocall:here/translation_service;
day_msgid nocall:translation_service/day_msgid;
weekday_english nocall:translation_service/weekday_english;
utranslate nocall:here/utranslate;
toLocalizedTime nocall:here/toLocalizedTime;
getEventString nocall:here/getEventString;">
<!-- The calendar, rendered as a table -->

<table class="ploneCalendar" id="thePloneCalendar" summary="Calendar" i18n:attributes="summary summary_calendar;">
<thead>
<tr>
<th id="calendar-previous">
<a href="#" rel="nofollow"
title="Previous month"
tal:attributes="href python:'%s/%s?%s&amp;month:int=%d&amp;year:int=%d&amp;orig_query=%s' % (anchor_url, anchor_method, query_string, prevMonthTime.month(),prevMonthTime.year(),url_quote(query_string))"
tal:condition="python: yearmonth > (prevYearMin.year(), prevYearMin.month())"
i18n:attributes="title title_previous_month;">&laquo;</a>
</th>
<th colspan="5">
<span i18n:translate="" tal:omit-tag="">
<span i18n:name="monthname"
tal:define="month_english python:translation_service.month_english(month);"
tal:attributes="id string:calendar-month-$month_english"
tal:content="python: utranslate(translation_service.month_msgid(month), default=month_english)"
tal:omit-tag=""
id="calendar-month-month">monthname</span>
<span i18n:name="year"
tal:content="string:$year"
tal:attributes="id string:calendar-year-$year;"
tal:omit-tag=""
id="calendar-year">year</span>
</span>
</th>
<th id="calendar-next">
<a href="#" rel="nofollow"
title="Next month"
tal:attributes="href python:'%s/%s?%s&amp;month:int=%d&amp;year:int=%d&amp;orig_query=%s' % (anchor_url, anchor_method, query_string, nextMonthTime.month(),nextMonthTime.year(),url_quote(query_string))"
tal:condition="python: yearmonth &lt; (nextYearMax.year(), nextYearMax.month())"
i18n:attributes="title title_next_month;">&raquo;</a>
</th>
</tr>
<tr tal:define="weekdaynumbers here/portal_calendar/getDayNumbers" class="weekdays">
<tal:data tal:repeat="daynumber weekdaynumbers">
<td tal:define="weekday_english python:weekday_english(daynumber,format='a');"
tal:content="python: utranslate(day_msgid(daynumber, format='s'), default=weekday_english)">Su</td>
</tal:data>
</tr>
</thead>

<tal:comment replace="nothing"><!--
Sorry for the obtuse formatting below (the stray end-of-tag markers), but until
tal:block doesn't render an entire line of blank space when used, this is the only way.
--></tal:comment>
<tbody>
<tr tal:repeat="week weeks"
><tal:block repeat="day week"
><tal:block define="int_daynumber python: int(day['day']);
day_event day/event;
is_today python: current_day==int_daynumber and current.month()==month and current.year()==year">
<td class="event" tal:condition="day_event"
tal:attributes="class python:is_today and 'todayevent' or 'event'"
><tal:data tal:define="cur_date python:DateTime(year,month,int_daynumber);
begin python:url_quote((cur_date.latestTime()).ISO());
end python:url_quote(cur_date.earliestTime().ISO());"
tal:omit-tag=""
><a href=""
tal:attributes="href string:${portal_url}/search?review_state=published&amp;start.query:record:list:date=${begin}&amp;start.range:record=max&amp;end.query:record:list:date=${end}&amp;end.range:record=min;
title python:'\n'.join([toLocalizedTime(cur_date)]+[getEventString(e) for e in day['eventslist']]);"
tal:content="python:int_daynumber or default">
31
</a
></tal:data>
</td
><tal:notdayevent tal:condition="not: day_event"
><td tal:attributes="class python:is_today and 'todaynoevent' or None"
tal:content="python:int_daynumber or default"></td
></tal:notdayevent
></tal:block
></tal:block>
</tr>
</tbody>
</table>

</div>

</body>

</html>

Implementation


We propose a new "portlet_calendar" Page Template wich is a lot cleaner. The template can ask a tool for an object that contains all the information necessary for showing the calendar. No calculations need to be done in the Page Template.

This is the call in the Page Template that fetches the information. "portal_ploneplus" is just the tool we used for the initial implementation, and this should probably be changed to the calendar tool.

calendar_features python:portal_ploneplus.calendarPortletFeatures(portal_url, here_url, template, REQUEST=None)

This is the code for the calendar. The calls to the calendar_features object are highlighted:

<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
i18n:domain="plone">

<body>

<div metal:define-macro="portlet"
tal:omit-tag=""
tal:define="portal_ploneplus portal/portal_ploneplus;
calendar_features python:portal_ploneplus.calendarPortletFeatures(portal_url, here_url, template, REQUEST=None)">
<!-- The calendar, rendered as a table -->

<table class="ploneCalendar"
id="thePloneCalendar"
summary="Calendar"
i18n:attributes="summary summary_calendar;">
<thead>
<tr>
<th id="calendar-previous"
tal:define="data calendar_features/previousMonth">
<a href="#" rel="nofollow"
title="Previous month"
tal:attributes="href data/link"
tal:condition="data/condition"
i18n:attributes="title title_previous_month;">&laquo;</a>
</th>
<th colspan="5"
tal:define="data calendar_features/thisMonth">
<span i18n:translate="" tal:omit-tag="">
<span i18n:name="monthname"
tal:attributes="id data/month_tag_id"
tal:content="data/month_value"
tal:omit-tag=""
id="calendar-month-month">monthname</span>
<span i18n:name="year"
tal:content="data/year_value"
tal:attributes="id data/year_tag_id"
tal:omit-tag=""
id="calendar-year">year</span>
</span>
</th>
<th id="calendar-next"
tal:define="data calendar_features/nextMonth">
<a href="#" rel="nofollow"
title="Next month"
tal:attributes="href data/link"
tal:condition="data/condition"
i18n:attributes="title title_next_month;">&raquo;</a>
</th>
</tr>
<tr class="weekdays">
<tal:data tal:repeat="daynick calendar_features/weekDays">
<td tal:content="daynick">Su</td>
</tal:data>
</tr>
</thead>

<tal:comment replace="nothing">
<!-- Sorry for the obtuse formatting below (the stray
end-of-tag markers), but until tal:block doesn't render an
entire line of blank space when used, this is the only way.
-->
</tal:comment>
<tbody>
<tr tal:repeat="week calendar_features/weeksOfMonth"
><tal:block repeat="day week"
><td class="event"
tal:attributes="class day/css_class"
><a href="#"
tal:attributes="href day/link;
title day/title"
tal:omit-tag="not: day/day_event"
tal:content="python:day.daynumber or default">
<!-- The day number -->
</a>
</td>
</tal:block>
</tr>
</tbody>
</table>

</div>

</body>

</html>

This is the method in the tool that instantiates a "CalendarPortletFeatures" class with all the necessary information:

security.declarePublic('calendarPortletFeatures')
def calendarPortletFeatures(self, portal_url, here_url, template, REQUEST=None):
"""Provides UI objects for calendar portlet"""

return CalendarPortletFeatures(self, portal_url, here_url, template, REQUEST)

This is the Python code that calculates all the information necessary for presenting the calendar. The methods that can be called from the portlet Page Template are highlighted :

import calendar

class CalendarPortletFeatures(object):
"""Browser view class for calendar portlet"""

# security = ClassSecurityInfo()
# security.declareObjectPublic()
# security.setDefaultAccess('allow')
# ISSUE: above security stuff don't work, only __allow_access... below works

__allow_access_to_unprotected_subobjects__ = True

month_nav_href = '%s/%s?%s&month:int=%d&year:int=%d&orig_query=%s'

def __init__(self, tool, portal_url, here_url, template, REQUEST):
"""Constructor"""

self.tool = tool
self.portal_url = portal_url
self.here_url = here_url
if REQUEST is None:
REQUEST = tool.REQUEST
portal_calendar = getToolByName(tool, 'portal_calendar')
self.portal_calendar = portal_calendar

# Localisation of calendar
calendar.setfirstweekday(tool.daysOfWeek().index(tool.firstDayOfWeek))

self.current = DateTime()
self.current_day = self.current.day()

# First priority goes to the data in the REQUEST
year = REQUEST.get('year', None)
month = REQUEST.get('month', None)

# Next get the data from the SESSION
session = None
if portal_calendar.getUseSession():
session = REQUEST.get('SESSION', None)
if session:
if not year:
year = session.get('calendar_year', None)
if not month:
month = session.get('calendar_month', None)

# Store the results in the session for next time
session.set('calendar_year', year)
session.set('calendar_month', month)

# Last resort to today
if not year:
year = self.current.year()
if not month:
month = self.current.month()
self.year, self.month = int(year), int(month)

# query for previous/next
self.anchor_url = REQUEST.get('anchor_url', here_url)
self.anchor_method = REQUEST.get('anchor_method', template.getId())
query_string = REQUEST.get('orig_query', None)
self.query_string = ((query_string is None
and REQUEST.get('QUERY_STRING', None))
or query_string)

# i18n resources
self.translation_service = getToolByName(tool, 'translation_service')

return


def previousMonth(self):
"""Features for previous month link"""

if self.month in (0, 1):
prev_month = 12
prev_year = self.year - 1
else:
prev_month = self.month - 1
prev_year = self.year

link = (self.month_nav_href %
(self.anchor_url, self.anchor_method, self.query_string,
prev_month, prev_year, url_quote(self.query_string)))
prev_year_min = self.current - 365
condition = (self.year, self.month) > (prev_year_min.year(), prev_year_min.month())
return ItemWithAttributes(link=link, condition=condition)


def thisMonth(self):
"""Features for this month (portlet title)"""

month_english = self.translation_service.month_english(self.month)
month_tag_id = 'calendar-month-%s' % month_english
month_value = self.translation_service.utranslate(
'plone', self.translation_service.month_msgid(self.month),
{}, context=self.tool, default=month_english)
year_tag_id = 'calendar-year-%d' % self.year
return ItemWithAttributes(month_tag_id=month_tag_id,
month_value=month_value,
year_tag_id=year_tag_id,
year_value=str(self.year))


def nextMonth(self):
"""Features for next month link"""

if self.month == 12:
next_month = 1
next_year = self.year + 1
else:
next_month = self.month + 1
next_year = self.year

link = (self.month_nav_href %
(self.anchor_url, self.anchor_method, self.query_string,
next_month, next_year, url_quote(self.query_string)))
next_year_max = self.current + 365
condition = (self.year, self.month) < (next_year_max.year(), next_year_max.month())
return ItemWithAttributes(link=link, condition=condition)


def weekDays(self):
"""Days nicks"""

weekday_numbers = self.portal_calendar.getDayNumbers()
utranslate = self.translation_service.utranslate
day_msgid = self.translation_service.day_msgid
weekday_english = self.translation_service.weekday_english
return [utranslate('plone', day_msgid(dn, format='s'), {}, context=self.tool,
default=weekday_english(dn, format='a'))
for dn in weekday_numbers]


def weeksOfMonth(self):
"""Features for weeks"""

weeks = self.portal_calendar.getEventsForCalendar(self.month, self.year)
show_this_month = (self.current.month() == self.month
and self.current.year() == self.year)
day_css_classes = {
(True, True): 'todayevent',
(True, False): 'event',
(False, True): 'todaynoevent',
(False, False): None
}
calendar_portal_types = list(self.portal_calendar.getCalendarTypes())
calendar_states = list(self.portal_calendar.getCalendarStates())
toLocalizedTime = self.translation_service.ulocalized_time
getEventString = self.tool.getEventString
view_month = []
for week in weeks:
view_week = []
for day in week:
int_daynumber = int(day['day'])
day_event = day['event']
is_today = show_this_month and self.current_day == int_daynumber
css_class = day_css_classes[(day_event, is_today)]
if day_event:
cur_date = DateTime(self.year, self.month, int_daynumber)
link = self.portal_url + '/search?'
link += make_query(
portal_type=calendar_portal_types,
review_state=calendar_states,
start={'query': [cur_date.latestTime()], 'range': 'max'},
end={'query': [cur_date.earliestTime()], 'range': 'min'},
sort_on='start')
localized_date = toLocalizedTime(cur_date, None, self.tool,
domain='plone')
title = '\n'.join([localized_date]
+ [getEventString(e) for e in day['eventslist']])
else:
link = None
title = None
view_week.append(ItemWithAttributes(
css_class=css_class,
daynumber=int_daynumber,
day_event=day_event,
link=link,
title=title))
# /for day...
view_month.append(view_week)
# /for week...
return view_month


Five

Posted by Martin Aspeli at March 20, 2006 - 14:37
This would need to be implemented with a Five view, not a new tool, imho. Otherwise, it's a good idea to refactor the portlet. In fact, I thought some refactoring (including the use of a view) already happened for 2.5?

repeating events?

Posted by John DeStefano at December 5, 2006 - 20:32
My apologies for putting this here, but I wasn't sure where else it might belong...

Is there any plan for adding repeating events to the calendar portlet? Its absence is likely the single-most calendar-related feature I've heard of so far.

For any issues with the web site functionality, please file a ticket.

Please consult the policy on plone.org content if you want your content published on this site.

Servers and hosting by