This tutorial explores extending the EventCalendar module in Silverstripe to create a complex calendar with very specific needs.
We will create a page of Workshops for an imaginary client. Workshops are a lot like regular events, but they have unique properties such as Category and Location, and each date of the workshop has many Instructors associated with it.
As stated above, a Workshop will behave like a CalendarEvent, so we need to create a subclass of CalendarEvent called Workshop. We'll add some new fields to this class that aren't included in the parent CalendarEvent class.
mysite/code/Workshop.php
class Workshop extends CalendarEvent { static $db = array ( 'Category' => "Enum('Industry, Finance, Administration')", 'Sponsor' => 'Varchar(50)', 'RegistrationLink' => 'Varchar(100)' ); static $has_one = array ( 'Image' => 'Image' );
Then we'll get our CMS fields just like any other page type.
public function getCMSFields() { $f = parent::getCMSFields(); $f->addFieldsToTab("Root.Content.Main", new DropdownField('Category','Worshop Category', singleton('Workshop')->dbObject('Category')->enumValues()), 'Content'); $f->addFieldToTab("Root.Content.Main", new TextField('Sponsor'), 'Content'); $f->addFieldToTab("Root.Content.Main", new TextField('RegistrationLink','Registration Link'),'Content'); $f->addFieldToTab("Root.Content.Image", new ImageField('Image')); return $f; } } // end Workshop subclass
And add our controller, which we can leave empty for now.
class Workshop_Controller extends CalendarEvent_Controller { }
Next, since we have created a custom CalendarEvent subclass, we should also create a custom holder for it, as well. The WorkshopHolder class will be a subclass of Calendar (which we can also think of as an EventHolder)..
/mysite/code/WorkshopHolder.php
class WorkshopHolder extends Calendar { static $has_many = array ( 'Workshops' => 'Workshop' ); static $allowed_children = array ( 'Workshop' ); }
Notice we have set up a has_many relationship with the Workshop class. This is required to make the Events() function work for any descendant of the Calendar class.
Lastly, we'll add our WorkshopHolder controller class, which can also be left empty for now.
class WorkshopHolder_Controller extends Calendar_Controller { }
Do a /db/build?flush=1, and see that everything gets built out correctly.
Login to the CMS and create the new WorkshopHolder page type called Workshops. Configure it as you like.
Now create a new Workshop underneath it. Fill out all the relevant fields.
Don't forget to add at least one date.
Save and publish.
The next thing we have to do is set up templates for both the WorkshopHolder and Workshop class that include our new fields.
We'll start by copying the base calendar template from /event_calendar/templates/Layout/Calendar.ss
Now, somewhere below the content, we need to add some of the custom Workshop fields we created. Remember, the Events control returns CalendarDateTime objects, so we need to refer to the $Event accessor to get to those fields.
We'll add the following below the $_Content line:
<div class="workshop-image"> <% control Event.Image %> <% control CroppedImage(100,100) %> <img src="$URL" alt="" /> <% end_control %> <% end_control %> </div> <dl> <dt>Category: </dt> <dd>$Event.Category</dd> <dt>Sponsor: </dt> <dd>$Event.Sponsor</dd> </dl>
And below the title, we can add our registration link.
<h4><a href="$Event.RegistrationLink">Register now!</a></h4>
We should now have something that looks like this:
Not the prettiest thing in the world, but we can clean it up later with some CSS.
We have now demonstrated that a CalendarEvent can be extended to include many custom fields and relationships. But sometimes this isn't enough. What if each date associated with the event has its own unique properties? In this step of the tutorial, continuing with our example of a Workshop page, we'll customize our workshop dates to have a Location field.
Just like we needed a subclass of CalendarEvent to create the Workshop, we need to subclass the CalendarDateTime class to create a custom date class. We'll call it WorkshopDateTime.
/mysite/code/WorkshopDateTime.php
class WorkshopDateTime extends CalendarDateTime { static $db = array ( 'Location' => 'Varchar(50)' ); static $has_one = array ( 'Workshop' => 'Workshop' ); }
The next thing we need to do is update our Workshop class to have a relationship with WorkshopDateTime. Currently it is inheriting a relationship to CalendarDateTime through its parent. Add this block of code to the Workshop class:
/mysite/code/Workshop.php
static $has_many = array ( 'Dates' => 'WorkshopDateTime' );
Do a /db/build?flush=1 and make sure everything gets built correctly.
Now that we have created a new class for Workshop dates, we need to be able to edit the fields on the Dates table. Because the WorkshopDateTime class is not child of SiteTree, it does not have a getCMSFields() function that we can simple augment to include our new field. Fortunately, the CalendarDateTime class has a simple function available that allows us to add to the table without re-creating it.
/mysite/code/WorkshopDateTime.php
public function extendTable() { $this->addTableTitles(array( 'Location' => 'Location' )); $this->addTableFields(array( 'Location' => 'TextField' )); }
The way this works is quite simple. The CalendarDateTime class automatically calls the extendTable() method before it builds the TableField. The member methods addTableTitles() and addTableFields() pass arrays to the TableField object to to set up the titles and fields of the table. Now with just a few lines of code, we save ourselves the trouble of rebuilding the table with start/end dates and times, etc.
For convenience, the following methods are also available in the extendTable function:
Now in the CMS, when we edit a Workshop, we should have a new field on the Dates and Times tab.
Lastly, we update our template to contain the Location field.
/mysite/templates/Layout/WorkshopHolder.ss
<dl> <dt>Category: </dt> <dd>$Event.Category</dd> <dt>Sponsor: </dt> <dd>$Event.Sponsor</dd> <dt>Location: </dt> <dd>$Location</dd> </dl>
Notice that we do not use the $Event accessor to display the Location field because it is part of the Date object, not the Event object.
Notice that while the Workshop is the same for each entry, the Location varies with each date.
We can add simple fields to the Workshop dates, but what if each Workshop has many Instructors associated with it? Let's walk through it.
Add to your WorkshopDateTime class:
/mysite/code/WorkshopDateTime.php
static $many_many = array ( 'Instructors' => 'StaffMember' );
The StaffMember object can be simple. Let's just use this for now:
/mysite/code/StaffMember.php
class StaffMember extends DataObject { static $db = array ( 'Name' => 'Varchar(50)', 'Title' => 'Varchar(50)', 'Description' => 'Text' ); static $belongs_many_many = array ( 'WorkshopDateTime' => 'WorkshopDateTime' ); }
Run a /db/build?flush=1 and make sure everything gets built correctly.
The problem with a many-to-many relationship is that it exceeds the capabilities of a TableField, which cannot handle a CheckboxSetField. Once again, we'll turn to the extendTable() method for a solution.
/mysite/code/WorkshopDateTime.php
public function extendTable() { $this->setComplex(true); $this->addTableTitles(array( 'Location' => 'Location' )); $this->removeTableTitle('is_all_day'); $staff = DataObject::get("StaffMember"); $map = $staff ? $staff->toDropdownMap('ID','Name') : array(); $this->addPopupFields(array( new TextField('Location'), new CheckboxSetField('Instructors','Instructors', $map) )); }
The setComplex() method tells the CalendarDateTime class to use a ComplexTableField in lieu of a standard TableField. From there we use the same addTableTitles() method to insert the table column headers. Since is_all_day is a boolean field usually appearing as a checkbox, it looks awkward on a ComplexTableField, when it returns a 1 or 0, so we'll take it off the table view.
Now we use addPopupFields() to add fields to the popup window.
Now we just need to display all the instructors associated with each date on our template.
/mysite/code/WorkshopHolder.ss
<dl> <dt>Category: </dt> <dd>$Event.Category</dd> <dt>Sponsor: </dt> <dd>$Event.Sponsor</dd> <dt>Location:</dt> <dd>$Location</dd> </dl> <% if Instructors %> <h4>Instructors</h4> <ul> <% control Instructors %> <li>$Name, $Title</li> <% end_control %> </ul> <% end_if %>
Now that we have new fields associated with our events, it would be nice to give the user more fields to filter the calendar. In addition to the start/end date filters that come with the CalendarFilterForm widget, let's add a Category filter.
Because of their unique functionality, filter fields are part of the custom container class CalendarFilterFieldSet. In your WorkshopHolder class, use the getFilterFields() method to obtain and manipulate this object.
/mysite/code/WorkshopHolder.php
public function getFilterFields() { $fields = parent::getFilterFields(); // returns a CalendarFilterFieldSet $fields->addFilterField(new DropdownField('Workshop_Category','Category', singleton('Workshop')->dbObject('Category')->enumValues())); return $fields; }
Notice the naming convention used on the filter field. The name of the field we're going to filter is Category. Since the Category field could exist on either our WorkshopDateTime object or our Workshop object, we need to tell the controller where to find it, so we prefix the field name with the name of the object followed by an underscore. In short, the naming convention for a filter field is [NameOfObject]_[NameOfField].
If you want to remove fields from your filter form, you can use the removeFilterField() function, e.g:
To remove all three start or end date fields, you can use these unique methods of the CalendarFilterFieldSet class.
Reload your Workshop page, and you should see a new dropdown menu on your filter form.
In this tutorial we have explored the extensibility of the EventCalendar module. We created a subclass of CalendarEvent called “Workshop” and showed that CalendarEvents can have custom fields. We also showed that each date associated with a CalendarEvent can have its own unique fields, and can even maintain complex data relationships. All of this can be done easily without duplicating code or making changes to the core classes.
Please use comments for notes, tips and corrections about the described
functionality.
Use the Silverstripe Forum to
ask questions.