{page.title}

Google Calendar in Grails Part 2: Displaying the Calendar

Following up on Part 1 where we developed a model in Grails for a calendar app like Google calendar in grails, we’re now going to make use of this model and create the controller and view to actually show our events.

In terms of the UI, the jQuery plugin Full calendar provides all the functionality we need and is visually very similar to Google Calendar. It even supports the same month/week/day views that we see on Google calendar. So I’ve added the following resources to my Config.groovy:

grails.resources.modules = {
    core {
        resource url:'/js/jquery-1.7.1.min.js', disposition: 'head'
    }

    fullCalendar {
        dependsOn 'core'
        resource url:'/js/fullcalendar.min.js'
        resource url:'/css/fullcalendar.css'
    }
   
    calendar {
        dependsOn 'fullCalendar'

        resource url: '/js/calendar.js'
        resource url: '/css/calendar.css'

    }
 
}

For this example project I’m using the default grails layout, so the calendar.css file is needed to override some of those CSS rules that clash with fullCalendar:

#calendar {
    padding: 20px;
}

#calendar table {
    margin-bottom: 0;
}

#calendar tr>td:first-child, tr>th:first-child {
    padding-left: 0;
}

#calendar tr>td:last-child, tr>th:last-child {
    padding-right: 0;
}

#calendar th:hover, tr:hover {
    background: inherit;
}

#calendar tbody td:hover {
    background: #E1F2B6;
}

#calendar table.fc-header {
    border: none;
}

#calendar table.fc-header td:hover {
    background: inherit;
}

