# 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:**

```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**:

```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:**

```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](https://www.kanzaki.com/docs/ical/uid.html) 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**:

```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](https://www.kanzaki.com/docs/ical/sequence.html) 
 - `getLocation()`: (optional) an event location string
 - `getBadge()`: (optional) a `humhub\widgets\bootstrap\Badge` 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**:

```php
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:**

```php
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**:

```php
return [
    'events' => [
        ['class' => 'humhub\modules\calendar\interfaces\CalendarService', 'event' => 'findItems', 'callback' => ['mymodule\Events', 'onFindCalendarItems']],
    ],
];
``` 

**Event.php:**

```php
public static function onFindCalendarItems(CalendarItemsEvent $event)
{
    $contentContainer = $event->contentContainer;

    if(!$contentContainer || $contentContainer->isModuleEnabled('mymodule')) {
        $event->addItems(static::ITEM_TYPE_KEY, CustomCalendarEventQuery::findForEvent($event));
    }
}
```

### CalDAV Object Events

The Calendar module triggers three additional events to handle **single event operations** for CalDAV synchronization.  
These events allow external modules to respond when a CalDAV client **requests, updates, or deletes** a single calendar object.

Modules can listen to these events using `SyncService` in their module configuration.

**config.php:**

```php
return [
    'events' => [
        ['class' => \humhub\modules\calendar\helpers\dav\SyncService::class, 'event' => 'event_get_object', 'callback' => [\humhub\modules\external_calendar\Events::class, 'onCaldavGetObject']],
        ['class' => \humhub\modules\calendar\helpers\dav\SyncService::class, 'event' => 'event_update_object', 'callback' => [\humhub\modules\external_calendar\Events::class, 'onCaldavUpdateObject']],
        ['class' => \humhub\modules\calendar\helpers\dav\SyncService::class, 'event' => 'event_delete_object', 'callback' => [\humhub\modules\external_calendar\Events::class, 'onCaldavDeleteObject']],
    ],
];
```

**Event.php:**

- `event_get_object`

Triggered when a CalDAV client requests a single calendar object (e.g., <uid>.ics).
Used to retrieve the event by its UID for CalDAV synchronization.

```php
public static function onCaldavGetObject(GetObjectEvent $event)
{
    $event->object = CustomCalendarEvent::findOne(['uid' => $event->objectId]);
}
```

- `event_update_object`

Triggered when a CalDAV client updates a single event.
Used to  modify an event based on the properties sent by the CalDAV client.

```php
public static function onCaldavUpdateObject(UpdateObjectEvent $event)
{
    if (!$event->object) {
        $event->object = new CustomCalendarEvent();
    }
    $event->object->applyProperties($event->properties);
    $event->object->save();
}
```

- `event_delete_object`

Triggered when a CalDAV client deletes a single event object.
Used to remove an event from the system when requested by a CalDAV client.

```php
public static function onCaldavDeleteObject(DeleteObjectEvent $event)
{
    if ($event->object) {
        $event->object->delete();
    }
}
```

### 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](https://fullcalendar.io/docs/event-object)

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](https://fullcalendar.io/docs/event-model)

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](https://www.kanzaki.com/docs/ical/rrule.html) string
- `exdate` a string field containing comma seperated [exdates](https://www.kanzaki.com/docs/ical/exdate.html)
- `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](https://www.kanzaki.com/docs/ical/rrule.html) in case the event is recurrent
- `setRrule()`: used to set the [rrule](https://www.kanzaki.com/docs/ical/rrule.html) of an event
- `getRecurrenceId()`: returns the [recurrence id](https://www.kanzaki.com/docs/ical/recurrenceId.html)  of this event
- `setRecurrenceId()`: sets the [recurrence id](https://www.kanzaki.com/docs/ical/recurrenceId.html) of this event
- `getExdate()`: returns the  [https://www.kanzaki.com/docs/ical/exdate.html](https://www.kanzaki.com/docs/ical/exdate.html) string of a root event
- `setExdate()`: sets the  [https://www.kanzaki.com/docs/ical/exdate.html](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);
}
```
