Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/recurring_events
  • issue/recurring_events-3183502
  • issue/recurring_events-3183463
  • issue/recurring_events-3183483
  • issue/recurring_events-3190526
  • issue/recurring_events-3191715
  • issue/recurring_events-3190833
  • issue/recurring_events-3188808
  • issue/recurring_events-3180479
  • issue/recurring_events-3122823
  • issue/recurring_events-3196649
  • issue/recurring_events-3196428
  • issue/recurring_events-3196702
  • issue/recurring_events-3196704
  • issue/recurring_events-3198532
  • issue/recurring_events-3164409
  • issue/recurring_events-3206960
  • issue/recurring_events-3115678
  • issue/recurring_events-3218496
  • issue/recurring_events-3207435
  • issue/recurring_events-3219082
  • issue/recurring_events-3217367
  • issue/recurring_events-3229514
  • issue/recurring_events-3231841
  • issue/recurring_events-3238591
  • issue/recurring_events-3282502
  • issue/recurring_events-3283128
  • issue/recurring_events-3240862
  • issue/recurring_events-3247034
  • issue/recurring_events-3071679
  • issue/recurring_events-3264621
  • issue/recurring_events-3266436
  • issue/recurring_events-3268690
  • issue/recurring_events-3269555
  • issue/recurring_events-3271328
  • issue/recurring_events-3272361
  • issue/recurring_events-3163804
  • issue/recurring_events-3297681
  • issue/recurring_events-3299575
  • issue/recurring_events-3300786
  • issue/recurring_events-3302916
  • issue/recurring_events-3304286
  • issue/recurring_events-3298679
  • issue/recurring_events-3309652
  • issue/recurring_events-3310360
  • issue/recurring_events-3311843
  • issue/recurring_events-3311712
  • issue/recurring_events-3312003
  • issue/recurring_events-3312084
  • issue/recurring_events-3312242
  • issue/recurring_events-3316080
  • issue/recurring_events-3295367
  • issue/recurring_events-3196417
  • issue/recurring_events-3309859
  • issue/recurring_events-3318590
  • issue/recurring_events-3244975
  • issue/recurring_events-3318998
  • issue/recurring_events-3321269
  • issue/recurring_events-3320512
  • issue/recurring_events-3321235
  • issue/recurring_events-3321550
  • issue/recurring_events-3322998
  • issue/recurring_events-3315836
  • issue/recurring_events-3324055
  • issue/recurring_events-3328907
  • issue/recurring_events-3318490
  • issue/recurring_events-3339288
  • issue/recurring_events-3345618
  • issue/recurring_events-3347935
  • issue/recurring_events-3362297
  • issue/recurring_events-3359696
  • issue/recurring_events-3318666
  • issue/recurring_events-3366907
  • issue/recurring_events-3366910
  • issue/recurring_events-3403064
  • issue/recurring_events-3404311
  • issue/recurring_events-3405567
  • issue/recurring_events-3376639
  • issue/recurring_events-3384836
  • issue/recurring_events-3382387
  • issue/recurring_events-3384389
  • issue/recurring_events-3315503
  • issue/recurring_events-3411229
  • issue/recurring_events-3415222
  • issue/recurring_events-3415308
  • issue/recurring_events-3172514
  • issue/recurring_events-3419694
  • issue/recurring_events-3178696
  • issue/recurring_events-3408924
  • issue/recurring_events-3447130
  • issue/recurring_events-3416436
  • issue/recurring_events-3451613
  • issue/recurring_events-3452632
  • issue/recurring_events-3453086
  • issue/recurring_events-3452641
  • issue/recurring_events-3454012
  • issue/recurring_events-3455716
  • issue/recurring_events-3456300
  • issue/recurring_events-3456641
  • issue/recurring_events-3462327
  • issue/recurring_events-3463467
  • issue/recurring_events-3463979
  • issue/recurring_events-3462480
  • issue/recurring_events-3464792
  • issue/recurring_events-3456045
  • issue/recurring_events-3468300
  • issue/recurring_events-3468521
  • issue/recurring_events-3475611
  • issue/recurring_events-3477247
  • issue/recurring_events-3477047
  • issue/recurring_events-3477650
  • issue/recurring_events-3257502
  • issue/recurring_events-3090186
  • issue/recurring_events-3478802
  • issue/recurring_events-3479449
  • issue/recurring_events-3479843
  • issue/recurring_events-3479860
  • issue/recurring_events-3480495
  • issue/recurring_events-3480500
  • issue/recurring_events-3480746
  • issue/recurring_events-3480973
  • issue/recurring_events-3481021
  • issue/recurring_events-3481722
  • issue/recurring_events-3482804
  • issue/recurring_events-3483283
  • issue/recurring_events-3484209
  • issue/recurring_events-3170156
  • issue/recurring_events-3484558
  • issue/recurring_events-3485904
  • issue/recurring_events-3485935
  • issue/recurring_events-3487412
  • issue/recurring_events-3496270
  • issue/recurring_events-3480508
  • issue/recurring_events-3499792
  • issue/recurring_events-3500920
  • issue/recurring_events-3510919
  • issue/recurring_events-3510942
  • issue/recurring_events-3478268