Our calendar.js is pretty straight forward, just some jquery that calls the fullCalendar plugin to render a calendar in a container div (#calendar) within our view:

$(document).ready(function() {
    $("#calendar").fullCalendar({
        events: 'list.json',
        header: {
            left: 'prev,next today',
            center: 'title',
            right: 'month,agendaWeek,agendaDay'
        }
    });

});

So here’s what our very simple event/index.gsp view looks like:

<html>
<head>
  <meta name="layout" content="main" />

  <title>Calendar</title>
  <r:require module="calendar" />
</head>
<body>

  <div id="calendar"></div>

</body>
</html>

This is incredibly simple so far. You’ll notice in the javascript above that the events property points to the list action as the source of our events. In the case or repeating events we need to return the start and end date (as UNIX time stamps) for each individual instance of the event that happens within the range. So our JSON should look something like this:

[
   {
      "id":1,
      "title":"Repeating MWF Event",
      "allDay":false,
      "start":1329454800,
      "end":1329458400
   },
   {
      "id":1,
      "title":"Repeating MWF Event",
      "allDay":false,
      "start":1330128744.162,
      "end":1330132344.162
   },
   {
      "id":1,
      "title":"Repeating MWF Event",
      "allDay":false,
      "start":1330387944.162,
      "end":1330391544.162
   },
   {
      "id":1,
      "title":"Repeating MWF Event",
      "allDay":false,
      "start":1330733544.162,
      "end":1330737144.162
   },
   {
      "id":1,
      "title":"Repeating MWF Event",
      "allDay":false,
      "start":1330992744.162,
      "end":1330996344.162
   },
   {
      "id":1,
      "title":"Repeating MWF Event",
      "allDay":false,
      "start":1331338344.162,
      "end":1331341944.162
   },
   {
      "id":2,
      "title":"Repeating MWF Event (different location)",
      "allDay":false,
      "start":1329783144.162,
      "end":1329786744.162
   },
   {
      "id":3,
      "title":"Just a normal event",
      "allDay":false,
      "start":1329437544.162,
      "end":1329439344.162
   }
]

Notice we need to return the start and end time for each instance, so the event with the id 1 is repeated several times with different dates corresponding to the different occurrences.

So I’ve added an EventService that contains a findOccurancesInRange method. This method returns an array of Date objects that corresponds to when the event occurs. Here’s our updated EventService.groovy class:

import org.joda.time.DateTime
import org.joda.time.Days
import org.joda.time.Weeks
import static org.joda.time.DateTimeConstants.MONDAY
import static org.joda.time.DateTimeConstants.SUNDAY
import org.joda.time.Months
import org.joda.time.Years

class EventService {

    def findOccurrencesInRange = {Event event, Date rangeStart, Date rangeEnd -&gt;
        def dates = []

        Date currentDate
        if (event.isRecurring) {
            currentDate = findNextOccurrence(event, rangeStart)

            while (currentDate &amp;&amp; currentDate &lt; rangeEnd) {
                dates.add(currentDate)
                Date nextDay = new DateTime(currentDate).plusDays(1).toDate()
                currentDate = findNextOccurrence(event, nextDay)
            }
        }
        // One time (non-recurring) event
        else {
            if (event.startTime &gt;= rangeStart &amp;&amp; event.endTime &lt;= rangeEnd) {
                dates.add(event.startTime)
            }
        }

        dates
    }

    // For repeating event get next occurrence after the specified date
    private Date findNextOccurrence(Event event, Date afterDate) {
        Date nextOccurrence

        if (!event.isRecurring) {
            // non-repeating event
            nextOccurrence = null
        } else if (event.recurUntil &amp;&amp; afterDate &gt; event.recurUntil) {
            // Event is already over
            nextOccurrence = null
        } else if (afterDate &lt; event.startTime) {
            // First occurrence
            if (event.recurType == EventRecurType.WEEKLY &amp;&amp; !(isOnRecurringDay(event, event.startTime))) {
                Date nextDay = new DateTime(event.startTime).plusDays(1).toDate()
                nextOccurrence = findNextOccurrence(event, nextDay)
            }
            else {
                nextOccurrence = event.startTime
            }
        } else {
            switch (event.recurType) {

                case EventRecurType.DAILY:
                    nextOccurrence = findNextDailyOccurrence(event, afterDate)
                    break
                case EventRecurType.WEEKLY:
                    nextOccurrence = findNextWeeklyOccurrence(event, afterDate)
                    break
                case EventRecurType.MONTHLY:
                    nextOccurrence = findNextMonthlyOccurrence(event, afterDate)
                    break
                case EventRecurType.YEARLY:
                    nextOccurrence = findNextYearlyOccurrence(event, afterDate)
                    break
            }


        }

        if (isOnExcludedDay(event, nextOccurrence)) {
            // Skip this occurrence and go to the next one
            DateTime nextDay = (new DateTime(nextOccurrence)).plusDays(1)

            nextOccurrence = findNextOccurrence(event, nextDay.toDate())
        }
        else if (event.recurUntil &amp;&amp; event.recurUntil &lt;= nextOccurrence) {
            // Next occurrence happens after recurUntil date
            nextOccurrence = null
        }

        nextOccurrence
    }

    private Date findNextDailyOccurrence(Event event, Date afterDate) {
        DateTime nextOccurrence = new DateTime(event.startTime)

        int daysBeforeDate = Days.daysBetween(new DateTime(event.startTime), new DateTime(afterDate)).getDays()
        int occurrencesBeforeDate = Math.floor(daysBeforeDate / event.recurInterval)

        nextOccurrence = nextOccurrence.plusDays((occurrencesBeforeDate + 1) * event.recurInterval)

        nextOccurrence.toDate()
    }


    private Date findNextWeeklyOccurrence(Event event, Date afterDate) {
        int weeksBeforeDate = Weeks.weeksBetween(new DateTime(event.startTime), new DateTime(afterDate)).getWeeks()
        int weekOccurrencesBeforeDate = Math.floor(weeksBeforeDate / event.recurInterval)

        DateTime lastOccurrence = new DateTime(event.startTime)
        lastOccurrence = lastOccurrence.plusWeeks(weekOccurrencesBeforeDate * event.recurInterval)
        lastOccurrence = lastOccurrence.withDayOfWeek(MONDAY)

        DateTime nextOccurrence
        if (isInSameWeek(lastOccurrence.toDate(), afterDate)) {
            nextOccurrence = lastOccurrence.plusDays(1)
        }
        else {
            nextOccurrence = lastOccurrence
        }

        boolean occurrenceFound = false

        while (!occurrenceFound) {
            if (nextOccurrence.toDate() &gt;= afterDate &amp;&amp; isOnRecurringDay(event, nextOccurrence.toDate())) {
                occurrenceFound = true
            }
            else {
                if (nextOccurrence.dayOfWeek() == SUNDAY) {
                    // we're about to pass into the next week
                    nextOccurrence = nextOccurrence.plusDays(1).plusWeeks(event.recurInterval)
                }
                else {
                    nextOccurrence = nextOccurrence.plusDays(1)
                }
            }

        }

        nextOccurrence.toDate()
    }

    private Date findNextMonthlyOccurrence(Event event, Date afterDate) {
        DateTime nextOccurrence = new DateTime(event.startTime)

        int monthsBeforeDate = Months.monthsBetween(new DateTime(event.startTime), new DateTime(afterDate)).getMonths()
        int occurrencesBeforeDate = Math.floor(monthsBeforeDate / event.recurInterval)
        nextOccurrence = nextOccurrence.plusMonths((occurrencesBeforeDate + 1) * event.recurInterval)

        nextOccurrence.toDate()
    }

    private Date findNextYearlyOccurrence(Event event, Date afterDate) {
        DateTime nextOccurrence = new DateTime(event.startTime)

        int yearsBeforeDate = Years.yearsBetween(new DateTime(event.startTime), new DateTime(afterDate)).getYears()
        int occurrencesBeforeDate = Math.floor(yearsBeforeDate / event.recurInterval)
        nextOccurrence = nextOccurrence.plusYears((occurrencesBeforeDate + 1) * event.recurInterval)

        nextOccurrence.toDate()
    }


    private boolean isInSameWeek(Date date1, Date date2) {
        DateTime dateTime1 = new DateTime(date1)
        DateTime dateTime2 = new DateTime(date2)

        ((Weeks.weeksBetween(dateTime1, dateTime2)).weeks == 0)
    }

    private boolean isOnSameDay(Date date1, Date date2) {
        DateTime dateTime1 = new DateTime(date1)
        DateTime dateTime2 = new DateTime(date2)

        ((Days.daysBetween(dateTime1, dateTime2)).days == 0)
    }

    private boolean isOnRecurringDay(Event event, Date date) {
        int day = new DateTime(date).getDayOfWeek()

        event.recurDaysOfWeek.find{it == day} != null
    }

    private def isOnExcludedDay = {Event event, Date date -&gt;
        date = (new DateTime(date)).withTime(0, 0, 0, 0).toDate()
        event.excludeDays.contains(date)
    }
}

So now we can use this method to create our list action within EventController. The fullCalendar plugin similarly posts a start and end parameter (again as a UNIX timestamp). So here’s what our action looks like:

import org.joda.time.DateTime
import org.joda.time.Instant

import grails.converters.JSON
class EventController {
  def eventService

    def index = {

    }
   
    def list = {
     def (startRange, endRange) = [params.long('start'), params.long('end')].collect { new Instant(it  * 1000L).toDate() }

        def events = Event.withCriteria {
            or {
                and {
                    eq("isRecurring", false)
                    between("startTime", startRange, endRange)
                }
                and {
                    eq("isRecurring", true)
                    or {
                        isNull("recurUntil")
                        ge("recurUntil", startRange)
                    }
                }
            }
        }
        
        // iterate through to see if we need to add additional Event instances because of recurring
        // events
        def eventList = []
        events.each {event -&gt;

            def dates = eventService.findOccurrencesInRange(event, startRange, endRange)

            dates.each { date -&gt;
                DateTime startTime = new DateTime(date)
                DateTime endTime = startTime.plusMinutes(event.durationMinutes)

                eventList 

So here’s what our calendar looks like so far:

Google Calendar in Grails Series

Check out the Live Calendar Demo

See the Grails Google Calendar Project on Github