Eventish content types¶
Description
Creating and programming event and eventish content types in Plone
Introduction¶
Plone supports events as content. Events have a start time, end time
and other fields. They can be exported to standard vCal
(compatible with
Outlook) and iCal
(compatible with OSX) formats. A default calendar
shows published events in a calendar view.
Note
Recurring events (events repeating with an interval) are not supported out-of-the-box on Plone 4.0 or older.
Further reading¶
- vs.event
- recurring events for Plone 3 and 4.0
- plone.app.event
- recurring events for Plone 4.1+
- Dateable
- Plone code to bring all the different calendar extensions together
portal_calendar
¶
The portal_calendar
service is provided by Products.CMFCalendar
.
It provides facilities to query the event calendar conveniently.
The most useful portal_calendar
call is
portal_calendar.getEventsForCalendar(month, year, path=navigation_root_path)
to get the event listing of a certain month.
Adding a new event type to the calendar¶
Use-case: you've created a content type and want it to be shown in the calendar portlet.
First add a custom import step. In profiles/default/import_steps.xml
<?xml version="1.0"?>
<import-steps>
<import-step
id="compass-types-various"
version="20090725-02"
handler="compass.types.setuphandlers.importVarious"
title="Additional Compass Types Setup">
</import-step>
</import-steps>
Then in this custom step call the portal_calendar
service.
Note that you might want to preserve the existing event types.
Plone's default event type is called Event
.
setuphandlers.py
:
from Products.CMFCore.utils import getToolByName
def addCalendarTypes(portal):
portal_calendar = getToolByName(portal, 'portal_calendar')
# 'Event' was already here, we're just adding the
# 'DD Training Class' content-type.
portal_calendar.calendar_types = ('Event', 'DD Training Class')
def importVarious(context):
"""Miscellaneous steps import handle
"""
if context.readDataFile('compass.types_various.txt') is None:
return
portal = context.getSite()
addCalendarTypes(portal)
Credits: ecarloshanson, optilude.
Getting eventish content types¶
portal_calendar
maintains the list of eventish content types
appearing in Plone calendar services.
Example:
# Get tuple of portal_type names for eventish content types
supported_event_types = portal_calendar.getCalendarTypes()
Getting calendar publishing states¶
Workflow states in which events appear in the calendar:
portal_calendar.getCalendarStates()
iCal export¶
Plone 3+ provides ics_view
which applies to:
- Single Event content items
- Folders
The view creates an iCal
export of the content.
A single exported iCal
file (mimetype: text/calendar
) can contain
several events.
When applied to a folder, the view exports all items that provide the
Products.ATContentTypes.interfaces.ICalendarSupport
interface.
More info:
Purging old events¶
After the event end day the event stays visible in Plone listings.
You need to have a special janiator script / job if you want to get old events deleted from your site after they have been passed.
Below is a ZMI script which will delete events which are more than 30 days past their ending date:
from StringIO import StringIO
import DateTime
buf = StringIO()
# DateTime deltas are days as floating points
# Select events which have the event ending date more than one month in past
end = DateTime.DateTime() - 30*1
start = DateTime.DateTime(2000, 1,1)
date_range_query = { 'query':(start,end), 'range': 'min:max'}
items = context.portal_catalog.queryCatalog({
"Language": "all", # Bypass LinguaPlone language check
"portal_type":["CompanyEvent", "VSEvent"],
"end" : date_range_query,
"sort_on" : "created" })
items = list(items)
print >> buf, "Found %d items to be purged" % len(items)
count = 0
for b in items:
count += 1
obj = b.getObject()
print >> buf, "Deleting:" + obj.absolute_url() + " " + str(obj.created())
obj.aq_parent.manage_delObjects([obj.getId()])
return buf.getvalue()
Recurrence calendar support in Plone 3¶
vs.event
has an index recurrence_days
which stores the dates when the recurrent event
appears five years ahead of the time when the event is saved.
Below is the glue code which is needed to support
the recurrent event in the Plone 3 calendar portlet.
It combines vs.event
, plone.app.portlets
and Products.CMFCalendar
bits to pull the necessary stuff together (a task which was not
trivial).
Making recurrent event appear in the calendar portlet¶
Below is a calendar portlet Renderer
code
which can be used to make recurrent events appear in the
standard Plone calendar portlet:
"""
Override the default Plone 3 calendar portlet to support
rendering of recurring events.
"""
import datetime
from Acquisition import aq_inner
from DateTime import DateTime
from zope.i18nmessageid import MessageFactory
from zope.interface import implements
from zope.component import getMultiAdapter
from plone.app.portlets.portlets import calendar as base
# Package with various calendar support code
# - not very well documented
import dateable.kalends
def convert_to_indexed_format(year, month, daynumber):
""" Convert datetime to vs.event recurrence_days index format.
recurrence_days holds the date as compressed int format
for efficiency reasons.
See vs.event.context.recurrence for more information.
@return: Indexed recurrenct_day format of given date or None if not supported
"""
# This is an empty cell in the calendar and does not represent any meaningful day
if daynumber == 0:
return None
cur_date = datetime.date(year, month, daynumber)
return cur_date.toordinal()
def create_event_structure(portal_calendar, results, year, month):
""" Create calendar dict/list struct for event presentation.
This code is mostly ripped from Products.CMFCalendar.calendar.CalendarTool catalog_getevents()
@param results: Iterable of eventish brain objects
@return: Dict day number -> event data
"""
last_day = portal_calendar._getCalendar().monthrange(year, month)[1]
first_date = portal_calendar.getBeginAndEndTimes(1, month, year)[0]
last_date = portal_calendar.getBeginAndEndTimes(last_day, month, year)[1]
# compile a list of the days that have events
eventDays={}
for daynumber in range(1, 32): # 1 to 31
eventDays[daynumber] = {'eventslist': [],
'event': 0,
'day': daynumber}
includedevents = []
for result in results:
if result.getRID() in includedevents:
break
else:
includedevents.append(result.getRID())
event={}
# we need to deal with events that end next month
if result.end.month() != month:
# doesn't work for events that last ~12 months
# fix it if it's a problem, otherwise ignore
eventEndDay = last_day
event['end'] = None
else:
eventEndDay = result.end.day()
event['end'] = result.end.Time()
# and events that started last month
if result.start.month() != month: # same as above (12 month thing)
eventStartDay = 1
event['start'] = None
else:
eventStartDay = result.start.day()
event['start'] = result.start.Time()
event['title'] = result.Title or result.getId
if eventStartDay != eventEndDay:
allEventDays = range(eventStartDay, eventEndDay+1)
eventDays[eventStartDay]['eventslist'].append(
{'end': None,
'start': result.start.Time(),
'title': event['title']} )
eventDays[eventStartDay]['event'] = 1
for eventday in allEventDays[1:-1]:
eventDays[eventday]['eventslist'].append(
{'end': None,
'start': None,
'title': event['title']} )
eventDays[eventday]['event'] = 1
if result.end == result.end.earliestTime():
last_day_data = eventDays[allEventDays[-2]]
last_days_event = last_day_data['eventslist'][-1]
last_days_event['end'] = (result.end-1).latestTime().Time()
else:
eventDays[eventEndDay]['eventslist'].append(
{ 'end': result.end.Time()
, 'start': None, 'title': event['title']} )
eventDays[eventEndDay]['event'] = 1
else:
eventDays[eventStartDay]['eventslist'].append(event)
eventDays[eventStartDay]['event'] = 1
# This list is not uniqued and isn't sorted
# uniquing and sorting only wastes time
# and in this example we don't need to because
# later we are going to do an 'if 2 in eventDays'
# so the order is not important.
# example: [23, 28, 29, 30, 31, 23]
return eventDays
class RecurrentEventCalendarPortletRenderer(base.Renderer):
""" Support recurring events """
def retroFitRecurrentEvents(self, year, month, weeks):
"""
List recurrencing events in the calendar
1. Get a list of supported event types
2. Build a list of queried recurrence_days
3. Query all recurrent events occurring in the given month
4. Retrofit calendar data with these recurrent events.
@param weeks: Array of displayable calendar weeks.
"""
context = aq_inner(self.context)
request = self.request
portal_calendar = self.context.portal_calendar
# Get tuple of portal_type names for eventish content types
supported_event_types = portal_calendar.getCalendarTypes()
# Build a list of queried dates in recurrence_days format
recurrence_days_in_this_month = []
for week in weeks:
for day in week:
# This is an empty cell in the calendar
# and does not present a meaningful date
daynumber = day['day']
date = convert_to_indexed_format(year, month, daynumber)
if date:
recurrence_days_in_this_month.append(date)
# print "recurrence_days:" + str(recurrence_days_in_this_month)
# Query all events on the site
# Note that there is no separate list for recurrent events
# so if you want to speed up you can hardcode
# recurrent event type list here.
matched_recurrence_events = self.context.portal_catalog(
portal_type=supported_event_types,
recurrence_days={
"query":recurrence_days_in_this_month,
"operator" : "or"
})
# print "Matched events:" + str(len(list(matched_recurrence_events)))
portal_catalog = self.context.portal_catalog
for week in weeks:
for day in week:
daynumber = day['day']
# This day is a filler slot and not a real date in a calendar
if daynumber == 0:
continue
cur_date = convert_to_indexed_format(year, month, daynumber)
for event in matched_recurrence_events:
# The event hit this date
# Get event brain result id
rid = event.getRID()
# Get list of recurrence_days indexed value.
# ZCatalog holds internal Catalog object which we can directly poke in evil way
# This call goes to Products.PluginIndexes.UnIndex.Unindex class and we
# read the persistent value from there what it has stored in our index
# recurrence_days
indexed_days = portal_catalog._catalog.getIndex("recurrence_days").getEntryForObject(rid, default=[])
if cur_date in indexed_days:
# Construct event info
# See CalendarTool.catalog_getevents()
day["event"] = True # This day has events
data = {}
# Shortcut the event to be one day event (though this might not be a case)
data["start"] = None
data["end"] = None
data["title"] = event["Title"]
day["eventslist"].append(data)
def getEventsForCalendar(self):
"""
This has been overridden to call recurrent event fetcher.
The code is basically copy-paste from the base class.
"""
context = aq_inner(self.context)
year = self.year
month = self.month
portal_state = getMultiAdapter((self.context, self.request), name=u'plone_portal_state')
navigation_root_path = portal_state.navigation_root_path()
weeks = self.calendar.getEventsForCalendar(month, year, path=navigation_root_path)
# Patched recurrent events go in here
self.retroFitRecurrentEvents(year, month, weeks)
for week in weeks:
for day in week:
daynumber = day['day']
if daynumber == 0:
continue
day['is_today'] = self.isToday(daynumber)
if day['event']:
cur_date = DateTime(year, month, daynumber)
localized_date = [self._ts.ulocalized_time(cur_date, context=context, request=self.request)]
day['eventstring'] = '\n'.join(localized_date+[' %s' % self.getEventString(e) for e in day['eventslist']])
day['date_string'] = '%s-%s-%s' % (year, month, daynumber)
return weeks
Beta code notice¶
Make sure that the recurrence_days
index from vs.event
is working -
if it isn't, check
Custom indexing example
how to create your own recurrency indexer.
After you save your vs.event
content item,
you should see data in the recurrence_days
index through
portal_catalog
browsing interface.
Further reading¶
- http://plone.293351.n2.nabble.com/what-s-dateable-chronos-how-to-render-recurrence-events-in-a-calendar-portlet-tp5282788p5287261.html
vs.event
hasKeywordIndex
recurrence_days
which contains a * value created byvs.event.content.recurrence.VSRecurrenceSupport.getOccurrenceDays()
. This value is a list of dates 5 years ahead when the event occurs.- Plone 3 provides a view called
calendar_view
(configured in *Products.CMFPlone/deprecated.zcml
) but this view is not used - do not it let fool you.
Required ZCML for the indexing:
<adapter factory=".indexing.recurrence_days"/>