138 results
Show changes
Showing
with 1687 additions and 90 deletions
services:
recurring_events_ical.event_ical:
class: Drupal\recurring_events_ical\EventICal
arguments: ['@entity_type.manager', '@request_stack', '@token']
<?php
namespace Drupal\recurring_events_ical\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\recurring_events\EventInterface;
use Drupal\recurring_events_ical\EventICalInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Route controller for exporting event data in iCalendar format.
*/
class EventExportController extends ControllerBase {
/**
* The event iCal service.
*
* @var \Drupal\recurring_events_ical\EventICalInterface
*/
protected $eventICal;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var \Drupal\recurring_events_ical\EventICalInterface $eventICal */
$eventICal = $container->get('recurring_events_ical.event_ical');
return new static(
$eventICal
);
}
/**
* Constructs a new EventExportController instance.
*
* @param \Drupal\recurring_events_ical\EventICalInterface $eventICal
* The event iCal service.
*/
public function __construct(EventICalInterface $eventICal) {
$this->eventICal = $eventICal;
}
/**
* Returns an iCalendar response for an event series.
*
* @param \Drupal\recurring_events\EventInterface $eventseries
* The event series.
*
* @return \Symfony\Component\HttpFoundation\Response
* An iCalendar file download.
*/
public function series(EventInterface $eventseries): Response {
return $this->response($eventseries);
}
/**
* Returns an iCalendar response for an event instance.
*
* @param \Drupal\recurring_events\EventInterface $eventinstance
* The event instance.
*
* @return \Symfony\Component\HttpFoundation\Response
* An iCalendar file download.
*/
public function instance(EventInterface $eventinstance): Response {
return $this->response($eventinstance);
}
/**
* Returns an event's iCalendar data as an HTTP response.
*
* @param \Drupal\recurring_events\EventInterface $event
* An event.
*
* @return \Symfony\Component\HttpFoundation\Response
* An iCalendar file download.
*/
protected function response(EventInterface $event): Response {
$headers = [
'Content-Type' => 'text/calendar',
'Content-Disposition' => 'attachment; filename="event.ics"',
];
return new Response($this->eventICal->render($event), 200, $headers);
}
}
<?php
namespace Drupal\recurring_events_ical\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\recurring_events_ical\EventICalMappingInterface;
/**
* Defines an event iCal property mapping entity.
*
* @ConfigEntityType(
* id = "event_ical_mapping",
* label = @Translation("Event iCal property mapping"),
* handlers = {
* "list_builder" = "Drupal\recurring_events_ical\EventICalMappingListBuilder",
* "form" = {
* "add" = "Drupal\recurring_events_ical\Form\EventICalMappingForm",
* "edit" = "Drupal\recurring_events_ical\Form\EventICalMappingForm",
* "delete" = "Drupal\recurring_events_ical\Form\EventICalMappingDeleteForm",
* }
* },
* config_prefix = "event_ical_mapping",
* admin_permission = "administer eventinstance types",
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* links = {
* "collection" = "/admin/structure/events/ical",
* "edit-form" = "/admin/structure/events/ical/{event_ical_mapping}",
* "delete-form" = "/admin/structure/events/ical/{event_ical_mapping}/delete",
* },
* config_export = {
* "id",
* "label",
* "properties"
* }
* )
*/
class EventICalMapping extends ConfigEntityBase implements EventICalMappingInterface {
/**
* The event iCal mapping ID.
*
* @var string
*/
protected $id;
/**
* The event iCal mapping label.
*
* @var string
*/
protected $label;
/**
* The iCal property mappings.
*
* @var array
*/
protected $properties = [];
/**
* {@inheritdoc}
*/
public function hasProperty(string $property): bool {
return array_key_exists($property, $this->properties);
}
/**
* {@inheritdoc}
*/
public function getProperty(string $property): ?string {
return $this->hasProperty($property) ? $this->properties[$property] : NULL;
}
/**
* {@inheritdoc}
*/
public function getAllProperties(): array {
return $this->properties;
}
/**
* {@inheritdoc}
*/
public function setProperty(string $property, string $value) {
$this->properties[$property] = $value;
}
}
<?php
namespace Drupal\recurring_events_ical;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Utility\Token;
use Drupal\recurring_events\Entity\EventSeries;
use Drupal\recurring_events\EventInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides a service to get the iCalendar data for an event.
*/
class EventICal implements EventICalInterface {
const VERSION = '2.0';
const PRODID = '-//Drupal//recurring_events_ical//2.0//EN';
const DATETIMEFORMAT = 'Ymd\THis\Z';
const LINELENGTH = 75;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Constructs an EventICalService object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
* @param \Drupal\Core\Utility\Token $token
* The tokens service.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager, RequestStack $requestStack, Token $token) {
$this->entityTypeManager = $entityTypeManager;
$this->request = $requestStack->getCurrentRequest();
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public function render(EventInterface $event): string {
/** @var \Drupal\recurring_events_ical\Entity\EventICalMapping|null $mapping */
$mapping = $this->entityTypeManager->getStorage('event_ical_mapping')->load($event->bundle());
/** @var \Drupal\recurring_events\Entity\EventInstance[] $instances */
$instances = $event instanceof EventSeries
? $event->get('event_instances')->referencedEntities()
: [$event];
$output = [];
$output[] = 'BEGIN:VCALENDAR';
$output[] = 'VERSION:' . static::VERSION;
$output[] = 'PRODID:' . static::PRODID;
foreach ($instances as $instance) {
$output[] = 'BEGIN:VEVENT';
$output[] = $this->prepareValue('UID', $instance->uuid() . '@' . $this->request->getHost());
$output[] = 'DTSTAMP:' . date(static::DATETIMEFORMAT, $instance->getChangedTime());
$output[] = 'DTSTART:' . $instance->date->start_date->format(static::DATETIMEFORMAT);
$output[] = 'DTEND:' . $instance->date->end_date->format(static::DATETIMEFORMAT);
if ($mapping) {
foreach ($mapping->getAllProperties() as $property => $value) {
// Process any tokens, removing those that have no replacement.
$value = $this->token->replace($value, ['eventinstance' => $instance], [
'clear' => TRUE,
]);
$value = $this->prepareValue($property, $value);
if (!empty($value)) {
$output[] = $value;
}
}
}
else {
// The summary is required, so if there's no mapping in place, default
// to the event label.
$output[] = $this->prepareValue('SUMMARY', $instance->label());
}
$output[] = 'END:VEVENT';
}
$output[] = 'END:VCALENDAR';
return implode("\r\n", $output);
}
/**
* Prepares a property value on an event for output as iCalendar data.
*
* @param string $property
* The property name.
* @param string $value
* The raw value.
*
* @return string
* The property and sanitized value, formatted as required by RFC 5545.
*/
protected function prepareValue(string $property, string $value): string {
// Sanitize twice to ensure that all tags and HTML codes are removed. For
// example, if the value came in as "&lt;p&gt;Hello,&#039;World!&lt;/p&gt;",
// the first pass would return "<p>Hello,&nbsp;World!</p>" and the second
// pass would return "Hello, World!", which is what we need.
$value = trim(PlainTextOutput::renderFromHtml(PlainTextOutput::renderFromHtml($value)));
// If there's nothing left after processing, return an empty string so that
// the property is omitted.
if (empty($value)) {
return '';
}
$value = strtoupper($property) . ':' . $value;
// Change all existing line endings to literal \n.
$value = str_replace(["\r\n", "\n\r", "\r", "\n"], '\n', $value);
// RFC 5545 3.1 requires lines longer than 75 bytes to be wrapped with CRLF
// followed by a single space.
$wrapped = [];
// Remember: strlen() counts bytes, not characters.
while (strlen($value) > static::LINELENGTH) {
// Grab a chunk up to line length, without splitting multibyte characters.
$chunk = $this->cut($value, 0, static::LINELENGTH, 'UTF-8');
$wrapped[] = $chunk;
// The required space after the CRLF counts against the line length, so
// add it to the front of the remaining text for the next loop.
$value = ' ' . $this->cut($value, strlen($chunk), NULL, 'UTF-8');
}
// $value now contains whatever is left on the last line after wrapping.
$wrapped[] = $value;
return implode("\r\n", $wrapped);
}
/**
* Wrapper for mb_strcut because Symfony's Mbstring polyfill doesn't have it.
*
* @param string $string
* The string being cut.
* @param int $start
* The start position in bytes.
* @param int|null $length
* The length of the cut in bytes. If NULL, runs to the end of the string.
* @param string|null $encoding
* The character encoding.
*
* @return string
* The portion of $string specified by the start and length parameters.
*
* @see mb_strcut()
*/
protected function cut(string $string, int $start, ?int $length = NULL, ?string $encoding = NULL): string {
if (function_exists('mb_strcut')) {
return mb_strcut($string, $start, $length, $encoding);
}
return substr($string, $start, $length);
}
}
<?php
namespace Drupal\recurring_events_ical;
use Drupal\recurring_events\EventInterface;
/**
* Provides an interface for the event iCal service.
*/
interface EventICalInterface {
/**
* Renders an event in iCalendar format.
*
* @param \Drupal\recurring_events\EventInterface $event
* The event.
*
* @return string
* The event data in iCalendar format.
*/
public function render(EventInterface $event): string;
}
<?php
namespace Drupal\recurring_events_ical;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface for event iCal property mapping entities.
*/
interface EventICalMappingInterface extends ConfigEntityInterface {
/**
* Returns TRUE if a property mapping exists.
*
* @param string $property
* The iCal property.
*
* @return bool
* TRUE if a mapping exists for the property.
*/
public function hasProperty(string $property): bool;
/**
* Returns the value of a property mapping.
*
* @param string $property
* The iCal property.
*
* @return string|null
* The mapped value, or NULL if not mapped.
*/
public function getProperty(string $property): ?string;
/**
* Returns all mapped property values.
*
* @return string[]
* An array of property mappings as $property => $value.
*/
public function getAllProperties(): array;
/**
* Sets the value of a property mapping.
*
* @param string $property
* The iCal property.
* @param string $value
* The value of the property.
*/
public function setProperty(string $property, string $value);
}
<?php
namespace Drupal\recurring_events_ical;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a listing of event iCal property mapping entities.
*/
class EventICalMappingListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Event instance type');
$header['id'] = $this->t('Machine name');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
$row['id'] = $entity->id();
return $row + parent::buildRow($entity);
}
}
<?php
namespace Drupal\recurring_events_ical\Field;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;
/**
* A computed field that generates a link to download an event's iCalendar data.
*/
class EventICalLinkItemList extends FieldItemList {
use ComputedItemListTrait;
/**
* {@inheritdoc}
*/
protected function computeValue() {
if (!isset($this->list[0])) {
$entity = $this->getEntity();
if (!$entity->isNew()) {
$this->list[0] = $this->createItem(0, [
'uri' => $entity->toUrl('ical')->toUriString(),
'title' => '',
]);
}
}
}
}
<?php
namespace Drupal\recurring_events_ical\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Form handler for the event iCal property mapping delete form.
*/
class EventICalMappingDeleteForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete %name?', [
'%name' => $this->entity->label(),
]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->entity->toUrl('collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->delete();
$this->messenger()->addMessage($this->t('%label iCalendar property mapping has been deleted.', [
'%label' => $this->entity->label(),
]));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}
<?php
namespace Drupal\recurring_events_ical\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\token\TokenEntityMapperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form handler for the event iCal property mapping add/edit form.
*/
class EventICalMappingForm extends EntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\recurring_events_ical\EventICalMappingInterface
*/
protected $entity;
/**
* The entity type bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* The token entity mapper service.
*
* @var \Drupal\token\TokenEntityMapperInterface
*/
protected $tokenEntityMapper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entityTypeBundleInfo */
$entityTypeBundleInfo = $container->get('entity_type.bundle.info');
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager */
$entityTypeManager = $container->get('entity_type.manager');
/** @var \Drupal\token\TokenEntityMapperInterface $tokenEntityMapper */
$tokenEntityMapper = $container->get('token.entity_mapper');
return new static(
$entityTypeBundleInfo,
$entityTypeManager,
$tokenEntityMapper
);
}
/**
* Constructs an EventICalMappingForm object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entityTypeBundleInfo
* The entity type bundle info service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\token\TokenEntityMapperInterface $tokenEntityMapper
* The token entity mapper service.
*/
public function __construct(EntityTypeBundleInfoInterface $entityTypeBundleInfo, EntityTypeManagerInterface $entityTypeManager, TokenEntityMapperInterface $tokenEntityMapper) {
$this->entityTypeBundleInfo = $entityTypeBundleInfo;
$this->entityTypeManager = $entityTypeManager;
$this->tokenEntityMapper = $tokenEntityMapper;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$eventType = $this->entity->isNew() ? $form_state->get('event_type') : $this->entity->id();
// If this is a new mapping, show a selector for unmapped event types.
if ($this->entity->isNew()) {
$form['#ajax_wrapper_id'] = 'event-ical-mapping-form-ajax-wrapper';
$ajax = [
'wrapper' => $form['#ajax_wrapper_id'],
'callback' => '::rebuildForm',
];
$form['#prefix'] = '<div id="' . $form['#ajax_wrapper_id'] . '">';
$form['#suffix'] = '</div>';
$form['id'] = [
'#type' => 'select',
'#title' => $this->t('Event instance type'),
'#description' => $this->t('Select the type of event for which to map iCalendar properties.'),
'#options' => $this->getUnmappedTypes(),
'#default_value' => $eventType,
'#required' => TRUE,
'#ajax' => $ajax + [
'trigger_as' => [
'name' => 'select_id_submit',
],
],
];
$form['select_id_submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
'#name' => 'select_id_submit',
'#ajax' => $ajax,
'#attributes' => [
'class' => ['js-hide'],
],
];
}
// Hide the rest of the form until a type is selected.
if (!isset($eventType)) {
return $form;
}
// Show the token browser.
$tokenTypes = [
'eventinstance' => $this->tokenEntityMapper->getTokenTypeForEntityType('eventinstance'),
];
$form['token_browser'] = [
'#theme' => 'token_tree_link',
'#token_types' => $tokenTypes,
'#global_types' => TRUE,
'#show_nested' => TRUE,
];
// iCalendar properties.
$form['properties'] = ['#tree' => TRUE];
$propertyDefaults = [
'#type' => 'textfield',
'#size' => 65,
'#maxlength' => 1280,
'#element_validate' => ['token_element_validate'],
'#after_build' => ['token_element_validate'],
'#token_types' => $tokenTypes,
];
$form['properties']['summary'] = [
'#title' => $this->t('Summary'),
'#default_value' => $this->entity->getProperty('summary') ?? '[eventinstance:title]',
'#description' => $this->t('Short summary or subject for the event.'),
'#required' => TRUE,
] + $propertyDefaults;
$form['properties']['contact'] = [
'#title' => $this->t('Contact'),
'#default_value' => $this->entity->getProperty('contact'),
'#description' => $this->t('Contact information for the event.'),
] + $propertyDefaults;
$form['properties']['description'] = [
'#title' => $this->t('Description'),
'#default_value' => $this->entity->getProperty('description') ?? '[eventinstance:description]',
'#description' => $this->t('A more complete description of the event than that provided by the summary.'),
] + $propertyDefaults;
$form['properties']['geo'] = [
'#title' => $this->t('Geographic Position'),
'#default_value' => $this->entity->getProperty('geo'),
'#description' => $this->t('The global position for the event. The value must be two semicolon-separated float values.'),
] + $propertyDefaults;
$form['properties']['location'] = [
'#title' => $this->t('Location'),
'#default_value' => $this->entity->getProperty('location'),
'#description' => $this->t('The intended venue for the event.'),
] + $propertyDefaults;
$form['properties']['priority'] = [
'#title' => $this->t('Priority'),
'#default_value' => $this->entity->getProperty('priority'),
'#description' => $this->t('The relative priority of the event. The value must be an integer in the range 0 to 9. A value of 0 specifies an undefined priority. A value of 1 is the highest priority. A value of 9 is the lowest priority.'),
] + $propertyDefaults;
$form['properties']['url'] = [
'#title' => $this->t('URL'),
'#default_value' => $this->entity->getProperty('url') ?? '[eventinstance:url]',
'#description' => $this->t('A URL associated with the event.'),
] + $propertyDefaults;
return $form;
}
/**
* Ajax form submit handler that returns the rebuilt form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The form structure.
*/
public function rebuildForm(array $form, FormStateInterface $form_state): array {
return $form;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
if ($this->entity->isNew() && empty($form['id']['#options'])) {
$form['id'] = [
'#markup' => $this->t('All event instance types are already mapped.'),
];
unset($form['actions']['submit']);
$form['actions']['cancel'] = [
'#type' => 'link',
'#title' => $this->t('Cancel'),
'#url' => new Url('entity.event_ical_mapping.collection'),
'#attributes' => [
'class' => [
'button',
],
],
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
$entity = parent::buildEntity($form, $form_state);
if ($entity->isNew()) {
$type = $form_state->getValue('id');
$types = $this->entityTypeBundleInfo->getBundleInfo('eventinstance');
$entity->set('label', $types[$type]['label']);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#name'] === 'select_id_submit') {
$form_state->set('event_type', $form_state->getValue('id'));
$form_state->setRebuild();
}
else {
parent::submitForm($form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$status = parent::save($form, $form_state);
$this->messenger()->addMessage($this->t('%label iCalendar property mapping saved.', [
'%label' => $this->entity->label(),
]));
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
return $status;
}
/**
* Returns an array of unmapped event instance types.
*
* @return array
* A list of unmapped event instance types as $id => $label.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getUnmappedTypes(): array {
$unmappedTypes = [];
$types = $this->entityTypeBundleInfo->getBundleInfo('eventinstance');
if ($types) {
$mappedTypes = $this->entityTypeManager->getStorage('event_ical_mapping')->loadMultiple(array_keys($types));
foreach ($types as $type => $info) {
if (!isset($mappedTypes[$type])) {
$unmappedTypes[$type] = $info['label'];
}
}
}
return $unmappedTypes;
}
}
<?php
namespace Drupal\recurring_events_ical\Plugin\Field\FieldFormatter;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\link\Plugin\Field\FieldFormatter\LinkFormatter;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'event_ical_link' formatter.
*
* @FieldFormatter(
* id = "event_ical_link",
* label = @Translation("Event iCalendar Link"),
* field_types = {
* "event_ical_link"
* }
* )
*/
class EventICalLinkFormatter extends LinkFormatter {
/**
* The configuration factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var \Drupal\Core\Path\PathValidatorInterface $pathValidator */
$pathValidator = $container->get('path.validator');
/** @var \Drupal\Core\Config\ConfigFactoryInterface $configFactory */
$configFactory = $container->get('config.factory');
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$pathValidator,
$configFactory
);
}
/**
* Constructs a new EventICalLinkFormatter.
*
* @param string $plugin_id
* The plugin_id for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Third party settings.
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
* The path validator service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, PathValidatorInterface $path_validator, ConfigFactoryInterface $config_factory) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $path_validator);
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$entity = $items->getEntity();
$entityType = $entity->getEntityType()->id();
$config = $this->configFactory->get("recurring_events.$entityType.config");
$linkTitle = $config->get('ical_link_title') ?? $this->t('Download as iCal');
foreach ($items as $item) {
$item->title = $linkTitle;
}
return parent::viewElements($items, $langcode);
}
}
<?php
namespace Drupal\recurring_events_ical\Plugin\Field\FieldType;
use Drupal\link\Plugin\Field\FieldType\LinkItem;
/**
* Plugin implementation of the 'event_ical_link' field type.
*
* @FieldType(
* id = "event_ical_link",
* label = @Translation("Event iCalendar Link"),
* description = @Translation("A link to an event's iCalendar download."),
* default_widget = "link_default",
* default_formatter = "event_ical_link",
* constraints = {
* "LinkType" = {},
* "LinkAccess" = {},
* "LinkExternalProtocols" = {},
* "LinkNotExistingInternal" = {},
* }
* )
*/
class EventICalLinkItem extends LinkItem {
// No implementation; only exists to define the default formatter.
}
<?php
namespace Drupal\Tests\recurring_events_ical\Kernel;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\KernelTests\KernelTestBase;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\recurring_events\Entity\EventSeries;
/**
* @coversDefaultClass \Drupal\recurring_events_ical\EventICal
* @group recurring_events_ical
*/
class EventICalTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'datetime',
'datetime_range',
'field',
'field_inheritance',
'options',
'recurring_events',
'recurring_events_ical',
'system',
'text',
'token',
'user',
];
/**
* {@inheritdoc}
*/
protected static $configSchemaCheckerExclusions = [
'core.entity_view_display.eventseries.default.default',
'core.entity_view_display.eventseries.default.list',
'core.entity_view_display.eventinstance.default.default',
'core.entity_view_display.eventinstance.default.list',
];
/**
* The service under test.
*
* @var \Drupal\recurring_events_ical\EventICalInterface
*/
protected $eventICal;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('field_inheritance');
$this->installEntitySchema('eventseries_type');
$this->installEntitySchema('eventinstance_type');
$this->installEntitySchema('eventseries');
$this->installEntitySchema('eventinstance');
$this->installEntitySchema('event_ical_mapping');
$this->installEntitySchema('user');
$this->installConfig([
'field_inheritance',
'recurring_events',
'recurring_events_ical',
'datetime',
'system',
'user',
]);
$this->eventICal = $this->container->get('recurring_events_ical.event_ical');
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
/**
* Tests EventICal::render() for basic values.
*/
public function testRenderBasic() {
$event = $this->createEventSeries(
'Test event with a title that is longer than 75 characters. Seriously, it just keeps going and going.',
[
[
'start' => '2022-01-01 00:00:00',
'end' => '2022-01-01 00:30:00',
],
]
);
$iCal = explode("\r\n", $this->eventICal->render($event));
$this->assertPreamble($iCal);
$this->assertSame('DTSTART:20220101T000000Z', $iCal[6]);
$this->assertSame('DTEND:20220101T003000Z', $iCal[7]);
// cspell:ignore Seriousl
$this->assertSame('SUMMARY:Test event with a title that is longer than 75 characters. Seriousl', $iCal[8]);
$this->assertSame(' y, it just keeps going and going.', $iCal[9]);
$this->assertSame('END:VEVENT', $iCal[10]);
$this->assertSame('END:VCALENDAR', $iCal[11]);
}
/**
* Tests EventICal::render() for mapped token values.
*/
public function testRenderMapped() {
$event = $this->createEventSeries(
'Mapped event series',
[
[
'start' => '2022-01-01 00:00:00',
'end' => '2022-01-01 00:30:00',
],
],
"<h2>Agenda</h2>\n\n<ul>\n\t<li>Talk about a lot of boring things</li>\n<li>Talk about more boring things</li>\n<li>Pizza</li>\n</ul>"
);
$mapping = $this->entityTypeManager->getStorage('event_ical_mapping')->create([
'id' => 'default',
'label' => 'Default',
'properties' => [
'summary' => '[eventinstance:title]',
'contact' => 'That Guy',
'description' => '[eventinstance:description]',
'geo' => '12.34;56.78',
'location' => 'Conference Room',
'priority' => '1',
'url' => '[eventinstance:url]',
],
]);
$mapping->save();
$iCal = explode("\r\n", $this->eventICal->render($event));
$this->assertPreamble($iCal);
$this->assertSame('DTSTART:20220101T000000Z', $iCal[6]);
$this->assertSame('DTEND:20220101T003000Z', $iCal[7]);
$this->assertSame('SUMMARY:Mapped event series', $iCal[8]);
$this->assertSame('CONTACT:That Guy', $iCal[9]);
$this->assertSame('DESCRIPTION:Agenda\n\n\n' . "\t" . 'Talk about a lot of boring things\nTalk about more', $iCal[10]);
$this->assertSame(' boring things\nPizza', $iCal[11]);
$this->assertSame('GEO:12.34;56.78', $iCal[12]);
$this->assertSame('LOCATION:Conference Room', $iCal[13]);
$this->assertSame('PRIORITY:1', $iCal[14]);
$this->assertMatchesRegularExpression('~URL:(http|https)://(.+)/events/([0-9]+)~', $iCal[15]);
$this->assertSame('END:VEVENT', $iCal[16]);
$this->assertSame('END:VCALENDAR', $iCal[17]);
}
/**
* Creates an event series for use in a test.
*
* @param string $title
* The series title.
* @param array $dates
* An array of event start/end dates in the format: [
* [
* 'start' => 'YYYY-MM-DD HH:MM:SS',
* 'end' => 'YYYY-MM-DD HH:MM:SS',
* ],
* ...
* ].
* @param string $body
* (optional) The event's body text.
*
* @return \Drupal\recurring_events\Entity\EventSeries
* The event series.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function createEventSeries(string $title, array $dates, string $body = ''): EventSeries {
$customDates = [];
foreach ($dates as $date) {
$start = new DrupalDateTime($date['start']);
$end = new DrupalDateTime($date['end']);
$customDates[] = [
'value' => $start->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
'end_value' => $end->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
];
}
/** @var \Drupal\recurring_events\Entity\EventSeries $event */
$event = $this->entityTypeManager->getStorage('eventseries')->create([
'title' => $title,
'uid' => 1,
'type' => 'default',
'recur_type' => 'custom',
'custom_date' => $customDates,
'body' => $body,
]);
$event->save();
return $event;
}
/**
* Asserts that the first six lines of iCal data are correct.
*
* @param array $iCal
* The iCal rendering exploded as an array.
*/
protected function assertPreamble(array $iCal) {
$this->assertSame('BEGIN:VCALENDAR', $iCal[0]);
$this->assertSame('VERSION:2.0', $iCal[1]);
$this->assertSame('PRODID:-//Drupal//recurring_events_ical//2.0//EN', $iCal[2]);
$this->assertSame('BEGIN:VEVENT', $iCal[3]);
$this->assertMatchesRegularExpression('/UID:([a-z0-9\-]+)@(.+)/', $iCal[4]);
$this->assertMatchesRegularExpression('/DTSTAMP:([0-9]{8})T([0-9]{6})Z/', $iCal[5]);
}
}
Recurring Events (recurring_events)
============
## Introduction
The Recurring Events Registration module is a submodule of recurring_events. It
provides a registration system designed to be site agnostic and extensible.
Detailed information about the module is available on the module's help page at
/admin/help/recurring_events_registration.
Introduction
------------
The Recurring Events Registration module is a submodule of `recurring_events`.
It provides a registration system designed to be site agnostic and extensible.
### Data Modeling Concepts
The `registrant` custom entity type represents a single person who has
registered for an `eventseries` or `eventinstance` - depending on the
registration configuration for the `eventseries`. The `registrant` entity type
is only available when the `recurring_events_registration` sub-module is
enabled. The entity can be bundled, but bundles are controlled by the series.
The `registrant` entity type has one base field - `email`, because email is so
integral to most registration systems. The entity is also fieldable and the
module ships with some installation configuration to add first name, last name
and phone number. These are added using the Fields API, so they can be removed
or edited as needed.
Registrants are not revisionable or translatable.
If an `eventseries` is set to have registrations enabled, then a number of
things are checked:
1. What is the capacity of the event?
2. How many users are already registered?
3. Is there a waiting list?
The answer to those questions dictates what experience a user has when they try
to register for an event. If registration is enabled and there is capacity then
the user will be able to register for the event one at a time. If there is no
capacity left, but there is a waiting list the verbiage will change a little and
the user will be added to a waitlist instead. If there is no capacity and no
waitlist, then the user cannot register.
There are several settings related to registration for events:
1. Registration type
There are two registration types:
1. Series Registration - when this mode is used users register for all
instances in a series with one registration. This is helpful should a
user need to attend all instances in a series, for example if the series
was a set of six computer classes.
2. Instance Registration - when this mode is used users register for
individual instances in a series. This is helpful if a user does not
need to attend every event, for example if the series is a recurring
story time at a library with limited capacity.
2. Registration Dates
A user creating an event series can specify when registration is allowed for
an event series or instance. There are two options:
1. Open Registration - when this mode is enabled then registration for an
event instance is available from the moment the instance is published,
until the moment the instance begins. For series registration users are
able to register from the moment the instances are published until the
moment the first event in the series begins.
2. Scheduled Registration - when registering for an entire series, users
are able to specify the date and time that event registration opens and
closes. For individual instance registration a user can specify how many
days or hours prior to the instance start date that registration opens -
in this case, registration closes when the event instance begins.
3. Capacity
Users can specify how many registrants can attend an event. If event series
registration is enabled, then the capacity applies across all instances. If
individual event instance registration is enabled, then each instance will
have the same capacity.
4. Waitlist
Users can specify whether an event has a waitlist. If event series
registration is enabled, then the waitlist applies across all instances. If
individual event instance registration is enabled, then each instance will
have its own waitlist. When an event is full users will automatically get
added to the waitlist if enabled. If a spot opens up on the registration
list because someone deleted their registration, then the first person on
the waitlist is automatically promoted to the registration list.
Users with appropriate permissions will be allowed to view the list of
registrants for an event and modify or delete them.
`Authenticated users` are able to modify and delete their own registrations
through their account page via a new `Registrations` tab in the user profile.
`Anonymous users` can optionally receive an email to the email address provided,
with a unique URL to edit/cancel their registration. The URL will contain a UUID
value to make them difficult to guess and therefore reduce the risk of gaming
the system. An `anonymous user` will be unable to view a list of their
registrations anywhere because they do not have an account page. Instead, they
will manage their registrations purely through emails and links within those
emails.
### Registration Emails
The ability to enable/disable individual registration emails or modify their
subject/body fields is built in to the core of the module. This gives users lots
of flexibility when it comes to messaging registrants. There are a number of
tokens available to administrators to use in the emails.
Out-of-the-box the following registrant emails are available
|Email|Notes|
|:----|:----|
|Registration|Send an email to a registrant to confirm they were registered for an event|
|Waitlist|Send an email to a registrant to confirm they were added to the waitlist|
|Promotion|Send an email to a registrant to confirm they were promoted from the waitlist|
|Instance Deletion|Send an email to a registrant to confirm an instance deletion|
|Series Deletion|Send an email to a registrant to confirm a series deletion|
|Instance Modification|Send an email to a registrant to confirm an instance modification|
|Series Modification|Send an email to a registrant to confirm a series modification|
More can be added using the Registrant API by implementing
the `hook_recurring_events_registration_notification_types_alter` hook.
Users with appropriate permissions are also able to resend emails to registrants
in the case that an email was not delivered. This is available from the
registrant list pages.
Users with appropriate permissions can also contact all registrants of an event
through a form where they can specify the subject and body of the email, again
with tokens available. Users can specify whether to contact just registrants,
just waitlisted users, or both.
### Hooks, extensibility and APIs
The Recurring Events Registration module exposes its own hooks to use to modify
core functionality. These hooks are defined
in: `recurring_events_registration.api.php`.
### Dependencies
This registration submodule only relies on the main recurring\_events module
being enabled:
- recurring\_events:recurring\_events
show_capacity: true
insert_redirect_choice: current
insert_redirect_other: ''
use_admin_theme: false
limit: 10
date_format: 'F jS, Y h:iA'
title: '[registrant:email]'
email_notifications: true
registration_notification_enabled: true
registration_notification_subject: 'You''ve Successfully Registered'
registration_notification_body: "Your registration for the [eventinstance:title] [eventinstance:reg_type] was successful.\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
waitlist_notification_enabled: true
waitlist_notification_subject: 'You''ve Been Added To The Waitlist'
waitlist_notification_body: "You have been added to the waitlist for the [eventinstance:title] [eventinstance:reg_type].\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
promotion_notification_enabled: true
promotion_notification_subject: 'You''ve Been Added To The Registration List'
promotion_notification_body: "You have been promoted from the waitlist to the registration list for the [eventinstance:title] [eventinstance:reg_type].\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
instance_deletion_notification_enabled: true
instance_deletion_notification_subject: 'An Event Has Been Deleted'
instance_deletion_notification_body: 'Unfortunately, the [eventinstance:title] [eventinstance:reg_type] has been deleted. Your registration has been deleted.'
series_deletion_notification_enabled: true
series_deletion_notification_subject: 'An Event Series Has Been Deleted'
series_deletion_notification_body: 'Unfortunately, the [eventinstance:title] [eventinstance:reg_type] has been deleted. Your registration has been deleted.'
instance_modification_notification_enabled: true
instance_modification_notification_subject: 'An Event Has Been Modified'
instance_modification_notification_body: "The [eventinstance:title] [eventinstance:reg_type] has been modified, please check back for details.\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
series_modification_notification_enabled: true
series_modification_notification_subject: 'An Event Series Has Been Modified'
series_modification_notification_body: 'The [eventinstance:title] [eventinstance:reg_type] has been modified, and all instances have been removed, and your registration has been deleted.'
successfully_registered: Registrant successfully created.
successfully_registered_waitlist: Successfully registered to the waitlist.
successfully_updated: Registrant successfully updated.
successfully_updated_waitlist: Successfully updated waitlist registrant.
already_registered: User already registered for this event.
registration_closed: Unfortunately, registration is not available at this time.
notifications:
registration_notification:
enabled: true
subject: 'You''ve Successfully Registered'
body: "Your registration for the [eventinstance:title] [eventinstance:reg_type] was successful.\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
waitlist_notification:
enabled: true
subject: 'You''ve Been Added To The Waitlist'
body: "You have been added to the waitlist for the [eventinstance:title] [eventinstance:reg_type].\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
promotion_notification:
enabled: true
subject: 'You''ve Been Added To The Registration List'
body: "You have been promoted from the waitlist to the registration list for the [eventinstance:title] [eventinstance:reg_type].\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
instance_deletion_notification:
enabled: true
subject: 'An Event Has Been Deleted'
body: 'Unfortunately, the [eventinstance:title] [eventinstance:reg_type] has been deleted. Your registration has been deleted.'
series_deletion_notification:
enabled: true
subject: 'An Event Series Has Been Deleted'
body: 'Unfortunately, the [eventinstance:title] [eventinstance:reg_type] has been deleted. Your registration has been deleted.'
instance_modification_notification:
enabled: true
subject: 'An Event Has Been Modified'
body: "The [eventinstance:title] [eventinstance:reg_type] has been modified, please check back for details.\r\n\r\nModify your registration: [registrant:edit_url]\r\nDelete your registration: [registrant:delete_url]"
series_modification_notification:
enabled: true
subject: 'An Event Series Has Been Modified'
body: 'The [eventinstance:title] [eventinstance:reg_type] has been modified, and all instances have been removed, and your registration has been deleted.'
......@@ -5,6 +5,15 @@ recurring_events_registration.registrant.config:
show_capacity:
type: boolean
label: 'Whether to display the remaining capacity for an event during registration'
insert_redirect_choice:
type: string
label: 'Choose where registrant form redirects'
insert_redirect_other:
type: string
label: 'Type custom URL here'
use_admin_theme:
type: boolean
label: 'Use the administration theme when managing registrations'
limit:
type: integer
label: 'The items per page to show on registrant listing'
......@@ -17,66 +26,46 @@ recurring_events_registration.registrant.config:
email_notifications:
type: boolean
label: 'Whether to enable email notifications'
registration_notification_enabled:
type: boolean
label: 'Whether to enable registration email notifications'
registration_notification_subject:
type: string
label: 'The email subject for the registration emails'
registration_notification_body:
type: string
label: 'The email body for the registration emails'
waitlist_notification_enabled:
type: boolean
label: 'Whether to enable waitlist email notifications'
waitlist_notification_subject:
type: string
label: 'The email subject for the waitlist emails'
waitlist_notification_body:
type: string
label: 'The email body for the waitlist emails'
promotion_notification_enabled:
type: boolean
label: 'Whether to enable promotion email notifications'
promotion_notification_subject:
type: string
label: 'The email subject for the promotion emails'
promotion_notification_body:
type: string
label: 'The email body for the promotion emails'
instance_deletion_notification_enabled:
email_notifications_queue:
type: boolean
label: 'Whether to enable instance deletion email notifications'
instance_deletion_notification_subject:
type: string
label: 'The email subject for the instance deletion emails'
instance_deletion_notification_body:
type: string
label: 'The email body for the instance deletion emails'
series_deletion_notification_enabled:
type: boolean
label: 'Whether to enable series deletion email notifications'
series_deletion_notification_subject:
type: string
label: 'The email subject for the series deletion emails'
series_deletion_notification_body:
type: string
label: 'The email body for the series deletion emails'
instance_modification_notification_enabled:
type: boolean
label: 'Whether to enable instance modification email notifications'
instance_modification_notification_subject:
type: string
label: 'The email subject for the instance modification emails'
instance_modification_notification_body:
type: string
label: 'The email body for the instance modification emails'
series_modification_notification_enabled:
label: 'Whether to use the email notifications queue'
successfully_registered:
type: label
label: 'The message to display when a registrant is successfully created'
successfully_registered_waitlist:
type: label
label: 'The message to display when a registrant is successfully added to the waitlist'
successfully_updated:
type: label
label: 'The message to display when a registrant is successfully updated'
successfully_updated_waitlist:
type: label
label: 'The message to display when a registrant on the waitlist is successfully updated'
already_registered:
type: label
label: 'The message to display when a user is already registered for an event'
registration_closed:
type: label
label: 'The message to display when registration is closed for an event'
notifications:
type: sequence
sequence:
type: mapping
label: 'Email notification'
mapping:
enabled:
type: boolean
label: 'Whether to enable these notifications'
subject:
type: label
label: 'The email subject for these notifications'
body:
type: text
label: 'The email body for these notifications'
field.widget.settings.event_registration:
type: mapping
label: 'Recurring Events Show Enable Waitlist'
mapping:
show_enable_waitlist:
type: boolean
label: 'Whether to enable series modification email notifications'
series_modification_notification_subject:
type: string
label: 'The email subject for the series modification emails'
series_modification_notification_body:
type: string
label: 'The email body for the series modification emails'
label: 'Enable Waiting List'
views.filter.eventinstance_registration_availability_count:
type: views.filter.numeric
views.filter_value.eventinstance_registration_availability_count:
type: views.filter_value.numeric
views.argument.eventinstance_registration_availability_count:
type: views.argument.numeric
name: Recurring Events Registration Reminders
type: module
description: Enables reminders to be sent for upcoming events.
package: Recurring Events
core_version_requirement: ^9.3 || ^10
dependencies:
- recurring_events:recurring_events
- recurring_events:recurring_events_registration
<?php
/**
* @file
* Install and update functions for the Registration Reminders submodule.
*/
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Install the schema updates for eventseries entities to add registration.
*
* @see hook_install()
*/
function recurring_events_reminders_install() {
// Add the registration reminders custom field to eventseries.
$storage_definition = BaseFieldDefinition::create('registration_reminders')
->setName('registration_reminders')
->setLabel(t('Event Registration Reminders'))
->setDescription('The event registration reminders configuration.')
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE)
->setRevisionable(TRUE)
->setTranslatable(FALSE)
->setCardinality(1)
->setRequired(FALSE)
->setDisplayOptions('form', [
'type' => 'registration_reminders',
'weight' => 11,
]);
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('registration_reminders', 'eventseries', 'eventseries', $storage_definition);
// Add a field to eventinstances to store the desired reminder date.
$storage_definition = BaseFieldDefinition::create('timestamp')
->setName('reminder_date')
->setLabel(t('Reminder Date'))
->setDescription('The date that reminders should be sent.')
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE)
->setRevisionable(FALSE)
->setTranslatable(FALSE)
->setCardinality(1)
->setRequired(FALSE);
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('reminder_date', 'eventinstance', 'eventinstance', $storage_definition);
// Add a field to eventinstances to store date reminders were sent.
$storage_definition = BaseFieldDefinition::create('timestamp')
->setName('reminder_sent')
->setLabel(t('Reminder Sent'))
->setDescription('The date that reminders were sent.')
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE)
->setRevisionable(FALSE)
->setTranslatable(FALSE)
->setCardinality(1)
->setRequired(FALSE);
\Drupal::entityDefinitionUpdateManager()
->installFieldStorageDefinition('reminder_sent', 'eventinstance', 'eventinstance', $storage_definition);
// Set the default reminder email subject and body config.
$config = \Drupal::configFactory()->getEditable('recurring_events_registration.registrant.config');
$values = $config->get('notifications');
$values['registration_reminder'] = [
'enabled' => TRUE,
'subject' => 'Upcoming Event Reminder',
'body' => '[eventseries:reminder_message]',
];
$config->set('notifications', $values);
$config->save(TRUE);
}
<?php
/**
* @file
* Primary module hooks for Recurring Events Registration Reminders module.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Implements hook_entity_base_field_info_alter().
*/
function recurring_events_reminders_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
if ($entity_type_id === 'eventseries') {
$fields['registration_reminders'] = BaseFieldDefinition::create('registration_reminders')
->setName('registration_reminders')
->setLabel(t('Event Registration Reminders'))
->setDescription('The event registration reminders configuration.')
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE)
->setRevisionable(TRUE)
->setTranslatable(FALSE)
->setCardinality(1)
->setRequired(FALSE)
->setTargetEntityTypeId($entity_type->id())
->setDisplayOptions('form', [
'type' => 'registration_reminders',
'weight' => 10,
]);
}
if ($entity_type_id === 'eventinstance') {
$fields['reminder_date'] = BaseFieldDefinition::create('timestamp')
->setName('reminder_date')
->setLabel(t('Reminder Date'))
->setDescription('The date that reminders should be sent.')
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE)
->setRevisionable(FALSE)
->setTranslatable(FALSE)
->setTargetEntityTypeId($entity_type_id)
->setCardinality(1)
->setRequired(FALSE);
$fields['reminder_sent'] = BaseFieldDefinition::create('timestamp')
->setName('reminder_sent')
->setLabel(t('Reminder Sent'))
->setDescription('The date that reminders were sent.')
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', FALSE)
->setRevisionable(FALSE)
->setTranslatable(FALSE)
->setTargetEntityTypeId($entity_type_id)
->setCardinality(1)
->setRequired(FALSE);
}
}
/**
* Implements hook_ENTITY_TYPE_insert().
*
* Set the reminder date for all instances in this series.
*/
function recurring_events_reminders_eventseries_insert(EntityInterface $entity) {
if (\Drupal::isConfigSyncing()) {
return;
}
if ($entity->registration_reminders->reminder) {
$amount = $entity->registration_reminders->reminder_amount;
$units = $entity->registration_reminders->reminder_units;
$instances = $entity->event_instances->referencedEntities();
if (!empty($instances)) {
foreach ($instances as $instance) {
$instance_date = $instance->date->start_date->getTimestamp();
$reminder_date = strtotime('-' . $amount . ' ' . $units, $instance_date);
$instance->set('reminder_date', $reminder_date);
$instance->setNewRevision(FALSE);
$instance->save();
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_update().
*
* Set the reminder date for all instances in this series.
*/
function recurring_events_reminders_eventseries_update(EntityInterface $entity) {
$original = $entity->original;
if ($entity->registration_reminders->reminder) {
$entity = $entity->load($entity->id());
if ($original->registration_reminders->reminder_amount !== $entity->registration_reminders->reminder_amount
|| $original->registration_reminders->reminder_units !== $entity->registration_reminders->reminder_units
) {
$amount = $entity->registration_reminders->reminder_amount;
$units = $entity->registration_reminders->reminder_units;
$instances = $entity->event_instances->referencedEntities();
if (!empty($instances)) {
foreach ($instances as $instance) {
$instance_date = $instance->date->start_date->getTimestamp();
$reminder_date = strtotime('-' . $amount . ' ' . $units, $instance_date);
$instance->set('reminder_date', $reminder_date);
$instance->set('reminder_sent', NULL);
$instance->setNewRevision(FALSE);
$instance->save();
}
}
}
}
// If reminders have been disabled, then set the reminder_date to NULL.
elseif ($original->registration_reminders->reminder !== $entity->registration_reminders->reminder) {
$instances = $entity->event_instances->referencedEntities();
if (!empty($instances)) {
foreach ($instances as $instance) {
$instance->set('reminder_date', NULL);
$instance->setNewRevision(FALSE);
$instance->save();
}
}
}
}
/**
* Implements hook_cron().
*/
function recurring_events_reminders_cron() {
$query = \Drupal::entityTypeManager()->getStorage('eventinstance')->getQuery();
$event_instances = $query
->condition('reminder_date', NULL, 'IS NOT NULL')
->condition('reminder_sent', NULL, 'IS NULL')
->condition('reminder_date', time(), '<=')
->accessCheck(FALSE)
->execute();
if (!empty($event_instances)) {
$instances = \Drupal::entityTypeManager()->getStorage('eventinstance')->loadMultiple($event_instances);
/** @var \Drupal\recurring_events\Entity\EventInstance */
foreach ($instances as $instance) {
$instance->set('reminder_sent', time());
$instance->setNewRevision(FALSE);
$instance->save();
/** @var \Drupal\recurring_events_registration\RegistrationCreationService */
$registration_creation_service = \Drupal::service('recurring_events_registration.creation_service');
$registration_creation_service->setEventInstance($instance);
$registrants = $registration_creation_service->retrieveRegisteredParties();
if (empty($registrants)) {
return;
}
$key = 'registration_reminder';
$config = \Drupal::config('recurring_events_registration.registrant.config');
$queue_is_enabled = $config->get('email_notifications_queue');
// Send an email to all registrants.
foreach ($registrants as $registrant) {
if ($queue_is_enabled) {
// Add each notification to be sent to the queue.
\Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
}
else {
// If the queue is not enabled, send this notification immediately.
recurring_events_registration_send_notification($key, $registrant);
}
}
}
}
}
/**
* Implements hook_recurring_events_registration_notification_types_alter().
*/
function recurring_events_reminders_recurring_events_registration_notification_types_alter(array &$notification_types) {
$notification_types += [
'registration_reminder' => [
'name' => t('Registration Reminder'),
'description' => t('Send an email to remind registrants about upcoming events? Note: The event series must be configured to send reminders.'),
],
];
}
/**
* Implements hook_config_schema_info_alter().
*/
function recurring_events_reminders_config_schema_info_alter(&$definitions) {
$definitions['recurring_events_registration.registrant.config']['mapping']['registration_reminder_enabled'] = [
'type' => 'boolean',
'label' => 'Whether to enable email reminders for upcoming events',
];
$definitions['recurring_events_registration.registrant.config']['mapping']['registration_reminder_subject'] = [
'type' => 'string',
'label' => 'The email subject for the reminder emails',
];
$definitions['recurring_events_registration.registrant.config']['mapping']['registration_reminder_body'] = [
'type' => 'string',
'label' => 'The email body for the reminder emails',
];
}