This guide describes how to implement the calendar interface in order to inject events into the calendar module.
All interface classes reside within the interface
directory of the calendar module. Calendar interface implementations
should reside within the integration/calendar
directory of your module.
Note: Calendar v1.0 switched from an array type interface to real class level interfaces. The old array type interface is still supported but deprecated.
A calendar item type is used to provide some meta data of your custom event type as for example:
title
: A translatable titledescription
: Short translatable description of your item typedefault color
(optional): A default color used for this item type, which can be overwritten in the calendar module configicon
(optional): Icon related to this event type e.g.fa-calendar
Add your own custom calendar item type by implementing the humhub\modules\calendar\interfaces\CalendarTypeIF
as follows:
CustomItemType.php:
use humhub\modules\calendar\interfaces\CalendarTypeIF;
class CustomItemType implements CalendarTypeIF
{
const ITEM_TYPE = 'customEvent';
public function getKey()
{
static::ITEM_TYPE;
}
public function getDefaultColor()
{
return '#ffffff';
}
public function getTitle()
{
return Yii::t('MymoduleModule.integration', 'CustomEvent');
}
public function getDescription()
{
return Yii::t('MymoduleModule.integration', 'A custom calendar event');
}
public function getIcon()
{
return 'fa-calendar-o';
}
}
Configure an event listener for the getItemTypes
of humhub\modules\calendar\interfaces\CalendarService
:
config.php:
return [
'id' => 'mymodule',
'class' => 'mymodule\Module',
'namespace' => 'mymodule',
'events' => [
//...
['class' => 'humhub\modules\calendar\interfaces\CalendarService', 'event' => 'getItemTypes', 'callback' => ['mymodule\Events', 'onGetCalendarItemTypes']],
],
];
Note: Define the class name as string to prevent a strict dependency to the calendar module.
Event.php:
public static function onGetCalendarItemTypes(CalendarItemTypesEvent $event)
{
$contentContainer = $event->contentContainer;
if(!$contentContainer || $contentContainer->isModuleEnabled('mymodule')) {
$event->addType(CustomItemType::ITEM_TYPE, new CustomItemType());
}
}
Your custom item type should now be listed within the Other calendars
section of your global and space calendar module settings (in case the module is enabled).
Note: Don't forget to check if your module is enabled on the given
$event->contentContainer
. If nocontentContainer
is given it's meant to be a global search for all available calendar item types.
Custom event models have to implement the humhub\modules\calendar\interfaces\CalendarEventIF
.
In most cases you'll want to implement an ActiveRecord
based event model, or even better a ContentActiveRecord
based
model. The following database fields should be used for your event model:
uid
: An event uid id which will be assigned automatically by the calendar interface in caseEditableEventIF
is implemented.start_datetime
: the start date timeend_datetime
: end date time
Note: In case you want to keep the dependency of your module to the calendar module optional, you should not directly implement the
CalendarEventIF
on the model class and instead implement a integration adapter class within yourintegration/calendar
directory.
The following example shows a calendar integration by means of a ContentActiveRecord
, with optional dependency to the calendar module.
First implement your custom ContentActiveRecord
class:
mymodule/models/CustomEvent.php:
namespace mymodule/models;
class CustomEvent extends ConentActiveRecord {
// Model logic of your module
}
Then implement the integration adapter class:
mymodule/integration/calendar/CustomCalendarEvent.php:
namespace mymodule/integration/calendar;
class CustomCalendarEvent extends CustomEvent implements CalendarEventIF {
// Implement all missing CalendarEventIF functions not alrady defined in your base model class
public static function find()
{
return new ActiveQueryContent(static::class);
}
public static function getObjectModel() {
return CustomEvent::class;
}
}
Note: the
getObjectModel()
needs to be overwritten in order to force the content relation to the originalCustomEvent
class in the content table instead ofCustomCalendarEvent
.
Note: the
find()
function needs to be overwritten in order to stay compatible with HumHub version < 1.4, if your modules min-version is = 1.4 you can omit this.
Here is a short description of the interfac functions:
getUid()
: The event uid as described above, when implementingEditableEventIF
, this uid will be assigned automatically if no custom uid was assigned.getType()
: returns an instance of the related calendar typeisAllDay()
: weather or not this event is an all day eventgetStartDateTime()
: start DateTime object of this eventgetEndDateTime()
: end DateTime object of this eventgetTimezone()
: string the timezone string of this eventgetEndTimezone()
: can be used in case the end date timezone differs from the start timezone, otherwise can be nullgetUrl()
: An url to the detail-view of this event, this will be used in the upcoming event snippet and in the calendar viewgetTitle()
: The event titlegetDescription()
: The event descriptiongetLastModified()
: The last modified DateTime used for ICal export e.g.new DateTime($this->content->updated_at);
getColor()
: (optional) A color used within the calendar view and sidebar snippet, if null is returned the default color will be usedgetSequence()
: (optional) The event revision sequencegetLocation()
: (optional) an event location stringgetBadge()
: (optional) ahumhub\widgets\Label
or string used in the sidebar snippetgetCalendarOptions()
: (optional) additional event configuration
Optional functions can return null if not supported.
The calendar module will trigger the findItems
event of humhub\modules\calendar\interfaces\CalendarService
to fetch
all events from external modules. The event may contain different filters and the search range. In order to ease the implementation
of filtering your events, you should extend the humhub\modules\calendar\interfaces\event\AbstractCalendarQuery
class.
The following example shows the most basic implementation of a custom event query:
mymodule/integration/calendar/CustomCalendarEventQuery:
class CustomCalendarEventQuery extends AbstractCalendarQuery
{
protected static $recordClass = CustomCalendarEvent::class;
}
The previous example implies that our CustomCalendarEvent
model uses the default database fields for start_datetime
and end_datetime
and the default database datetime format Y-m-d H:i:s
.
The AbstractCalendarQuery:dateQueryType
is used to define the behaviour of the date query, by setting one of the following values:
AbstractCalendarQuery:DATE_QUERY_TYPE_TIME
(default): Will assume all dates are timezone relevant and the default date format isY-m-d H:i:s
.AbstractCalendarQuery:DATE_QUERY_TYPE_DATE
: Will assume all dates are all day events without timezone translations the default date format isY-m-d
.AbstractCalendarQuery:DATE_QUERY_TYPE_MIXED
: Should be used in case all day events and time relevant events are mixed, the query will only consider timezone offset differences between user and the system when anall_day
database flag is not set.
Beside the dateQueryType
the following fields can be overwritten in order to change the default behavior:
recordClass
: Required in order to set yourActiveRecord
class used for the queryallDayField
: Defines the database field name of your models all day flag. This is only required when usingDATE_QUERY_TYPE_MIXED
(default isall_day
)startField
: The name of your start date field (defaultstart_datetime
)endField
: The name of your end date field (defaultend_datetime
)dateFormat
: The date format of start and end fields see above
In case your model extends ContentActiveRecord
the query class provides a default implementation for the following filter:
filterDashboard()
: this filter is used for the dashboard upcoming events snippets, by default this filter will make use of theUSER_RELATED_SCOPE_SPACE
andUSER_RELATED_SCOPE_OWN_PROFILE
filterGuests()
: used for guest users which are not able to use other filtersfilterUserRelated()
: used for user related queries e.g: 'Only content from following spaces' (seeActiveQueryContent::userRelated
)filterContentContainer()
: used to filter content of a specific ContentContainer (Space/User)filterReadable()
: only include content readable by the current userfilterMine()
: only include items created by mesetupDateCriteria()
: responsible for the date interval filter
Some filter can be implemented manually if supported by your model:
filterIsParticipant()
: in case the item type supports an own participation logic, this filter is used to only include items in which the current logged in user participates (optional)
In case a filter is not supported, the respective filter function should throw a FilterNotSupportedException
, which by default is the case for
all filters except the date criteria filter when a non ContentActiveRecord
is used as base model.
Note: Guest users are not able to use other filters than the
filterGuests
The following example shows the implementation of a more complex AbstractCalendarQuery with custom start and end date field name , custom date format and custom participation filter.
MeetingCalendarQuery example:
class MeetingCalendarQuery extends AbstractCalendarQuery
{
protected static $recordClass = Meeting::class;
public $startField = 'start_date';
public $endField = 'end_date';
/**
* @inheritdoc
*/
public function filterIsParticipant()
{
$this->_query->leftJoin('meeting_participant', 'meeting.id=meeting_participant.meeting_id AND meeting_participant.user_id=:userId', [':userId' => $this->_user->id]);
$this->_query->andWhere('meeting_participant.id IS NOT NULL');
}
}
In order to inject our events to the calendar we need to listen to the findItems
event of humhub\modules\calendar\interfaces\CalendarService
as follows:
config.php:
return [
'events' => [
['class' => 'humhub\modules\calendar\interfaces\CalendarService', 'event' => 'findItems', 'callback' => ['mymodule\Events', 'onFindCalendarItems']],
],
];
Event.php:
public static function onFindCalendarItems(CalendarItemsEvent $event)
{
$contentContainer = $event->contentContainer;
if(!$contentContainer || $contentContainer->isModuleEnabled('mymodule')) {
$event->addItems(static::ITEM_TYPE_KEY, CustomCalendarEventQuery::findForEvent($event));
}
}
The humhub\modules\calendar\interfaces\event\EditableEventIF
extends the CalendarEventIF
and can be implemented in order to support auto uid
generation
when saving or fetching a model.
The interface extends the base calendar interface with the following functions:
setUid($uid)
: Set the uid of this event, in case no manual uid generation was donesave()
: Should persist all data set by this or sub interfaces.setSequence($sequence)
: (optional) Should set the sequence counter field if supported by your event type
Example:
class CustomCalendarEvent extends CustomEvent implements EditableEventIF {
// Implementation of other CalendarEventIF functions...
public function setUid($uid)
{
$this->uid = $uid;
}
public function setSequence($sequence)
{
$this-sequence = $sequence;
}
public function saveEvent()
{
return $this->save();
}
public static function find()
{
return new ActiveQueryContent(static::class);
}
}
The humhub\modules\calendar\interfaces\fullcalendar\FullCalendarEventIF
can be used to change the event
behavior in the calendar view or set additional Fullcalendar options
The following functions are available to change the event behavior:
isUpdatable()
: enables the drag/drop and resize feature of fullcalendar for this event. Here you should make sure the current logged in user is allowed to edit the underlying model e.g.$this->content->canEdit()
updateTime()
: should update and persist the start and end DateTime of this event.getCalendarViewUrl()
: here you can overwrite the url returned bygetUrl()
, usually used to provide a modal view instead of a detail view. If null is returned thegetUrl()
is used andredirect
view mode is used.getCalendarViewMode()
: defines the way the event is opened after being clicked in the calendar view.modal
: should be used for modal based viewsredirect
: is the default and should be used for a detail view opened as full page
getFullCalendarOptions()
: see Fullcalendar options
In case you want to skip the default drag/drop and resize update mechanism and implement a own one, you can
set a updateUrl
option within getFullCalendarOptions()
.
In case you need to refresh the whole calendar view after drag/drop and resize you can set the refreshAfterUpdate
option within getFullCalendarOptions()
. This is may required if updating your event affects other events as well.
Example:
class CustomCalendarEvent extends CustomEvent implements FullCalendarEventIF {
// Implementation of other CalendarEventIF functions...
public function isUpdatable()
{
return $this->content->canEdit();
}
public function updateTime(DateTime $start, DateTime $end)
{
$this->start_datetime = CalendarUtils::toDBDateFormat($start);
$this->end_datetime = CalendarUtils::toDBDateFormat($end);
return $this->save();
}
public function getCalendarViewUrl()
{
return $this->conent->container->createUrl('/mymodule/calendar/view-modal', ['id' => $this-id]);
}
public function getCalendarViewMode()
{
return static::VIEW_MODE_MODAL;
}
public function getFullCalendarOptions()
{
return [
'rendering' => 'background';
]
}
}
A recurrent event consist of a root event and multiple recurrent instances. The root event serves as template for all recurrent instances and is not part of a calendar query result itself. The first instance of a recurring event has the same start/end date as the root event unless it has been updated.
The recurrent event interface provided by the calendar module supports:
- The creation of
rrules
by means of thehumhub\modules\calendar\interfaces\recurrence\RecurrenceFormModel
andhumhub\modules\calendar\interfaces\recurrence\widgets\RecurrenceFormWidget
- The filtering and expansion of recurring events by means of
humhub\modules\calendar\interfaces\recurrenc\AbstractRecurrenceQuery
- Editing of recurrent events either by
- Editing all events
- Splitting an recurrent event into two seperate recurrent events
- Edit single instances which serve as exceptional events
- Deleting recurrence instances with automatic
exdate
management - Deleting a recurrence root with all recurrence instances
The humhub\modules\calendar\interfaces\recurrence\RecurrentEventIF
can be used in order to support recurrent events.
Your recurrent event model needs to support the following fields:
id
the event idsequnece
the event idstart_datetime
as datetime field defining the start of the eventend_datetime
as datetime field defining the start of the eventrrule
a rrule stringexdate
a string field containing comma seperated exdatesparent_event_id
the id of the recurring root event
A recurring event and instances have to follow the following urles:
rrule
is set on both recurrent instances and root events in order to detect it as recurrentparen_event_id
is not set on non recurrent events and not set on the root event itselfexdate
is only set on the root event
The interface requires the following additional functions:
getId()
: returns the id of your modelgetRecurrenceRootId()
: returns the id of the root eventgetRrule()
: returns the rrule in case the event is recurrentsetRrule()
: used to set the rrule of an eventgetRecurrenceId()
: returns the recurrence id of this eventsetRecurrenceId()
: sets the recurrence id of this eventgetExdate()
: returns the https://www.kanzaki.com/docs/ical/exdate.html string of a root eventsetExdate()
: sets the https://www.kanzaki.com/docs/ical/exdate.html string of a root eventcreateRecurrence()
: is used to create an event instance for a given start and end (without persisting it!)syncEventData()
: should copy all necessary data of a given root event into this instancegetRecurrenceQuery()
: should return an instance of yourAbstractRecurrenceQuery
delete()
: deletes a model from the database
Example
class CustomCalendarEvent extends CustomEvent implements RecurrentEventIF {
private $query;
public function init()
{
parent::init();
$this->query = new CustomCalendarEventRecurrenceQuery(['event' => $this]);
}
// Implementation of other CalendarEventIF functions...
public function getId()
{
return $this->id;
}
public function getRecurrenceRootId()
{
return $this->parent_event_id;
}
public function getRrule()
{
return $this->rrule;
}
public function setRrule($rrule)
{
$this->rrule = $rrule;
}
public function getRecurrenceId()
{
return $this->recurrence_id;
}
public function setRecurrenceId($recurrenceId)
{
$this->recurrence_id = "recurrence_id;
}
public function getExdate()
{
return $this->exdate;
}
public function setExdate($exdate)
{
$this->exdate = $exdate;
}
public function createRecurrence($start, $end)
{
$instance = new self($this->content->container, $this->content->visibility);
$instance->start_datetime = $start;
$instance->end_datetime = $end;
// Turn off notifications and wall entry creation
$instance->silentContentCreation = true;
$instance->content->stream_channel = null;
return $instance;
}
public function syncEventData($root, $original = null)
{
$this->content->created_by = $root->content->created_by;
$this->content->visibility = $root->content->visibility;
// Only align description if we did not already overwrite it for this event
if (!$original || empty($this->description) || $original->description === $this->description) {
$this->description = $root->description;
}
if (!$original || empty($this->participant_info) || $original->participant_info === $this->participant_info) {
$this->participant_info = $root->participant_info;
}
$this->title = $root->title;
$this->time_zone = $root->time_zone;
$this->all_day = $root->all_day;
}
public function getRecurrenceQuery()
{
return $this->query;
}
}
Note:
delete()
is already implemented byActiveRecord
.
In case of a recurrent event model your query class need to extend
humhub\modules\calendar\interfaces\recurrence\AbstractRecurrenceQuery
instead of the AbstractCalendarQuery
which
allows you to overwrite the following fields in addition to the fields defined in AbstractCalendarQuery
.
idField
: the field name of your recordid
field (defaultid
)rruleField
: the field name of yourrrule
field (defaultrrule
)sequenceField
: the field name of yoursequence
field (defaultsequence
)recurrenceIdField
: the field name of yourrecurrence_id
field (defaultrecurrence_id
)
When following the database field naming recommendation a recurrence query class can look like:
class CustomCalendarEventRecurrenceQuery extends AbstractRecurrenceQuery
{
public static $recordClass = CustomCalendarEvent::class;
}
The humhub\modules\calendar\interfaces\reminder\CalendarEventReminderIF
is used to support setting
reminders for an event. This interface is currently only supported by ContentActiveRecord
based events and
extends the CalendarEventIF
by:
getContentRecord()
: should return the relatedContent
instancegetReminderUserQuery()
: should return anActiveQueryUser
filtering users to receive the reminder
class CustomCalendarEvent extends CustomEvent implements CalendarEventReminderIF {
// Implementation of other CalendarEventIF functions...
public function getContentRecord()
{
return $this->content;
}
public function getReminderUserQuery() {
return $this->findParticipantUsers();
}
}
When implementing an optional dependency to the calendar module as in the previous examples your base
model CustomEvent
needs to implement a getCalendarEvent()
function which returns a CustomCalendarEvent
.
This is required to determine a CalendarEventIF
from a given Content
instance.
public function getCalendarEvent()
{
return new CustomCalendarEvent($this->attributes);
}