recurring_events-2.0.x-dev/recurring_events.module
recurring_events.module
<?php
/**
* @file
* Contains recurring_events.module.
*/
declare(strict_types=1);
use Drupal\Component\Utility\Html;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\recurring_events\Entity\EventSeries;
use Drupal\recurring_events\EventInterface;
use Drupal\recurring_events\Plugin\ComputedField\EventInstances;
/**
* Implements hook_help().
*/
function recurring_events_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the recurring_events_views module.
case 'help.page.recurring_events':
$text = file_get_contents(__DIR__ . '/README.md');
if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
return '<pre>' . Html::escape($text) . '</pre>';
}
else {
// Use the Markdown filter to render the README.
$filter_manager = \Drupal::service('plugin.manager.filter');
$settings = \Drupal::configFactory()->get('markdown.settings')->getRawData();
$config = ['settings' => $settings];
$filter = $filter_manager->createInstance('markdown', $config);
return $filter->process($text, 'en');
}
break;
}
}
/**
* Implements hook_entity_operation().
*/
function recurring_events_entity_operation(EntityInterface $entity) {
$operations = [];
if ($entity->getEntityTypeId() == 'eventseries' || $entity->getEntityTypeId() == 'eventinstance') {
if ($entity->access('clone')) {
$operations['clone'] = [
'title' => t('Clone'),
'weight' => 50,
'url' => $entity->toUrl('clone-form'),
];
}
}
if ($entity->getEntityTypeId() == 'eventseries') {
if ($entity->access('update')) {
$operations['add_instance'] = [
'title' => t('Add Instance'),
'weight' => 50,
'url' => $entity->toUrl('add-instance-form'),
];
}
}
return $operations;
}
/**
* Implements hook_theme().
*/
function recurring_events_theme() {
$theme = [];
$theme['eventinstance'] = [
'render element' => 'elements',
'template' => 'eventinstance',
];
$theme['eventseries'] = [
'render element' => 'elements',
'template' => 'eventseries',
];
$theme['eventseries_add_list'] = [
'variables' => ['content' => NULL],
];
return $theme;
}
/**
* Implements template_preprocess_entity().
*/
function template_preprocess_eventinstance(array &$variables) {
// Set the eventinstance object to be accessible in the template.
$variables['eventinstance'] = $variables['elements']['#eventinstance'];
// Set a class on the eventinstance to differentiate between view modes.
$variables['view_mode'] = $variables['elements']['#view_mode'];
$variables['attributes']['class'][] = 'eventinstance-' . $variables['view_mode'];
// Allow field groups to be rendered too.
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Implements template_preprocess_entity().
*/
function template_preprocess_eventseries(array &$variables) {
// Set the eventseries object to be accessible in the template.
$variables['eventseries'] = $variables['elements']['#eventseries'];
// Set a class on the eventseries to differentiate between view modes.
$variables['view_mode'] = $variables['elements']['#view_mode'];
$variables['attributes']['class'][] = 'eventseries-' . $variables['view_mode'];
// Allow field groups to be rendered too.
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Prepares variables for list of available eventseries type templates.
*
* Default template: eventseries-add-list.html.twig.
*
* @param array $variables
* An associative array containing:
* - content: An array of eventseries types.
*/
function template_preprocess_eventseries_add_list(array &$variables) {
$variables['types'] = [];
if (!empty($variables['content'])) {
foreach ($variables['content'] as $type) {
$variables['types'][$type->id()] = [
'type' => $type->id(),
'add_link' => Link::fromTextAndUrl($type->label(), new Url('entity.eventseries.add_form', ['eventseries_type' => $type->id()])),
'description' => [
'#markup' => $type->getDescription(),
],
];
}
}
}
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function recurring_events_eventseries_insert(EntityInterface $entity) {
if (\Drupal::isConfigSyncing()) {
return;
}
/** @var \Drupal\recurring_events\EventCreationService $creation_service */
$creation_service = \Drupal::service('recurring_events.event_creation_service');
$instances = $creation_service->createInstances($entity);
foreach ($instances as $instance) {
$instance->set('eventseries_id', $entity->id());
$instance->setNewRevision(FALSE);
$creation_service->configureDefaultInheritances($instance, (int) $entity->id());
$creation_service->updateInstanceStatus($instance, $entity);
$instance->save();
}
}
/**
* Implements hook_ENTITY_TYPE_translation_insert().
*/
function recurring_events_eventseries_translation_insert(EntityInterface $translation) {
if (\Drupal::isConfigSyncing()) {
return;
}
/** @var \Drupal\recurring_events\Entity\EventInstance[] $instances */
$instances = $translation->event_instances->referencedEntities();
if (!empty($instances)) {
/** @var \Drupal\recurring_events\EventCreationService $creation_service */
$creation_service = \Drupal::service('recurring_events.event_creation_service');
$translation_langcode = $translation->language()->getId();
foreach ($instances as $instance) {
if ($instance->hasTranslation($translation_langcode)) {
$instance = $instance->getTranslation($translation_langcode);
}
else {
$instance = $instance->addTranslation($translation_langcode);
}
$instance->set('eventseries_id', $translation->id());
$instance->setNewRevision(FALSE);
$creation_service->configureDefaultInheritances($instance, (int) $translation->id());
$creation_service->updateInstanceStatus($instance, $translation);
$instance->save();
}
}
}
/**
* Implements hook_ENTITY_TYPE_update().
*/
function recurring_events_eventseries_update(EntityInterface $entity) {
$original = $entity->original;
$moderated = $entity->hasField('moderation_state');
/** @var \Drupal\recurring_events\EventCreationService $creation_service */
$creation_service = \Drupal::service('recurring_events.event_creation_service');
$date_changes = $creation_service->checkForOriginalRecurConfigChanges($entity, $original);
// If the eventseries is being published then
// there may be date recurrence changes that need to be converted into new
// eventinstance entities.
if ($date_changes) {
if ($entity->isPublished() || !$moderated) {
if ($entity->isDefaultTranslation()) {
$plugin_manager = \Drupal::service('plugin.manager.event_instance_creator');
$config = \Drupal::config('recurring_events.eventseries.config');
$active_plugin = $plugin_manager->createInstance($config->get('creator_plugin'), []);
\Drupal::moduleHandler()->alter('recurring_events_event_instance_creator_plugin', $active_plugin, $plugin_manager, $entity);
$active_plugin->processInstances($entity);
}
}
// Get a fresh version of the series to get the updated instances.
$storage = \Drupal::entityTypeManager()->getStorage('eventseries');
$storage->resetCache([$entity->id()]);
$entity = $entity->load($entity->id());
}
$instances = $entity->event_instances->referencedEntities();
$updated_statuses = $skipped_statuses = 0;
if (!empty($instances)) {
foreach ($instances as $instance) {
$status_updated = $creation_service->updateInstanceStatus($instance, $entity);
if ($status_updated) {
$updated_statuses++;
$instance->save();
}
else {
$skipped_statuses++;
}
}
\Drupal::messenger()->addMessage(t('Successfully updated @success instance statuses. Skipped @skipped instances due to status or workflow mismatch with series.', [
'@success' => $updated_statuses,
'@skipped' => $skipped_statuses,
]));
}
}
/**
* Implements hook_ENTITY_TYPE_insert().
*/
function recurring_events_eventseries_type_insert(EntityInterface $entity) {
if (\Drupal::isConfigSyncing()) {
return;
}
// Event series types are tied to event instance types, and optionally
// registrant types. Therefore, we need to create equivalent instance and
// registrant types every time an event series type is created.
$series_type_label = $entity->label();
$series_type_description = $entity->getDescription();
$series_type_id = $entity->id();
$instance_types = \Drupal::entityTypeManager()->getStorage('eventinstance_type')->load($series_type_id);
if (empty($instance_types)) {
$instance_type = \Drupal::entityTypeManager()->getStorage('eventinstance_type')->create([
'id' => $series_type_id,
'label' => $series_type_label,
'description' => $series_type_description,
]);
$instance_type->save();
$instance_title = \Drupal::entityTypeManager()->getStorage('field_inheritance')->create([
'id' => 'title',
'label' => 'Title',
'type' => 'inherit',
'sourceEntityType' => 'eventseries',
'sourceEntityBundle' => $series_type_id,
'sourceField' => 'title',
'destinationEntityType' => 'eventinstance',
'destinationEntityBundle' => $series_type_id,
'plugin' => 'default_inheritance',
]);
$instance_title->save();
$instance_description = \Drupal::entityTypeManager()->getStorage('field_inheritance')->create([
'id' => 'description',
'label' => 'Description',
'type' => 'append',
'sourceEntityType' => 'eventseries',
'sourceEntityBundle' => $series_type_id,
'sourceField' => 'body',
'destinationEntityType' => 'eventinstance',
'destinationEntityBundle' => $series_type_id,
'destinationField' => 'body',
'plugin' => 'default_inheritance',
]);
$instance_description->save();
}
// Ensure that field_inheritance is enabled for eventinstance.
$config = \Drupal::configFactory()->getEditable('field_inheritance.config');
$data = $config->getRawData();
if (!in_array('eventinstance', $data['included_entities'])) {
$data['included_entities'][] = 'eventinstance';
$config->setData($data)->save();
}
if (\Drupal::moduleHandler()->moduleExists('recurring_events_registration')) {
$registrant_types = \Drupal::entityTypeManager()->getStorage('registrant_type')->load($series_type_id);
if (empty($registrant_types)) {
$registrant_type = \Drupal::entityTypeManager()->getStorage('registrant_type')->create([
'id' => $series_type_id,
'label' => $series_type_label,
'description' => $series_type_description,
]);
$registrant_type->save();
}
}
\Drupal::cache('menu')->deleteAll();
\Drupal::service('plugin.manager.menu.link')->rebuild();
}
/**
* Implements hook_ENTITY_TYPE_predelete().
*/
function recurring_events_eventseries_predelete(EntityInterface $entity) {
// Only delete instances if we're deleting the default translation of the
// series.
if ($entity->isDefaultTranslation()) {
$instances = $entity->event_instances->referencedEntities();
// Allow other modules to react prior to deleting all instances after a
// date configuration change.
\Drupal::moduleHandler()->invokeAll('recurring_events_pre_delete_instances', [$entity]);
// Loop through all instances and remove them.
foreach ($instances as $instance) {
$instance->delete();
}
// Allow other modules to react after deleting all instances after a date
// configuration change.
\Drupal::moduleHandler()->invokeAll('recurring_events_post_delete_instances', [$entity]);
}
}
/**
* Implements hook_ENTITY_TYPE_delete().
*/
function recurring_events_eventseries_type_delete(EntityInterface $entity) {
// Event series types are tied to event instance types, and optionally
// registrant types. Therefore, we need to delete equivalent instance and
// registrant types every time an event series type is deleted. We also must
// delete all inheritances that use these types as either the source or the
// destination.
$query = \Drupal::entityQuery('field_inheritance');
$and_destination = $query->andConditionGroup()
->condition('destinationEntityType', 'eventseries')
->condition('destinationEntityBundle', $entity->id());
$and_source = $query->andConditionGroup()
->condition('sourceEntityType', 'eventseries')
->condition('sourceEntityBundle', $entity->id());
$or = $query->orConditionGroup()
->condition($and_destination)
->condition($and_source);
$query->condition($or);
$inherited_field_ids = $query->accessCheck(FALSE)->execute();
if (!empty($inherited_field_ids)) {
$inherited_fields = \Drupal::entityTypeManager()->getStorage('field_inheritance')->loadMultiple($inherited_field_ids);
foreach ($inherited_fields as $field) {
$field->delete();
}
}
$instance_type = \Drupal::entityTypeManager()->getStorage('eventinstance_type')->load($entity->id());
if (!empty($instance_type)) {
$query = \Drupal::entityQuery('field_inheritance');
$and_destination = $query->andConditionGroup()
->condition('destinationEntityType', 'eventinstance')
->condition('destinationEntityBundle', $instance_type->id());
$and_source = $query->andConditionGroup()
->condition('sourceEntityType', 'eventinstance')
->condition('sourceEntityBundle', $instance_type->id());
$or = $query->orConditionGroup()
->condition($and_destination)
->condition($and_source);
$query->condition($or);
$inherited_field_ids = $query->accessCheck(FALSE)->execute();
if (!empty($inherited_field_ids)) {
$inherited_fields = \Drupal::entityTypeManager()->getStorage('field_inheritance')->loadMultiple($inherited_field_ids);
foreach ($inherited_fields as $field) {
$field->delete();
}
}
$instance_type->delete();
}
if (\Drupal::moduleHandler()->moduleExists('recurring_events_registration')) {
$registrant_type = \Drupal::entityTypeManager()->getStorage('registrant_type')->load($entity->id());
if (!empty($registrant_type)) {
$query = \Drupal::entityQuery('field_inheritance');
$and_destination = $query->andConditionGroup()
->condition('destinationEntityType', 'registrant')
->condition('destinationEntityBundle', $registrant_type->id());
$and_source = $query->andConditionGroup()
->condition('sourceEntityType', 'registrant')
->condition('sourceEntityBundle', $registrant_type->id());
$or = $query->orConditionGroup()
->condition($and_destination)
->condition($and_source);
$query->condition($or);
$inherited_field_ids = $query->accessCheck(FALSE)->execute();
if (!empty($inherited_field_ids)) {
$inherited_fields = \Drupal::entityTypeManager()->getStorage('field_inheritance')->loadMultiple($inherited_field_ids);
foreach ($inherited_fields as $field) {
$field->delete();
}
}
$registrant_type->delete();
}
}
}
/**
* Implements hook_entity_operation_alter().
*/
function recurring_events_entity_operation_alter(array &$operations, EntityInterface $entity) {
if ($entity->getEntityTypeId() == 'eventinstance_type') {
if (!empty($operations['delete'])) {
unset($operations['delete']);
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function recurring_events_form_content_moderation_entity_moderation_form_alter(&$form, FormStateInterface $form_state) {
$entity = $form_state->get('entity');
if ($entity instanceof EventInterface && $entity->getEntityTypeId() === 'eventseries') {
$original = \Drupal::entityTypeManager()->getStorage('eventseries')->load($entity->id());
$creation_service = \Drupal::service('recurring_events.event_creation_service');
if ($creation_service->checkForOriginalRecurConfigChanges($entity, $original)) {
// Show a table when viewing a revision displaying any date recurrence
// differences to alert users before they publish the revision.
$diff_array = $creation_service->buildDiffArray($original, NULL, $entity);
if (!empty($diff_array)) {
$form['diff'] = [
'#type' => 'container',
'#weight' => -99,
];
$form['diff']['diff_title'] = [
'#type' => '#markup',
'#prefix' => '<h2>',
'#markup' => t('Revision Date Changes'),
'#suffix' => '</h2>',
];
$form['diff']['diff_message'] = [
'#type' => '#markup',
'#prefix' => '<p>',
'#markup' => t('Recurrence configuration has been changed in this revision, as a result if you choose to publish this revision all instances will be removed and recreated. This action cannot be undone.'),
'#suffix' => '</p>',
];
$form['diff']['table'] = [
'#type' => 'table',
'#header' => [
t('Data'),
t('Stored'),
t('Overridden'),
],
'#rows' => $diff_array,
];
}
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for field_inheritance_settings.
*/
function recurring_events_form_field_inheritance_settings_alter(&$form, FormStateInterface $form_state): void {
// Do not allow to disable support for eventinstances. That would break the
// module behavior.
$form['included_entities']['eventinstance']['#disabled'] = TRUE;
}
/**
* Implements hook_recurring_events_event_instances_pre_create_alter().
*/
function recurring_events_recurring_events_event_instances_pre_create_alter(array &$event_instances, EventSeries $event) {
$config = \Drupal::config('recurring_events.eventseries.config');
$messenger = \Drupal::messenger();
$global_exclude = $global_include = [];
$event_exclude = $event_include = [];
$exclude_config = \Drupal::entityTypeManager()->getStorage('excluded_dates')->loadMultiple();
$include_config = \Drupal::entityTypeManager()->getStorage('included_dates')->loadMultiple();
if (!empty($exclude_config)) {
foreach ($exclude_config as $date_config) {
$global_exclude[] = [
'value' => $date_config->start(),
'end_value' => $date_config->end(),
];
}
}
if (!empty($include_config)) {
foreach ($include_config as $date_config) {
$global_include[] = [
'value' => $date_config->start(),
'end_value' => $date_config->end(),
];
}
}
if ($config->get('excludes')) {
if (!empty($event->excluded_dates)) {
$event_exclude = $event->excluded_dates->getValue();
}
}
if ($config->get('includes')) {
if (!empty($event->included_dates)) {
$event_include = $event->included_dates->getValue();
}
}
$exclude = array_merge($global_exclude, $event_exclude);
$include = array_merge($global_include, $event_include);
if (!empty($exclude)) {
foreach ($event_instances as $key => $dates) {
$start = $dates['start_date']->getTimestamp();
$end = $dates['end_date']->getTimestamp();
foreach ($exclude as $date) {
$exclude_start = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $date['value'] . 'T00:00:00');
$exclude_start = $exclude_start->getTimestamp();
$excluded_end = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $date['end_value'] . 'T23:59:59');
$excluded_end = $excluded_end->getTimestamp();
if ($start >= $exclude_start && $start <= $excluded_end) {
$messenger->addMessage(t('Skipping excluded date: @start_date - @end_date', [
'@start_date' => $dates['start_date']->format($config->get('date_format')),
'@end_date' => $dates['end_date']->format($config->get('date_format')),
]));
unset($event_instances[$key]);
break;
}
if ($end >= $exclude_start && $end <= $excluded_end) {
$messenger->addMessage(t('Skipping excluded date: @start_date - @end_date', [
'@start_date' => $dates['start_date']->format($config->get('date_format')),
'@end_date' => $dates['end_date']->format($config->get('date_format')),
]));
unset($event_instances[$key]);
break;
}
}
}
}
if (!empty($include)) {
foreach ($event_instances as $key => $dates) {
$include_event = FALSE;
$start = $dates['start_date']->getTimestamp();
$end = $dates['end_date']->getTimestamp();
for ($x = 0; $x <= (count($include) - 1); $x++) {
$included_start = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $include[$x]['value'] . 'T00:00:00');
$included_start = $included_start->getTimestamp();
$included_end = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $include[$x]['end_value'] . 'T23:59:59');
$included_end = $included_end->getTimestamp();
if ($start >= $included_start && $start <= $included_end && $end >= $included_start && $end <= $included_end) {
$include_event = TRUE;
// This event is in the inclusion range, so move on to the next one.
break;
}
}
if (!$include_event) {
$messenger->addMessage(t('Skipping non-included date: @start_date - @end_date', [
'@start_date' => $dates['start_date']->format($config->get('date_format')),
'@end_date' => $dates['end_date']->format($config->get('date_format')),
]));
unset($event_instances[$key]);
}
}
}
}
/**
* Implements callback_allowed_values_function().
*/
function recurring_events_allowed_values_function(FieldStorageDefinitionInterface $definition, ?FieldableEntityInterface $entity = NULL) {
$values = ['custom' => t('Custom/Single Event')];
$fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('eventseries');
foreach ($fields as $field) {
$field_definition = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field->getType());
$class = new \ReflectionClass($field_definition['class']);
if ($class->implementsInterface('\Drupal\recurring_events\RecurringEventsFieldTypeInterface')) {
$values[$field->getName()] = $field->getLabel();
}
}
return $values;
}
/**
* Implements hook_recurring_events_recur_field_types_alter().
*/
function recurring_events_recurring_events_recur_field_types_alter(&$fields) {
// Enable/disable the recur field types based on the eventseries settings.
$config = \Drupal::config('recurring_events.eventseries.config');
$enabled_fields = explode(',', $config->get('enabled_fields'));
foreach ($fields as $field_name => $field_label) {
if (array_search($field_name, $enabled_fields) === FALSE) {
unset($fields[$field_name]);
}
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function recurring_events_theme_suggestions_eventseries(array $variables) {
$suggestions = [];
$entity = $variables['elements']['#eventseries'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'eventseries__' . $sanitized_view_mode;
$suggestions[] = 'eventseries__' . $entity->bundle();
$suggestions[] = 'eventseries__' . $entity->bundle() . '__' . $sanitized_view_mode;
$suggestions[] = 'eventseries__' . $entity->id();
$suggestions[] = 'eventseries__' . $entity->id() . '__' . $sanitized_view_mode;
return $suggestions;
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function recurring_events_theme_suggestions_eventinstance(array $variables) {
$suggestions = [];
$entity = $variables['elements']['#eventinstance'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'eventinstance__' . $sanitized_view_mode;
$suggestions[] = 'eventinstance__' . $entity->bundle();
$suggestions[] = 'eventinstance__' . $entity->bundle() . '__' . $sanitized_view_mode;
$suggestions[] = 'eventinstance__' . $entity->id();
$suggestions[] = 'eventinstance__' . $entity->id() . '__' . $sanitized_view_mode;
return $suggestions;
}
/**
* Implements hook_entity_base_field_info_alter().
*/
function recurring_events_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'eventseries') {
$fields['event_instances'] = BaseFieldDefinition::create('entity_reference')
->setName('event_instances')
->setLabel(t('Event Instances'))
->setDescription('The event instances for this event.')
->setTargetEntityTypeId('eventseries')
->setReadOnly(TRUE)
->setComputed(TRUE)
->setSetting('target_type', 'eventinstance')
->setDisplayConfigurable('view', TRUE)
->setDisplayOptions('view', [
'label' => 'above',
'weight' => 0,
])
->setDisplayConfigurable('form', FALSE)
->setTranslatable(TRUE)
->setClass(EventInstances::class)
->setProvider('recurring_events');
}
}
