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.
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:
Creating the Event document type:
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.
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 basic idea behind the calendar extension is:
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.
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:
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.
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>
<th colspan="7"><xsl:value-of select="@month"/>, <xsl:value-of select="@year"/></th>
</tr>
<tr>
<xsl:for-each select="cal:week[2]/cal:day">
<th><xsl:value-of select="@weekday"/></th>
</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>
<th colspan="7"><xsl:value-of select="@month"/>, <xsl:value-of select="@year"/></th>
</tr>
<tr>
<xsl:for-each select="cal:week[2]/cal:day">
<th><xsl:value-of select="@weekday"/></th>
</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.
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>
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 |
Now that we have some way of displaying a calendar, you may be interested in displaying this calendar in the context of a document. An application of this could be, for example, to have a 'What's happening this month' section in the home page of a site.
Embedding is simply done by doing a cocoon include. To try it out, create a new SimpleDocument (or use an existing one), click on the include icon and replace daisy:<enter document ID> by the following:
cocoon:/ext/calendar/calendar
Save your document and admire the result: we now have an embedded page (not just the calendar) that looks like this:
| Click to enlarge |
What needs to be done is thus to clean up the code get rid of everything but the calendar itself. The drawback of doing so is that pointing your browser to http://localhost:8888/daisy/<sitename>/ext/calendar/calendar will no longer produce a nice output but just a bare-bone calendar.
First let's edit the calendar extension sitemap.xmap. You need to comment out the following line: <map:transform src="daisyskin:xslt/layout.xsl"/> like so:
<!-- <map:transform src="daisyskin:xslt/layout.xsl"/> -->
Removing this line will remove most of the the daisy skin. You can view the result if you want by reloading the document where you did your cocoon include or by pointing your browser to your calendar extension. You should now see something that looks like this:
| Click to enlarge |
Now that the skin is gone, we need to edit calendar.xsl. Replace this block of code:
<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>
With this:
<xsl:template match="page">
<page>
<content>
<xsl:apply-templates select="cal:calendar" mode="full"/>
</content>
</page>
</xsl:template>
Removing the <xsl:copy-of select="context"/> line will get rid of various bits of contextual info (user name, etc.). The rest of the changes should be fairly obvious. Basically, we are removing all titles and we are only displaying the full calendar view.
Try reloading the document where you did the cocoon include and/or pointing your browser to the calendar extension. You should now have something that looks like this:
| Click to enlarge |
The current implementation of the calendar extension will always display months and days of the week in English (US) since this is hardcoded. It is still possible, however, to modify the the calendar to be more friendly to multilingual sites. Here is an overview of what we need to do to make this happen:
First thing we need to do is to add the i18n namespace to calendar.xsl. To do so, insert xmlns:i18n="http://apache.org/cocoon/i18n/2.1" in the beginning of the calendar.xsl document like so:
<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" xmlns:i18n="http://apache.org/cocoon/i18n/2.1" version="1.0">
Next, we must tell the calendar to pull the proper translations instead of simply using the hardcoded english-US string. To so, we will replace every instances of <xsl:value-of select="@month"/> and <xsl:value-of select="@weekday"/> by <i18n:text catalogue="skin" key="calendarext.months.{@month}"/> and <i18n:text catalogue="skin" key="calendarext.weekdays.{@weekday}"/> respectively. Your calendar.xsl <xsl:template match="cal:calendar" mode="full"> section should now look like this:
<xsl:template match="cal:calendar" mode="full">
<table class="default" width="100%">
<tr>
<th colspan="7"><i18n:text catalogue="skin" key="calendarext.months.{@month}"/>, <xsl:value-of-select="@year"/></th>
</tr>
<tr>
<xsl:for-each select="cal:week[2]/cal:day">
<th><i18n:text catalogue="skin" key="calendarext.weekdays.{@weekday}"/></th>
</xsl:for-each>
</tr>
<xsl:apply-templates select="cal:week" mode="full"/>
</table>
</xsl:template>
Basically, what happens here is that we are asking calendar.xsl to go look in our skin for a message titled calendarext.months.{@month} and calendarext.weekdays.{@weekday} where {@month} and {@weekday} will be replaced by the actual month (January, February, etc.) and weekday (SUNDAY, MONDAY, etc.), respectively. That that is left to do now is to create the translations for these messages (we will need one for each month and each weekday).
It is never a good idea to customize things directly in $DAISY_HOME so in order to customize the days and months label, we will first duplicate the default daisy skin into our wikidata directory. It is this copy of the skin that we will modify, instead of modifying the original. To copy the skin, issue the command below (in a linux terminal) taking care to replace the <wikidatalocation> portion with the actual path to your wikidata directory:
cp -R $DAISY_HOME/daisywiki/webapp/daisy/resources/skins/default /<wikidatalocation>/resources/skins/myskin
This will copy the default skin into the appropriate wikidata directory and rename it 'myskin'.
Before we change anything else, we must make sure that 'myskin' will be the skin used for the site on which we want to display the calendar. To set the skin for a site, open the siteconf.xml file located here (replacing <wikidatalocation> and <yoursitename> by the appropriate values for your setup):
/<wikidatalocation>/sites/<yoursitename>/siteconf.xml
Change the <skin>default</skin> value to <skin>myskin</skin>.
You should do the same change to the other language versions of your site(s) (i.e. if you have an English and a French site, you must make sure that both the English and French sites will use 'myskin').
Add the following code to the wikidata/resources/skins/myskin/i18n/message.xml and any other message_*.xml document (translate the values for weekdays and months match the language of the message_*.xml file):
<!-- Calandar extensions messages --> <message key="calendarext.weekdays.SUNDAY">Sunday</message> <message key="calendarext.weekdays.MONDAY">Monday</message> <message key="calendarext.weekdays.TUESDAY">Tuesday</message> <message key="calendarext.weekdays.WEDNESDAY">Wednesday</message> <message key="calendarext.weekdays.THURSDAY">Thursday</message> <message key="calendarext.weekdays.FRIDAY">Friday</message> <message key="calendarext.weekdays.SATURDAY">Saturday</message> <message key="calendarext.months.January">January</message> <message key="calendarext.months.February">February</message> <message key="calendarext.months.March">March</message> <message key="calendarext.months.April">April</message> <message key="calendarext.months.May">May</message> <message key="calendarext.months.June">June</message> <message key="calendarext.months.July">July</message> <message key="calendarext.months.August">August</message> <message key="calendarext.months.September">September</message> <message key="calendarext.months.October">October</message> <message key="calendarext.months.November">November</message> <message key="calendarext.months.December">December</message>
If you have done everything right, your calendar should now display the calendar in the language of the Daisy user interface.
Some ideas for improvements of this basic calendar extension:
An alternative cool rendering is this timeline widget.
| Name | Value |
|---|---|
| Category | Frontend (wiki) tutorials & extensions |