Calendar extension
Displaying a calendar in Daisy
This tutorial will show you how to create a Daisy Wiki extension to display a calendar. Each date of the calendar will be associated with documents in the Daisy repository based on a date field in the documents.
Create a field and document type
For the purpose of this tutorial, we will use a document type called "Event" with a field called "EventDate" (of type date). We will create these first.
Creating the EventDate field type:
- Login to Daisy, switch the role to Administrator, click on the Administrator link in the navigation
- Select "Manage Field Types"
- Select "Create a new field type"
- In the name field, enter EventDate
- In the value type dropdown, choose date
- Leave the other settings untouched
- Press save
Creating the Event document type:
- Select "Document types" in the navigation
- Select "Create a new document type"
- In the name field, enter Event
- In the drop down below the header "Field Types", select EventDate and press the Add Field Type button
- Leave the other settings untouched
- Press save
The document type can have any other fields and parts you need to describe the event. And it doesn't have to describe an 'event' per se, any document type with a date field can be used.
Create some event documents
You can now create a couple of Event documents. For this, go to a Daisy site (via the Daisy Home link in the top right corner), select New Document in the navigation, and then select the Event document type. In the EventDate field, select a date in the current month, as the calendar display we are going to create will show the events of the current month.
The calendar extension
The basic idea behind the calendar extension is:
- perform a query of all events of the current month (will need some flowscript)
- use the Cocoon CalendarGenerator to generate a calendar of the current month
- use an XSL to merge the data from these two while formatting it as HTML
The CalendarGenerator
We will first have a look at the CalendarGenerator.
Create a directory for the extension called calendar as a subdirectory of:
DAISY_HOME/daisywiki/webapp/daisy/sites/cocoon/
so that you get:
DAISY_HOME/daisywiki/webapp/daisy/sites/cocoon/calendar
In this directory, create a file called sitemap.xmap with the following contents:
<?xml version="1.0"?>
<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">
<map:components>
<map:generators>
<map:generator name="calendar"
src="org.apache.cocoon.generation.CalendarGenerator"/>
</map:generators>
</map:components>
<map:views>
</map:views>
<map:resources>
</map:resources>
<map:pipelines>
<map:pipeline>
<map:match pattern="currentMonthCalendar">
<map:generate type="calendar">
<map:parameter name="dateFormat" value="M/d/yy"/>
<map:parameter name="lang" value="en"/>
<map:parameter name="country" value="US"/>
</map:generate>
<map:serialize type="xml"/>
</map:match>
</map:pipeline>
</map:pipelines>
</map:sitemap>
This sitemap describes that when a request is made for the path "currentMonthCalendar", a pipeline should be executed which generates calendar data (technically: as SAX-events) and then serializes this as XML.
You can try this out by surfing to the following URL in your browser:
http://localhost:8888/daisy/<sitename>/ext/calendar/currentMonthCalendar
in which you change <sitename> to the name of your site.
What you will now see depends on your browser, if you do not see the calendar XML use the view source command of your browser. The calendar XML looks like this:
<calendar:calendar xmlns:calendar="http://apache.org/cocoon/calendar/1.0"
year="2006" month="February"
prevYear="2006" prevMonth="01"
nextYear="2006" nextMonth="03">
<calendar:week number="1">
<calendar:day number="1" weekday="WEDNESDAY" date="2/1/06"/>
<calendar:day number="2" weekday="THURSDAY" date="2/2/06"/>
<calendar:day number="3" weekday="FRIDAY" date="2/3/06"/>
<calendar:day number="4" weekday="SATURDAY" date="2/4/06"/>
</calendar:week>
<calendar:week number="2">
<calendar:day number="5" weekday="SUNDAY" date="2/5/06"/>
<calendar:day number="6" weekday="MONDAY" date="2/6/06"/>
<calendar:day number="7" weekday="TUESDAY" date="2/7/06"/>
<calendar:day number="8" weekday="WEDNESDAY" date="2/8/06"/>
<calendar:day number="9" weekday="THURSDAY" date="2/9/06"/>
<calendar:day number="10" weekday="FRIDAY" date="2/10/06"/>
<calendar:day number="11" weekday="SATURDAY" date="2/11/06"/>
</calendar:week>
...
We have instructed the CalendarGenerator to format the dates in its output in exactly the same format as the dates returned by Daisy in its query results when the locale is US. We will use this date attribute to join the information from the query result with the calendar.
On to the actual calendar extension
The flowscript
In the calendar extension directory, create a file called calendar.js with the following content:
cocoon.load("resource://org/outerj/daisy/frontend/util/daisy-util.js");
importClass(Packages.java.util.Calendar);
importClass(Packages.java.util.GregorianCalendar);
importClass(Packages.org.outerj.daisy.repository.query.QueryHelper);
importClass(Packages.org.outerj.daisy.frontend.util.XmlObjectXMLizable);
function showCalendar() {
var daisy = new Daisy();
var pageContext = daisy.getPageContext();
var beginOfMonth = new GregorianCalendar();
beginOfMonth.set(Calendar.DAY_OF_MONTH, 1);
var endOfMonth = new GregorianCalendar();
endOfMonth.set(Calendar.DAY_OF_MONTH, endOfMonth.getActualMaximum(Calendar.DAY_OF_MONTH));
var queryManager = daisy.getRepository().getQueryManager();
var events = queryManager.performQuery("select $EventDate, name where documentType = 'Event'"
+" and $EventDate between "
+ QueryHelper.formatDate(beginOfMonth.getTime()) + " and "
+ QueryHelper.formatDate(endOfMonth.getTime()), java.util.Locale.US);
var viewData = new Object();
viewData["pageContext"] = pageContext;
viewData["events"] = new XmlObjectXMLizable(events);
cocoon.sendPage("CalendarPipe", viewData);
}
It should be easy to follow what this Cocoon flowscript does:
- It uses the daisy-util.js for getting easy access to the Daisy Wiki things (see the Daisy Wiki extension documentation)
- It finds out the beginning and the end of the current month
- Using the Daisy API, it executes a query on the repository. Note that we specify a hardcoded locale of Locale.US to be sure the returned dates will be in the same format as in the CalendarGenerator output. The first selected value in the query is the EventDate field, the second the document name. Our XSL will rely on this, so don't change this.
- A map viewData is created containing some items which we want to make available to the view.
- A pipeline is called which will be responsible for rendering the calendar (see further on).
The template to aggregate all needed data
Our CalendarPipe will start from a JXTemplate to generate the input for the XSL. Therefore, create a file called calendar.xml with the following content:
<?xml version="1.0"?>
<page xmlns:cinclude="http://apache.org/cocoon/include/1.0">
${pageContext}
${events}
<cinclude:include src="cocoon:/currentMonthCalendar"/>
</page>
The ${pageContext} and ${events} refer to the items passed in the viewData map of the flowscript. To merge the calendar data from the CalendarGenerator, we use an include instruction, which will be processed by the CInclude transformer, see the pipeline definition further on. The include URL starts with "cocoon:", which means it includes the output from another pipeline defined in the Cocoon sitemap.
The XSL to render the calendar
Now we arrive at the XSL. Create a file calendar.xsl with the following content:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:d="http://outerx.org/daisy/1.0"
xmlns:cal="http://apache.org/cocoon/calendar/1.0"
version="1.0">
<xsl:variable name="eventRows" select="/page/d:searchResult/d:rows"/>
<xsl:variable name="mountPoint" select="/page/context/mountPoint"/>
<xsl:variable name="siteName" select="/page/context/site/@name"/>
<xsl:variable name="basePath" select="concat($mountPoint, '/', $siteName)"/>
<xsl:template match="page">
<page>
<xsl:copy-of select="context"/>
<content>
<h1>Current Month Events</h1>
<h2><xsl:value-of select="concat(cal:calendar/@month, ' ', cal:calendar/@year)"/></h2>
<xsl:apply-templates select="cal:calendar" mode="compact"/>
<br/>
<br/>
<xsl:apply-templates select="cal:calendar" mode="full"/>
</content>
</page>
</xsl:template>
<xsl:template match="cal:calendar" mode="compact">
<table class="default">
<tr>
<xsl:for-each select="cal:week[2]/cal:day">
<td><xsl:value-of select="@weekday"/></td>
</xsl:for-each>
</tr>
<xsl:apply-templates select="cal:week" mode="compact"/>
</table>
</xsl:template>
<xsl:template match="cal:week" mode="compact">
<tr>
<xsl:if test="position() = 1">
<xsl:call-template name="insertEmptyCells">
<xsl:with-param name="count" select="7 - count(cal:day)"/>
</xsl:call-template>
</xsl:if>
<xsl:for-each select="cal:day">
<td>
<xsl:value-of select="@number"/>
<xsl:variable name="currentDate" select="@date"/>
<!-- Note: the [1] behind $eventRows is not really needed,
but is to work around a Xalan bug -->
<xsl:variable name="eventCount"
select="count($eventRows[1]/d:row[d:value[1] = $currentDate])"/>
<xsl:if test="$eventCount > 0">
(<xsl:value-of select="$eventCount"/>)
</xsl:if>
</td>
</xsl:for-each>
<xsl:if test="position() = last()">
<xsl:call-template name="insertEmptyCells">
<xsl:with-param name="count" select="7 - count(cal:day)"/>
</xsl:call-template>
</xsl:if>
</tr>
</xsl:template>
<xsl:template match="cal:calendar" mode="full">
<table class="default">
<tr>
<xsl:for-each select="cal:week[2]/cal:day">
<td><xsl:value-of select="@weekday"/></td>
</xsl:for-each>
</tr>
<xsl:apply-templates select="cal:week" mode="full"/>
</table>
</xsl:template>
<xsl:template match="cal:week" mode="full">
<tr>
<xsl:if test="position() = 1">
<xsl:call-template name="insertEmptyCells">
<xsl:with-param name="count" select="7 - count(cal:day)"/>
</xsl:call-template>
</xsl:if>
<xsl:for-each select="cal:day">
<td style="vertical-align: top">
<xsl:value-of select="@number"/>
<xsl:variable name="currentDate" select="@date"/>
<xsl:for-each select="$eventRows[1]/d:row[d:value[1] = $currentDate]">
<br/>
<a href="{$basePath}/{@documentId}.html?branch={@branchId}&language={@languageId}">
<xsl:value-of select="d:value[2]"/>
</a>
</xsl:for-each>
</td>
</xsl:for-each>
<xsl:if test="position() = last()">
<xsl:call-template name="insertEmptyCells">
<xsl:with-param name="count" select="7 - count(cal:day)"/>
</xsl:call-template>
</xsl:if>
</tr>
</xsl:template>
<xsl:template name="insertEmptyCells">
<xsl:param name="count"/>
<td/>
<xsl:if test="$count > 1">
<xsl:call-template name="insertEmptyCells">
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
This XSL produces input suited for the layout.xsl as defined by the layout.xsl input specification. See the docs on skinning for more details on this. The XSL creates two different renderings of the calendar: a compact that only shows the number of events on each date, and a larger one which displays the actual events on each day, linked to the event document.
The sitemap
Finally, replace the content of the existing sitemap.xmap file with the following:
<?xml version="1.0"?>
<map:sitemap xmlns:map="http://apache.org/cocoon/sitemap/1.0">
<map:components>
<map:generators>
<map:generator name="calendar" src="org.apache.cocoon.generation.CalendarGenerator"/>
</map:generators>
</map:components>
<map:views>
</map:views>
<map:resources>
</map:resources>
<map:flow language="javascript">
<map:script src="calendar.js"/>
</map:flow>
<map:pipelines>
<map:pipeline internal-only="true" type="noncaching">
<map:parameter name="outputBufferSize" value="8192"/>
<map:match pattern="currentMonthCalendar">
<map:generate type="calendar">
<map:parameter name="dateFormat" value="M/d/yy"/>
<map:parameter name="lang" value="en"/>
<map:parameter name="country" value="US"/>
</map:generate>
<map:serialize/>
</map:match>
<map:match pattern="CalendarPipe">
<map:generate type="jx" src="calendar.xml"/>
<map:transform type="cinclude"/>
<map:transform src="calendar.xsl"/>
<map:transform src="daisyskin:xslt/layout.xsl"/>
<map:transform type="i18n">
<map:parameter name="locale" value="{request-attr:localeAsString}"/>
</map:transform>
<map:serialize/>
</map:match>
</map:pipeline>
<map:pipeline type="noncaching">
<map:parameter name="outputBufferSize" value="8192"/>
<map:match pattern="calendar">
<map:call function="showCalendar"/>
<map:serialize/>
</map:match>
</map:pipeline>
</map:pipelines>
</map:sitemap>
Trying it out
You can now try it out by surfing to:
http://localhost:8888/daisy/<sitename>/ext/calendar/calendar
Replace the <sitename> in this URL with the name of an actual site.
You should see something like in this screenshot:
| Click to enlarge |
Conclusion and further notes
Some ideas for improvements of this basic calendar extension:
- The layout in the above example is kept very simle on purpose, but with some changes to the XSL it could easily be made more attractive.
- Allow paging through the months and years.
- Make it generic by allowing to specify the document type and date field as URL parameters.
- In this example we made the calendar a stand-alone page. It is easy to adjust it to make the calendar embeddable in any Daisy Wiki document: throw out the layout.xsl transform from the sitemap, let the calendar.xsl produce an embeddable piece of HTML (something with a <div> as root element), and then in any Daisy document do an include of the URL 'cocoon:/ext/calendar/calendar'.
- With the new support for functions in the query language in Daisy 1.5, it will be possible to add a FormatDate function to reliably format the date independent of the used locale. Furthermore the calculation of the beginOfMonth and endOfMonth dates could be done directly with functions in the query language. (Note: at the time of this writing such functions do not yet exist, but it sure would be useful to add them)
An alternative cool rendering is this timeline widget.



I would like som more in depth explanation of this if possible.