Skip to content

Latest commit

 

History

History
579 lines (436 loc) · 22 KB

interface.md

File metadata and controls

579 lines (436 loc) · 22 KB

Calendar interface v1.0

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.

Calendar item types

A calendar item type is used to provide some meta data of your custom event type as for example:

  • title: A translatable title
  • description: Short translatable description of your item type
  • default color (optional): A default color used for this item type, which can be overwritten in the calendar module config
  • icon (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 no contentContainer is given it's meant to be a global search for all available calendar item types.

Calendar Events Model

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 case EditableEventIF is implemented.
  • start_datetime: the start date time
  • end_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 your integration/calendar directory.

CalendarEventIF implementation

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 original CustomEvent class in the content table instead of CustomCalendarEvent.

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 implementing EditableEventIF, this uid will be assigned automatically if no custom uid was assigned.
  • getType(): returns an instance of the related calendar type
  • isAllDay(): weather or not this event is an all day event
  • getStartDateTime(): start DateTime object of this event
  • getEndDateTime(): end DateTime object of this event
  • getTimezone(): string the timezone string of this event
  • getEndTimezone(): can be used in case the end date timezone differs from the start timezone, otherwise can be null
  • getUrl(): An url to the detail-view of this event, this will be used in the upcoming event snippet and in the calendar view
  • getTitle(): The event title
  • getDescription(): The event description
  • getLastModified(): 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 used
  • getSequence(): (optional) The event revision sequence
  • getLocation(): (optional) an event location string
  • getBadge(): (optional) a humhub\widgets\Label or string used in the sidebar snippet
  • getCalendarOptions(): (optional) additional event configuration

Optional functions can return null if not supported.

AbstractCalendarQuery implementation

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 is Y-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 is Y-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 an all_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 your ActiveRecord class used for the query
  • allDayField: Defines the database field name of your models all day flag. This is only required when using DATE_QUERY_TYPE_MIXED (default is all_day)
  • startField: The name of your start date field (default start_datetime)
  • endField: The name of your end date field (default end_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 the USER_RELATED_SCOPE_SPACE and USER_RELATED_SCOPE_OWN_PROFILE
  • filterGuests(): used for guest users which are not able to use other filters
  • filterUserRelated(): used for user related queries e.g: 'Only content from following spaces' (see ActiveQueryContent::userRelated)
  • filterContentContainer(): used to filter content of a specific ContentContainer (Space/User)
  • filterReadable(): only include content readable by the current user
  • filterMine(): only include items created by me
  • setupDateCriteria(): 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');
    }
}

Inject calendar entries

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));
    }
}

Implementation of EditableEventIF

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 done
  • save(): 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);
  }
}

Implementation of FullCalendarEventIF

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 by getUrl(), usually used to provide a modal view instead of a detail view. If null is returned the getUrl() is used and redirect 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 views
    • redirect: 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';
    ]
 }
}

Recurrent events

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 the humhub\modules\calendar\interfaces\recurrence\RecurrenceFormModel and humhub\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

Implementation of RecurrentEventIF

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 id
  • sequnece the event id
  • start_datetime as datetime field defining the start of the event
  • end_datetime as datetime field defining the start of the event
  • rrule a rrule string
  • exdate a string field containing comma seperated exdates
  • parent_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 recurrent
  • paren_event_id is not set on non recurrent events and not set on the root event itself
  • exdate is only set on the root event

The interface requires the following additional functions:

  • getId(): returns the id of your model
  • getRecurrenceRootId(): returns the id of the root event
  • getRrule(): returns the rrule in case the event is recurrent
  • setRrule(): used to set the rrule of an event
  • getRecurrenceId(): returns the recurrence id of this event
  • setRecurrenceId(): sets the recurrence id of this event
  • getExdate(): returns the https://www.kanzaki.com/docs/ical/exdate.html string of a root event
  • setExdate(): sets the https://www.kanzaki.com/docs/ical/exdate.html string of a root event
  • createRecurrence(): 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 instance
  • getRecurrenceQuery(): should return an instance of your AbstractRecurrenceQuery
  • 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 by ActiveRecord.

Implementation of AbstractRecurrenceQuery

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 record id field (default id)
  • rruleField: the field name of your rrule field (default rrule)
  • sequenceField: the field name of your sequence field (default sequence)
  • recurrenceIdField: the field name of your recurrence_id field (default recurrence_id)

When following the database field naming recommendation a recurrence query class can look like:

class CustomCalendarEventRecurrenceQuery extends AbstractRecurrenceQuery
{
    public static $recordClass = CustomCalendarEvent::class;
}

CalendarEventReminderIF

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 related Content instance
  • getReminderUserQuery(): should return an ActiveQueryUser 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);
}