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>
          <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}&amp;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:

Calendar Extension Screenshot 1
Click to enlarge

Creating an embeddable calendar

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:

Calendar Extension Screenshot 2
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:

Calendar Extension Screenshot 3
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:

Calendar Extension Screenshot 4
Click to enlarge

Internationalizing the Calendar

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:

  1. Modify calendar.xsl
  2. Add the translations in the skin messages

Modifying calendar.xsl

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).

Adding translations

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.

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 its current implementation, the calendar will always show all 'Event' documents, no matter what their variant is. If someone wanted filter event variants based on the locale of the user interface, the calendar.js script would have to be improved.

An alternative cool rendering is this timeline widget.

Fields

NameValue
CategoryFrontend (wiki) tutorials & extensions