diff --git a/modules/recurring_events_registration/recurring_events_registration.links.menu.yml b/modules/recurring_events_registration/recurring_events_registration.links.menu.yml index 7881024c5b7a99e2f11b4a1578a23dcb6f728d76..751363f52668833f0127518e2dd6254238d16924 100644 --- a/modules/recurring_events_registration/recurring_events_registration.links.menu.yml +++ b/modules/recurring_events_registration/recurring_events_registration.links.menu.yml @@ -21,3 +21,11 @@ entity.registrant_type.collection: route_name: entity.registrant_type.collection parent: events.admin.overview weight: 13 + + # Orphaned Event Registrants +recurring_events_registration.orphaned_registrants: + title: 'Orphaned Event Registrants' + description: 'Delete orphaned event registrants' + route_name: recurring_events_registration.orphaned_registrants + parent: events.admin.overview + weight: 101 diff --git a/modules/recurring_events_registration/recurring_events_registration.module b/modules/recurring_events_registration/recurring_events_registration.module index 98b582f66d0f68b3ffe33b90500ea2730836f555..147f7b012d0ab0a09f41f301c8b67ac0b66313b2 100644 --- a/modules/recurring_events_registration/recurring_events_registration.module +++ b/modules/recurring_events_registration/recurring_events_registration.module @@ -284,20 +284,30 @@ function recurring_events_registration_recurring_events_pre_delete_instance(Even * Implements hook_recurring_events_pre_delete_instances(). */ function recurring_events_registration_recurring_events_pre_delete_instances(EventSeries $event_series) { + /** @var \Drupal\recurring_events_registration\RegistrationCreationService $registration_creation_service */ $registration_creation_service = \Drupal::service('recurring_events_registration.creation_service'); $registration_creation_service->setEventSeries($event_series); - // Get all the registrants who have registered for any event in this series. - $registrants = $registration_creation_service->retrieveAllSeriesRegisteredParties(TRUE); + // Get the registrants registered for a future event and email them about the + // series deletion. + $future_registrants = $registration_creation_service->retrieveAllSeriesRegisteredParties(TRUE); + if (!empty($future_registrants)) { + $key = 'series_deletion_notification'; + + // Send an email to the future registrants. + foreach ($future_registrants as $registrant) { + recurring_events_registration_send_notification($key, $registrant); + } + } + + // Now get all the registrants for this series so we can remove them. + $registrants = $registration_creation_service->retrieveAllSeriesRegisteredParties(); if (empty($registrants)) { return; } - $key = 'series_deletion_notification'; - // Send an email to all registrants. foreach ($registrants as $registrant) { - recurring_events_registration_send_notification($key, $registrant); $registrant->delete(); } } diff --git a/modules/recurring_events_registration/recurring_events_registration.routing.yml b/modules/recurring_events_registration/recurring_events_registration.routing.yml index c80f322d6c7ea2c8e06abc8e195ebeae9c07b097..0e6f352901970daf407140ced074a188eebaad0a 100644 --- a/modules/recurring_events_registration/recurring_events_registration.routing.yml +++ b/modules/recurring_events_registration/recurring_events_registration.routing.yml @@ -191,3 +191,12 @@ entity.registrant_type.collection: _permission: 'administer registrant types' options: _admin_route: TRUE + +# Orphaned registrant cleanup. +recurring_events_registration.orphaned_registrants: + path: '/admin/structure/events/orphaned-registrants' + defaults: + _form: '\Drupal\recurring_events_registration\Form\OrphanedEventRegistrantsForm' + _title: 'Orphaned Event Registrants' + requirements: + _permission: 'administer orphaned events entities' diff --git a/modules/recurring_events_registration/src/Entity/Registrant.php b/modules/recurring_events_registration/src/Entity/Registrant.php index f3c67a7e02392c1e5dbceb25c0b936605f9fc7ad..ac8fd789af119d502b9681ab50d7fec6b9ae4c29 100644 --- a/modules/recurring_events_registration/src/Entity/Registrant.php +++ b/modules/recurring_events_registration/src/Entity/Registrant.php @@ -377,7 +377,7 @@ class Registrant extends EditorialContentEntityBase implements RegistrantInterfa */ protected function urlRouteParameters($rel) { $uri_route_parameters = parent::urlRouteParameters($rel); - $uri_route_parameters['eventinstance'] = $this->getEventInstance()->id(); + $uri_route_parameters['eventinstance'] = $this->getEventInstance() ? $this->getEventInstance()->id() : 0; $uri_route_parameters['registrant'] = $this->id(); if ($rel == 'anon-edit-form' || $rel == 'anon-delete-form') { $uri_route_parameters['uuid'] = $this->uuid->value; diff --git a/modules/recurring_events_registration/src/Form/OrphanedEventRegistrantsForm.php b/modules/recurring_events_registration/src/Form/OrphanedEventRegistrantsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..b89886b5fd764841dd554deaf480ad2340d5dac0 --- /dev/null +++ b/modules/recurring_events_registration/src/Form/OrphanedEventRegistrantsForm.php @@ -0,0 +1,171 @@ +<?php + +namespace Drupal\recurring_events_registration\Form; + +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandler; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\recurring_events_registration\Entity\RegistrantInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a form for handling orphaned registrants. + * + * @ingroup recurring_events + */ +class OrphanedEventRegistrantsForm extends FormBase { + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandler + */ + protected $moduleHandler; + + /** + * Returns a unique string identifying the form. + * + * @return string + * The unique string identifying the form. + */ + public function getFormId() { + return 'recurring_events_registration_orphaned_registrants'; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('database'), + $container->get('entity_type.manager'), + $container->get('module_handler') + ); + } + + /** + * Construct an OrphanedEventInstanceForm. + * + * @param \Drupal\Core\Database\Connection $database + * The database connection. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\Core\Extension\ModuleHandler $module_handler + * The module handler service. + */ + public function __construct(Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandler $module_handler) { + $this->database = $database; + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $registrants = $this->getOrphanedRegistrants(); + $count = count($registrants); + if (!empty($registrants)) { + foreach ($registrants as $registrant) { + $registrant->delete(); + } + } + $this->messenger()->addMessage($this->t('Successfully deleted @count registrants(s).', [ + '@count' => $count, + ])); + } + + /** + * Define the form used for EventInstance settings. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * Form definition array. + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $registrants = $this->getOrphanedRegistrants(); + + $rows = []; + $header = ['Registrant ID', 'Series ID', 'Label', 'Actions']; + $form['table'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No orphaned registrants found.'), + ]; + + if (!empty($registrants)) { + foreach ($registrants as $registrant) { + $rows[] = [ + $registrant->id(), + $registrant->get('eventseries_id')->first()->target_id ?? $this->t('N/A'), + $registrant->label(), + new FormattableMarkup('@view_link | @delete_link', [ + '@view_link' => $registrant->toLink('View')->toString(), + '@delete_link' => $registrant->toLink('Delete', 'delete-form')->toString(), + ]), + ]; + } + + $form['table']['#rows'] = $rows; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete Orphaned Registrants'), + ]; + } + + return $form; + } + + /** + * Get all the orphaned registrants. + * + * @return RegistrantInterface[]|array + * An array of event instance entities, or an empty array. + */ + protected function getOrphanedRegistrants() { + $query = $this->database + ->select('registrant', 'r') + ->fields('r', ['id']); + $query->leftJoin('eventseries', 'es', 'r.eventseries_id = es.id'); + $query->leftJoin('eventinstance', 'ei', 'r.eventinstance_id = ei.id'); + $or_group = $query->orConditionGroup() + ->condition('es.id', NULL, 'IS NULL') + ->condition('ei.id', NULL, 'IS NULL'); + $registrants = $query->condition($or_group) + ->execute() + ->fetchCol(); + + if (!empty($registrants)) { + $registrants = $this->entityTypeManager->getStorage('registrant')->loadMultiple($registrants); + } + return $registrants; + } +} diff --git a/recurring_events.links.menu.yml b/recurring_events.links.menu.yml index 235f7216aaeffb8c8b7450c03f8913fba4270b3b..13399974a296565d4b639e0e5a6bf795398bcd7e 100644 --- a/recurring_events.links.menu.yml +++ b/recurring_events.links.menu.yml @@ -75,3 +75,11 @@ entity.eventinstance_type.collection: route_name: entity.eventinstance_type.collection parent: events.admin.overview weight: 3 + +# Orphaned Event Instances +recurring_events.orphaned_instances: + title: 'Orphaned Event Instances' + description: 'Delete orphaned event instances' + route_name: recurring_events.orphaned_instances + parent: events.admin.overview + weight: 100 diff --git a/recurring_events.permissions.yml b/recurring_events.permissions.yml index 76e212749bea2864d75e1bc0e1ba3dfa9496db93..eced703301071da3c27bb2d167be2b4b5f59b8ca 100644 --- a/recurring_events.permissions.yml +++ b/recurring_events.permissions.yml @@ -90,3 +90,9 @@ administer eventinstance types: title: 'Administer eventinstance types' description: 'Manage types of eventinstance.' restrict access: true + +# Orphaned Content Cleanup +administer orphaned events entities: + title: 'Cleanup orphaned events entities' + description: 'Delete orphaned instances or registrants' + restrict access: true diff --git a/recurring_events.routing.yml b/recurring_events.routing.yml index 71d54467a21ac1efd364292382e6aaa636b4a3f0..a257666227ab79b4fd515f7ca48870d42b20ab79 100644 --- a/recurring_events.routing.yml +++ b/recurring_events.routing.yml @@ -215,6 +215,15 @@ eventinstance.settings: requirements: _permission: 'administer eventinstance entity' +# Orphaned content cleanup. +recurring_events.orphaned_instances: + path: '/admin/structure/events/orphaned-instances' + defaults: + _form: '\Drupal\recurring_events\Form\OrphanedEventInstanceForm' + _title: 'Orphaned Event Instances' + requirements: + _permission: 'administer orphaned events entities' + # Event Series admin table list route. entity.eventseries.admin_collection: path: '/admin/content/events/series' diff --git a/src/Form/OrphanedEventInstanceForm.php b/src/Form/OrphanedEventInstanceForm.php new file mode 100644 index 0000000000000000000000000000000000000000..70175b457aa1299ac267181340c2f7d10990af22 --- /dev/null +++ b/src/Form/OrphanedEventInstanceForm.php @@ -0,0 +1,168 @@ +<?php + +namespace Drupal\recurring_events\Form; + +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandler; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a form for handling orphaned instances. + * + * @ingroup recurring_events + */ +class OrphanedEventInstanceForm extends FormBase { + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandler + */ + protected $moduleHandler; + + /** + * Returns a unique string identifying the form. + * + * @return string + * The unique string identifying the form. + */ + public function getFormId() { + return 'recurring_events_orphaned_instances'; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('database'), + $container->get('entity_type.manager'), + $container->get('module_handler') + ); + } + + /** + * Construct an OrphanedEventInstanceForm. + * + * @param \Drupal\Core\Database\Connection $database + * The database connection. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\Core\Extension\ModuleHandler $module_handler + * The module handler service. + */ + public function __construct(Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandler $module_handler) { + $this->database = $database; + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * Form submission handler. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $instances = $this->getOrphanedInstances(); + $count = count($instances); + if (!empty($instances)) { + foreach ($instances as $instance) { + $this->moduleHandler->invokeAll('recurring_events_pre_delete_instance', [$instance]); + $instance->delete(); + } + } + $this->messenger()->addMessage($this->t('Successfully deleted @count instance(s).', [ + '@count' => $count, + ])); + } + + /** + * Define the form used for EventInstance settings. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * An associative array containing the current state of the form. + * + * @return array + * Form definition array. + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $instances = $this->getOrphanedInstances(); + + $rows = []; + $header = ['Instance ID', 'Series ID', 'Title', 'Actions']; + $form['table'] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No orphaned instances found.'), + ]; + + if (!empty($instances)) { + foreach ($instances as $instance) { + $rows[] = [ + $instance->id(), + $instance->get('eventseries_id')->first()->target_id ?? $this->t('N/A'), + $instance->label() ?? $this->t('Unable to determine'), + new FormattableMarkup('@view_link | @delete_link', [ + '@view_link' => $instance->toLink('View')->toString(), + '@delete_link' => $instance->toLink('Delete', 'delete-form')->toString(), + ]), + ]; + } + + $form['table']['#rows'] = $rows; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete Orphaned Instances'), + ]; + } + + return $form; + } + + /** + * Get all the orphaned instances. + * + * @return EventInstance[]|array + * An array of event instance entities, or an empty array. + */ + protected function getOrphanedInstances() { + $query = $this->database + ->select('eventinstance_field_data', 'efd') + ->fields('efd', ['id']); + $query->leftJoin('eventseries', 'es', 'efd.eventseries_id = es.id'); + $instances = $query->condition('es.id', NULL, 'IS NULL') + ->execute() + ->fetchCol(); + + if (!empty($instances)) { + $instances = $this->entityTypeManager->getStorage('eventinstance')->loadMultiple($instances); + } + return $instances; + } + +}