diff --git a/config/sync/dpl_event.settings.yml b/config/sync/dpl_event.settings.yml index 63541ed49..3b61ae667 100644 --- a/config/sync/dpl_event.settings.yml +++ b/config/sync/dpl_event.settings.yml @@ -1,2 +1,4 @@ -unpublish_schedule: 0 +unpublish_schedule: '21600' price_currency: DKK +unpublish_enable: 1 +unpublish_series_enable: 0 diff --git a/config/sync/views.view.events.yml b/config/sync/views.view.events.yml index fc06ebc12..385282804 100644 --- a/config/sync/views.view.events.yml +++ b/config/sync/views.view.events.yml @@ -161,8 +161,12 @@ display: options: perm: 'access content' cache: - type: none - options: { } + type: search_api_time + options: + results_lifespan: 3600 + results_lifespan_custom: 0 + output_lifespan: 3600 + output_lifespan_custom: 0 empty: area: id: area @@ -377,5 +381,7 @@ display: - url.query_args - user.permissions tags: + - 'config:facets.facet.branch' + - 'config:facets.facet.event_categories' - 'config:search_api.index.events' - 'search_api_list:events' diff --git a/web/modules/custom/dpl_admin/assets/dpl_admin.css b/web/modules/custom/dpl_admin/assets/dpl_admin.css index 43645637d..a8bc60625 100644 --- a/web/modules/custom/dpl_admin/assets/dpl_admin.css +++ b/web/modules/custom/dpl_admin/assets/dpl_admin.css @@ -198,3 +198,12 @@ body:has(.eventseries-form--confirm) .form-actions { display: none; } + +/** + A very simple element, used in Drupal forms when displaying warnings. + */ +.dpl-form-warning { + color: #d80404; + font-weight: bold; + max-width: 500px; +} diff --git a/web/modules/custom/dpl_event/dpl_event.install b/web/modules/custom/dpl_event/dpl_event.install index 2917df63b..c68ec473d 100644 --- a/web/modules/custom/dpl_event/dpl_event.install +++ b/web/modules/custom/dpl_event/dpl_event.install @@ -6,6 +6,8 @@ */ use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\dpl_event\Form\SettingsForm; +use Drupal\dpl_event\Workflows\UnpublishSchedule; use Drupal\drupal_typed\DrupalTyped; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -18,6 +20,7 @@ use Drupal\locale\StringDatabaseStorage; function dpl_event_install(): void { _dpl_event_create_mock_title(); _dpl_event_add_weekday_translations(); + _dpl_event_set_scheduler_settings(); } /** @@ -67,6 +70,45 @@ function dpl_event_update_10004(): string { return _dpl_event_add_weekday_translations(); } +/** + * Set default values of event-unpublishing-schedule settings, and re-schedule. + */ +function dpl_event_update_10005(): string { + return _dpl_event_set_scheduler_settings(); +} + +/** + * Set default values of event-unpublishing-schedule settings, and re-schedule. + */ +function _dpl_event_set_scheduler_settings(): string { + $config_name = SettingsForm::CONFIG_NAME; + + $config = \Drupal::configFactory()->getEditable($config_name); + + $existing_schedule = $config->get('unpublish_schedule'); + + // If we have no existing schedule set, set it as default to 6 hours. + $schedule_time = !empty($existing_schedule) ? $existing_schedule : 21600; + + // If no schedule is set already, we'll disable the setting. + $unpublish_enable = ($existing_schedule != 0); + + $config->set('unpublish_schedule', $schedule_time); + $config->set('unpublish_enable', $unpublish_enable); + + // This was originally enabled for all sites, but as we no longer recommend + // it, we'll set the default to false. + // The libraries will be informed of this in the changelog. + $config->set('unpublish_series_enable', FALSE); + + $config->save(); + + $unpublish_scheduler = DrupalTyped::service(UnpublishSchedule::class, 'dpl_event.unpublish_schedule'); + $count = $unpublish_scheduler->rescheduleAll(); + + return "Enabled new unpublishing settings. $count events has been rescheduled."; +} + /** * Add a mock title field to eventinstance, to fix entity reference fields. * diff --git a/web/modules/custom/dpl_event/src/EventSubscriber/EventSeriesRedirect.php b/web/modules/custom/dpl_event/src/EventSubscriber/EventSeriesRedirect.php index 09da17ed2..8487e297d 100644 --- a/web/modules/custom/dpl_event/src/EventSubscriber/EventSeriesRedirect.php +++ b/web/modules/custom/dpl_event/src/EventSubscriber/EventSeriesRedirect.php @@ -12,7 +12,12 @@ use Symfony\Component\HttpKernel\KernelEvents; /** - * Redirects requests for single instance event series to that instance. + * Redirect to AND from eventseries, depending on the situation. + * + * - If accessing an eventseries with a single active instance, redirect to + * that instance + * - If failing to access an unpublished eventinstance, redirect to the + * parent series. * * @package Drupal\dpl_event\EventSubscriber */ @@ -31,7 +36,14 @@ public function __construct( */ public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => 'checkEventSeriesRedirect', + KernelEvents::REQUEST => [ + ['checkEventSeriesRedirect'], + ], + // Only target users that end up on an unpublished eventinstance page, + // without being allowed to see it. + KernelEvents::EXCEPTION => [ + ['checkEventInstanceRedirect'], + ], ]; } @@ -47,7 +59,6 @@ public static function getSubscribedEvents(): array { * The request event. */ public function checkEventSeriesRedirect(RequestEvent $event): void { - $request = $event->getRequest(); $route_name = $request->attributes->get('_route'); $event_series = $request->attributes->get('eventseries'); @@ -73,4 +84,31 @@ public function checkEventSeriesRedirect(RequestEvent $event): void { } } + /** + * Redirect unpublished eventinstances to eventseries. + * + * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event + * The response event. + */ + public function checkEventInstanceRedirect(RequestEvent $event): void { + $request = $event->getRequest(); + + if ($request->attributes->get('_route') !== 'entity.eventinstance.canonical') { + return; + } + + $event_instance = $request->attributes->get('eventinstance'); + + if (!($event_instance instanceof EventInstance) || $event_instance->isPublished()) { + return; + } + + // At this stage, we know we're on an unpublished eventinstance. + // Look up the eventseries, and redirect to it. + $event_series = $event_instance->getEventSeries(); + + $response = new RedirectResponse($event_series->toUrl()->toString()); + $event->setResponse($response); + } + } diff --git a/web/modules/custom/dpl_event/src/Form/SettingsForm.php b/web/modules/custom/dpl_event/src/Form/SettingsForm.php index 9140c5687..14fe149c1 100644 --- a/web/modules/custom/dpl_event/src/Form/SettingsForm.php +++ b/web/modules/custom/dpl_event/src/Form/SettingsForm.php @@ -72,12 +72,21 @@ public function buildForm(array $form, FormStateInterface $form_state): array { ], ]; + $form['enable_screen_name'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enable screen names'), + '#default_value' => $config->get('enable_screen_name'), + '#description' => $this->t('Enable screen names on events. Requires an external system to actually fetch and display the events on real physical screens.'), + ]; + $form['unpublish'] = [ '#type' => 'fieldset', '#title' => $this->t('Automatic unpublication', [], ['context' => 'DPL event']), ]; $period = [ + // 1 hour + 3600, // 6 hours 21600, // 12 hours @@ -98,21 +107,66 @@ public function buildForm(array $form, FormStateInterface $form_state): array { 15552000, ]; $period = array_map([$this->dateFormatter, 'formatInterval'], array_combine($period, $period)); + + $form['unpublish']['unpublish_enable'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Unpublish eventinstances when they have occured (recommended)', [], ['context' => 'DPL event']), + '#default_value' => $config->get('unpublish_enable'), + ]; + + $form['unpublish']['unpublish_disable_warning'] = [ + '#type' => 'container', + // js-form-wrapper is important - otherwise, Drupal states will not work. + '#prefix' => '
', + '#markup' => $this->t('Notice - if you do not choose that eventinstances get unpublished, they may show up in automatic and manual lists, across the site.', [], ['context' => 'DPL event']), + '#suffix' => '
', + '#states' => [ + 'visible' => [ + ':input[name="unpublish_enable"]' => ['checked' => FALSE], + ], + ], + ]; + $form['unpublish']['unpublish_schedule'] = [ '#type' => 'select', - '#title' => $this->t('Schedule', [], ['context' => "DPL event"]), + '#title' => $this->t('How much time should pass after an eventinstance has occurred before it should be unpublished?', [], ['context' => "DPL event"]), '#default_value' => $config->get('unpublish_schedule'), '#options' => $period, - '#empty_option' => $this->t('Automatic unpublication disabled', [], ['context' => "DPL event"]), - '#empty_value' => 0, - '#description' => $this->t('How much time should pass after an event has occurred before it should be unpublished automatically.', [], ['context' => "DPL event"]), + '#states' => [ + 'visible' => [ + ':input[name="unpublish_enable"]' => ['checked' => TRUE], + ], + ], ]; - $form['enable_screen_name'] = [ + $form['unpublish']['unpublish_series_enable'] = [ '#type' => 'checkbox', - '#title' => $this->t('Enable screen names'), - '#default_value' => $config->get('enable_screen_name'), - '#description' => $this->t('Enable screen names on events. Requires an external system to actually fetch and display the events on real physical screens.'), + '#title' => $this->t('Unpublish the series when all instances have occurred (not recommended)', [], ['context' => "DPL event"]), + '#default_value' => $config->get('unpublish_series_enable'), + // Only display the field when unpublish schedule has a non-0 value. + '#states' => [ + 'visible' => [ + ':input[name="unpublish_enable"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['unpublish']['unpublish_series_enable_warning'] = [ + '#type' => 'container', + // js-form-wrapper is important - otherwise, Drupal states will not work. + '#prefix' => '
', + '#markup' => $this->t('Notice - if series get unpublished, old instance links will no longer work. If you however keep the series published, expired instances will redirect to the associated series.', [], ['context' => 'DPL event']), + '#suffix' => '
', + '#states' => [ + 'visible' => [ + ':input[name="unpublish_enable"]' => ['checked' => TRUE], + // PHPCS doesn't understand Drupal's weird way of doing states. + // phpcs:disable Squiz.Arrays.ArrayDeclaration.NoKeySpecified + 'and', + // phpcs:enable Squiz.Arrays.ArrayDeclaration.NoKeySpecified + ':input[name="unpublish_series_enable"]' => ['checked' => TRUE], + ], + ], ]; return parent::buildForm($form, $form_state); @@ -124,7 +178,9 @@ public function buildForm(array $form, FormStateInterface $form_state): array { public function submitForm(array &$form, FormStateInterface $form_state): void { $this->config(self::CONFIG_NAME) ->set('price_currency', $form_state->getValue('price_currency')) + ->set('unpublish_enable', $form_state->getValue('unpublish_enable')) ->set('unpublish_schedule', $form_state->getValue('unpublish_schedule')) + ->set('unpublish_series_enable', $form_state->getValue('unpublish_series_enable')) ->set('enable_screen_name', $form_state->getValue('enable_screen_name')) ->save(); parent::submitForm($form, $form_state); diff --git a/web/modules/custom/dpl_event/src/Workflows/UnpublishSchedule.php b/web/modules/custom/dpl_event/src/Workflows/UnpublishSchedule.php index a86b36500..b64bd05c2 100644 --- a/web/modules/custom/dpl_event/src/Workflows/UnpublishSchedule.php +++ b/web/modules/custom/dpl_event/src/Workflows/UnpublishSchedule.php @@ -49,6 +49,17 @@ public function getSchedule(): array { * Callback to execute scheduled unpublication. */ public function callback(JobSchedule $job): void { + $config = $this->configFactory->get(SettingsForm::CONFIG_NAME); + + $enabled = (boolean) $config->get('unpublish_enable'); + + // If the automatic unpublication is disabled, we will skip past this job. + // This should technically not happen, as updating the settings will trigger + // rescheduleAll() on all eventinstances, but this is a nice fallback. + if (!$enabled) { + return; + } + $event = $this->eventInstanceStorage->load($job->getId()); if (!$event || !$event instanceof EventInstance) { throw new \UnexpectedValueException("Unable to load event instance {$job->getId()} for automatic unpublication"); @@ -56,14 +67,26 @@ public function callback(JobSchedule $job): void { $event->setUnpublished()->save(); - // If all instances in the series are unpublished then also unpublish the - // series. + // Detect if site wishes series to be unpublished when all instances are + // unpublished. + $seriesUnpublishingEnabled = (boolean) $config->get('unpublish_series_enable'); + + if (!$seriesUnpublishingEnabled) { + return; + } + + // Count the number of published eventinstances, and if it is 0, unpublish + // the series. $eventSeries = $event->getEventSeries(); - $allEventInstances = $eventSeries->get('event_instances')->referencedEntities(); - $publishedInstances = array_filter($allEventInstances, function (EventInstance $event) { - return $event->isPublished(); - }); - if (empty($publishedInstances)) { + + $publishedEventInstanceIds = ($this->eventInstanceStorage->getQuery()) + ->accessCheck(FALSE) + ->condition('eventseries_id', $eventSeries->id()) + ->condition('status', 1) + ->count() + ->execute(); + + if (empty($publishedEventInstanceIds)) { $eventSeries->setUnpublished()->save(); } } @@ -73,6 +96,7 @@ public function callback(JobSchedule $job): void { */ public function scheduleUnpublication(EventInstance $event): void { $config = $this->configFactory->get(SettingsForm::CONFIG_NAME); + $enabled = (boolean) $config->get('unpublish_enable'); $schedule = (int) $config->get('unpublish_schedule'); $now_timestamp = $this->time->getCurrentTime(); @@ -93,9 +117,10 @@ public function scheduleUnpublication(EventInstance $event): void { // Remove any preexisting job with the same name, type and id. $this->jobScheduler->remove($job); + // If automatic unpublication is enabled and needed then schedule a new // unpublication. - if ($schedule > 0 && $event->isPublished()) { + if ($enabled && $schedule > 0 && $event->isPublished()) { $this->jobScheduler->set($job); $this->logger->debug( @@ -108,10 +133,10 @@ public function scheduleUnpublication(EventInstance $event): void { /** * Reschedule all event instances for unpublication. * - * This will update schedules for all events even those that where not + * This will update schedules for all events even those that were not * scheduled in the past. */ - public function rescheduleAll(): void { + public function rescheduleAll(): int { $this->jobScheduler->removeAll(self::JOB_SCHEDULE_NAME, self::JOB_SCHEDULE_TYPE); $publishedEventInstanceIds = ($this->eventInstanceStorage->getQuery()) @@ -124,6 +149,8 @@ public function rescheduleAll(): void { array_walk($publishedEventInstances, function (EventInstance $event) { $this->scheduleUnpublication($event); }); + + return count($publishedEventInstanceIds); } }