From 6d9ecd8df06999793cecdb3a0dc06ccacddceaf2 Mon Sep 17 00:00:00 2001 From: Oliver Davies Date: Mon, 24 Aug 2020 02:00:22 +0100 Subject: [PATCH] Fix the ordering for future talks Rather than the custom event sorting plugin being based on the `created` value, this change adds a new `field_event_date` field to the talk node type and uses this for the sorting instead. This commit also adds a new `TalkDateUpdater` service that extracts either the next event date if there is a future date, or the last past event date if there is no future date, from `field_events` for each talk and saves it into the event date field. For consistency, and to ensure that the results are ordered correctly, the talk date updater converts the date from a date string (e.g. `2020-08-24`) into a UNIX timestamp, and the timestamp is saved in the event date field. This can be changed at a later date if needed. The talks view has been updated to use the updated sort plugin, and the existing tests have been updated to use the new field. References #204 --- ....entity_form_display.node.talk.default.yml | 4 +- ....entity_view_display.node.talk.default.yml | 2 + ...e.entity_view_display.node.talk.teaser.yml | 2 + ...field.field.node.talk.field_event_date.yml | 23 ++++ .../field.storage.node.field_event_date.yml | 20 ++++ config/default/views.view.talks.yml | 3 +- .../custom/custom/src/Entity/Node/Talk.php | 12 ++ ...field.field.node.talk.field_event_date.yml | 21 ++++ .../field.storage.node.field_event_date.yml | 20 ++++ .../config/install/views.view.talks.yml | 3 +- .../custom/tests/src/Kernel/TalksTestBase.php | 7 +- web/modules/custom/opd_talks/opd_talks.module | 4 +- .../custom/opd_talks/opd_talks.services.yml | 3 + .../opd_talks/src/Plugin/views/sort/Event.php | 46 ++++++-- .../opd_talks/src/Service/TalkDateUpdater.php | 71 ++++++++++++ .../tests/src/Kernel/TalkEventDateTest.php | 104 ++++++++++++++++++ .../tests/src/Kernel/TalksPageSortTest.php | 16 ++- 17 files changed, 340 insertions(+), 21 deletions(-) create mode 100644 config/default/field.field.node.talk.field_event_date.yml create mode 100644 config/default/field.storage.node.field_event_date.yml create mode 100644 web/modules/custom/custom/tests/modules/custom_test/config/install/field.field.node.talk.field_event_date.yml create mode 100644 web/modules/custom/custom/tests/modules/custom_test/config/install/field.storage.node.field_event_date.yml create mode 100644 web/modules/custom/opd_talks/src/Service/TalkDateUpdater.php create mode 100644 web/modules/custom/opd_talks/tests/src/Kernel/TalkEventDateTest.php diff --git a/config/default/core.entity_form_display.node.talk.default.yml b/config/default/core.entity_form_display.node.talk.default.yml index e10196f..859e3c5 100644 --- a/config/default/core.entity_form_display.node.talk.default.yml +++ b/config/default/core.entity_form_display.node.talk.default.yml @@ -4,6 +4,7 @@ status: true dependencies: config: - field.field.node.talk.body + - field.field.node.talk.field_event_date - field.field.node.talk.field_events - field.field.node.talk.field_excerpt - field.field.node.talk.field_slides @@ -143,4 +144,5 @@ content: region: content settings: { } third_party_settings: { } -hidden: { } +hidden: + field_event_date: true diff --git a/config/default/core.entity_view_display.node.talk.default.yml b/config/default/core.entity_view_display.node.talk.default.yml index 946a159..f35e433 100644 --- a/config/default/core.entity_view_display.node.talk.default.yml +++ b/config/default/core.entity_view_display.node.talk.default.yml @@ -4,6 +4,7 @@ status: true dependencies: config: - field.field.node.talk.body + - field.field.node.talk.field_event_date - field.field.node.talk.field_events - field.field.node.talk.field_excerpt - field.field.node.talk.field_slides @@ -66,4 +67,5 @@ content: settings: { } third_party_settings: { } hidden: + field_event_date: true field_excerpt: true diff --git a/config/default/core.entity_view_display.node.talk.teaser.yml b/config/default/core.entity_view_display.node.talk.teaser.yml index 6d3e96d..c3c6395 100644 --- a/config/default/core.entity_view_display.node.talk.teaser.yml +++ b/config/default/core.entity_view_display.node.talk.teaser.yml @@ -5,6 +5,7 @@ dependencies: config: - core.entity_view_mode.node.teaser - field.field.node.talk.body + - field.field.node.talk.field_event_date - field.field.node.talk.field_events - field.field.node.talk.field_excerpt - field.field.node.talk.field_slides @@ -32,6 +33,7 @@ content: third_party_settings: { } hidden: body: true + field_event_date: true field_events: true field_slides: true field_type: true diff --git a/config/default/field.field.node.talk.field_event_date.yml b/config/default/field.field.node.talk.field_event_date.yml new file mode 100644 index 0000000..e579bb1 --- /dev/null +++ b/config/default/field.field.node.talk.field_event_date.yml @@ -0,0 +1,23 @@ +uuid: 5c54e34a-4e53-4e70-b621-1d40953385cd +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_event_date + - node.type.talk +id: node.talk.field_event_date +field_name: field_event_date +entity_type: node +bundle: talk +label: 'Next event date' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + min: 0 + max: null + prefix: '' + suffix: '' +field_type: integer diff --git a/config/default/field.storage.node.field_event_date.yml b/config/default/field.storage.node.field_event_date.yml new file mode 100644 index 0000000..ca28857 --- /dev/null +++ b/config/default/field.storage.node.field_event_date.yml @@ -0,0 +1,20 @@ +uuid: 86aec221-e3cb-4da3-90b5-522b661c6313 +langcode: en +status: true +dependencies: + module: + - node +id: node.field_event_date +field_name: field_event_date +entity_type: node +type: integer +settings: + unsigned: false + size: normal +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/config/default/views.view.talks.yml b/config/default/views.view.talks.yml index 44397ff..4bd6586 100644 --- a/config/default/views.view.talks.yml +++ b/config/default/views.view.talks.yml @@ -144,7 +144,7 @@ display: sorts: event_sort: id: event_sort - table: node_field_data + table: node__field_event_date field: event_sort relationship: none group_type: group @@ -154,7 +154,6 @@ display: expose: label: '' granularity: second - entity_type: node plugin_id: event_sort title: Talks header: { } diff --git a/web/modules/custom/custom/src/Entity/Node/Talk.php b/web/modules/custom/custom/src/Entity/Node/Talk.php index 157789d..1eed5e1 100644 --- a/web/modules/custom/custom/src/Entity/Node/Talk.php +++ b/web/modules/custom/custom/src/Entity/Node/Talk.php @@ -30,6 +30,14 @@ class Talk extends Node implements ContentEntityBundleInterface { ->referencedEntities()); } + public function getNextDate(): ?int { + if ($this->get('field_event_date')->isEmpty()) { + return NULL; + } + + return (int) $this->get('field_event_date')->getString(); + } + /** * Find the date for the latest event. * @@ -42,4 +50,8 @@ class Talk extends Node implements ContentEntityBundleInterface { ->max(); } + public function setNextDate(int $date): void { + $this->set('field_event_date', $date); + } + } diff --git a/web/modules/custom/custom/tests/modules/custom_test/config/install/field.field.node.talk.field_event_date.yml b/web/modules/custom/custom/tests/modules/custom_test/config/install/field.field.node.talk.field_event_date.yml new file mode 100644 index 0000000..04a859b --- /dev/null +++ b/web/modules/custom/custom/tests/modules/custom_test/config/install/field.field.node.talk.field_event_date.yml @@ -0,0 +1,21 @@ +uuid: 5bb25694-3431-4c5e-9dc2-c7c46a91eab5 +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_event_date + - node.type.talk + module: + - datetime +id: node.talk.field_event_date +field_name: field_event_date +entity_type: node +bundle: talk +label: 'Next event date' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: datetime diff --git a/web/modules/custom/custom/tests/modules/custom_test/config/install/field.storage.node.field_event_date.yml b/web/modules/custom/custom/tests/modules/custom_test/config/install/field.storage.node.field_event_date.yml new file mode 100644 index 0000000..aa75ad9 --- /dev/null +++ b/web/modules/custom/custom/tests/modules/custom_test/config/install/field.storage.node.field_event_date.yml @@ -0,0 +1,20 @@ +uuid: e718e9e3-0765-4cf4-b7e8-cccf41ee3d1a +langcode: en +status: true +dependencies: + module: + - datetime + - node +id: node.field_event_date +field_name: field_event_date +entity_type: node +type: datetime +settings: + datetime_type: date +module: datetime +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/custom/tests/modules/custom_test/config/install/views.view.talks.yml b/web/modules/custom/custom/tests/modules/custom_test/config/install/views.view.talks.yml index e05fb2b..b7fb867 100644 --- a/web/modules/custom/custom/tests/modules/custom_test/config/install/views.view.talks.yml +++ b/web/modules/custom/custom/tests/modules/custom_test/config/install/views.view.talks.yml @@ -143,7 +143,7 @@ display: sorts: event_sort: id: event_sort - table: node_field_data + table: node__field_event_date field: event_sort relationship: none group_type: group @@ -153,7 +153,6 @@ display: expose: label: '' granularity: second - entity_type: node plugin_id: event_sort title: Talks header: { } diff --git a/web/modules/custom/custom/tests/src/Kernel/TalksTestBase.php b/web/modules/custom/custom/tests/src/Kernel/TalksTestBase.php index b272b29..62f67bc 100644 --- a/web/modules/custom/custom/tests/src/Kernel/TalksTestBase.php +++ b/web/modules/custom/custom/tests/src/Kernel/TalksTestBase.php @@ -12,10 +12,8 @@ use Drupal\paragraphs\ParagraphInterface; abstract class TalksTestBase extends EntityKernelTestBase { - protected $strictConfigSchema = FALSE; - /** - * {@inheritDoc} + * {@inheritdoc} */ public static $modules = [ // Core. @@ -32,8 +30,11 @@ abstract class TalksTestBase extends EntityKernelTestBase { // Custom. 'custom', 'custom_test', + 'opd_talks', ]; + protected $strictConfigSchema = FALSE; + protected function createEvent(array $overrides = []): ParagraphInterface { /** @var \Drupal\paragraphs\ParagraphInterface $event */ $event = Paragraph::create(array_merge([ diff --git a/web/modules/custom/opd_talks/opd_talks.module b/web/modules/custom/opd_talks/opd_talks.module index 9b237e7..b0268d1 100644 --- a/web/modules/custom/opd_talks/opd_talks.module +++ b/web/modules/custom/opd_talks/opd_talks.module @@ -11,12 +11,12 @@ declare(strict_types=1); * Implements hook_views_data_alter(). */ function opd_talks_views_data_alter(array &$data): void { - $data['node_field_data']['event_sort'] = [ + $data['node__field_event_date']['event_sort'] = [ 'title' => t('Custom event sort'), 'group' => t('Content'), 'help' => t('Sort events by past/future, then distance from now.'), 'sort' => [ - 'field' => 'created', + 'field' => 'field_event_date_value', 'id' => 'event_sort', ] ]; diff --git a/web/modules/custom/opd_talks/opd_talks.services.yml b/web/modules/custom/opd_talks/opd_talks.services.yml index c8aebb7..3b6a5cf 100644 --- a/web/modules/custom/opd_talks/opd_talks.services.yml +++ b/web/modules/custom/opd_talks/opd_talks.services.yml @@ -1,3 +1,6 @@ services: Drupal\opd_talks\Repository\TalkRepository: autowire: true + + Drupal\opd_talks\Service\TalkDateUpdater: + autowire: true diff --git a/web/modules/custom/opd_talks/src/Plugin/views/sort/Event.php b/web/modules/custom/opd_talks/src/Plugin/views/sort/Event.php index b9b21a6..48ad8b5 100644 --- a/web/modules/custom/opd_talks/src/Plugin/views/sort/Event.php +++ b/web/modules/custom/opd_talks/src/Plugin/views/sort/Event.php @@ -4,24 +4,55 @@ declare(strict_types=1); namespace Drupal\opd_talks\Plugin\views\sort; -use Drupal\views\Plugin\views\sort\Date; +use Carbon\Carbon; +use Drupal\Component\Datetime\TimeInterface; use Drupal\views\Annotation\ViewsSort; +use Drupal\views\Plugin\views\sort\Date; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * @ViewsSort("event_sort") */ final class Event extends Date { + private TimeInterface $time; + + public function __construct( + array $configuration, + $pluginId, + $pluginDefinition, + TimeInterface $time + ) { + parent::__construct($configuration, $pluginId, $pluginDefinition); + + $this->time = $time; + } + + public static function create( + ContainerInterface $container, + array $configuration, + $pluginId, + $pluginDefinition + ) { + return new static( + $configuration, + $pluginId, + $pluginDefinition, + $container->get('datetime.time') + ); + } + public function query() { $this->ensureMyTable(); - $currentTime = time(); + $currentDate = Carbon::parse('today')->getTimestamp(); + $dateAlias = "$this->tableAlias.$this->realField"; // Is this event in the past? $this->query->addOrderBy( NULL, - sprintf("%d > %s", $currentTime, $dateAlias), + sprintf("%d > %s", $currentDate, $dateAlias), $this->options['order'], "in_past" ); @@ -29,10 +60,11 @@ final class Event extends Date { // How far in the past/future is this event? $this->query->addOrderBy( NULL, - sprintf('ABS(%s - %d)', $dateAlias, $currentTime), - $this->options['order'], - "distance_from_now" - ); + sprintf('ABS(%s - %d)', $dateAlias, $currentDate), + $this->options['order'], + "distance_from_now" + ); } + } diff --git a/web/modules/custom/opd_talks/src/Service/TalkDateUpdater.php b/web/modules/custom/opd_talks/src/Service/TalkDateUpdater.php new file mode 100644 index 0000000..afddb44 --- /dev/null +++ b/web/modules/custom/opd_talks/src/Service/TalkDateUpdater.php @@ -0,0 +1,71 @@ +talkRepository = $talkRepository; + $this->time = $time; + } + + public function __invoke(): void { + foreach ($this->talkRepository->getAll() as $talk) { + $this->updateNextEventDate($talk); + } + } + + private function updateNextEventDate(Talk $talk) { + if (!$nextDate = $this->findNextEventDate($talk)) { + return; + } + + $nextDateTimestamp = Carbon::parse($nextDate) + ->getTimestamp(); + + if ($nextDateTimestamp == $talk->getNextDate()) { + return; + } + + $talk->setNextDate($nextDateTimestamp); + $talk->save(); + } + + private function findNextEventDate(Talk $talk): ?string { + $currentTime = Carbon::today() + ->format(DateTimeItemInterface::DATE_STORAGE_FORMAT); + + $dates = $talk->getEvents() + ->map(fn(ParagraphInterface $event) => $event->get('field_date') + ->getString()) + ->sort(); + + if ($dates->isEmpty()) { + return NULL; + } + + // If a future date is found, return it. + if ($futureDate = $dates->first(fn(string $eventDate) => $eventDate > $currentTime)) { + return $futureDate; + } + + // If no future date is found, return the last past date. + return $dates->last(); + } + +} diff --git a/web/modules/custom/opd_talks/tests/src/Kernel/TalkEventDateTest.php b/web/modules/custom/opd_talks/tests/src/Kernel/TalkEventDateTest.php new file mode 100644 index 0000000..4fe674d --- /dev/null +++ b/web/modules/custom/opd_talks/tests/src/Kernel/TalkEventDateTest.php @@ -0,0 +1,104 @@ +createTalk([ + 'field_event_date' => NULL, + 'field_events' => [ + $this->createEvent([ + 'field_date' => Carbon::today() + ->subWeeks(2) + ->format($dateFormat), + ]), + $this->createEvent([ + 'field_date' => Carbon::today() + ->subDays(2) + ->format($dateFormat), + ]), + $this->createEvent([ + 'field_date' => Carbon::today() + ->addDays(4) + ->format($dateFormat), + ]), + $this->createEvent([ + 'field_date' => Carbon::today() + ->addDays(10) + ->format($dateFormat), + ]), + ], + ]); + + $dateUpdater = $this->container->get(TalkDateUpdater::class); + $dateUpdater->__invoke(); + + $expected = Carbon::today()->addDays(4)->getTimestamp(); + + $talk = Node::load($talk->id()); + $this->assertNextEventDateIs($talk, $expected); + } + + /** @test */ + public function talk_event_dates_are_set_to_the_last_past_date(): void { + $dateFormat = DateTimeItemInterface::DATE_STORAGE_FORMAT; + + $talk = $this->createTalk([ + 'field_event_date' => NULL, + 'field_events' => [ + $this->createEvent([ + 'field_date' => Carbon::today() + ->subDays(4) + ->format($dateFormat), + ]), + $this->createEvent([ + 'field_date' => Carbon::today() + ->subDays(2) + ->format($dateFormat), + ]), + ], + ]); + + $dateUpdater = $this->container->get(TalkDateUpdater::class); + $dateUpdater->__invoke(); + + $expected = Carbon::today()->subDays(2)->getTimestamp(); + + $talk = Node::load($talk->id()); + $this->assertNextEventDateIs($talk, $expected); + } + + /** @test */ + public function next_event_date_is_empty_if_there_are_no_events(): void { + $talk = $this->createTalk([ + 'field_event_date' => NULL, + 'field_events' => [], + ]); + + $dateUpdater = $this->container->get(TalkDateUpdater::class); + $dateUpdater->__invoke(); + + $talk = Node::load($talk->id()); + $this->assertNoNextEventDate($talk); + } + + private function assertNextEventDateIs(Talk $talk, $expected): void { + $this->assertSame($expected, $talk->getNextDate()); + } + + private function assertNoNextEventDate(Talk $talk): void { + $this->assertNull($talk->getNextDate()); + } + +} diff --git a/web/modules/custom/opd_talks/tests/src/Kernel/TalksPageSortTest.php b/web/modules/custom/opd_talks/tests/src/Kernel/TalksPageSortTest.php index d86aa39..4b4d59d 100644 --- a/web/modules/custom/opd_talks/tests/src/Kernel/TalksPageSortTest.php +++ b/web/modules/custom/opd_talks/tests/src/Kernel/TalksPageSortTest.php @@ -20,10 +20,18 @@ final class TalksPageSortTest extends TalksTestBase { * @test */ public function upcoming_talks_are_shown_first_followed_by_past_talks_and_ordered_by_distance() { - $this->createTalk(['created' => Carbon::parse('+4 days')->getTimestamp()]); - $this->createTalk(['created' => Carbon::parse('-2 days')->getTimestamp()]); - $this->createTalk(['created' => Carbon::parse('+1 days')->getTimestamp()]); - $this->createTalk(['created' => Carbon::parse('-10 days')->getTimestamp()]); + $this->createTalk([ + 'field_event_date' => Carbon::today()->addDays(4)->getTimestamp(), + ]); + $this->createTalk([ + 'field_event_date' => Carbon::today()->subDays(2)->getTimestamp(), + ]); + $this->createTalk([ + 'field_event_date' => Carbon::today()->addDay()->getTimestamp(), + ]); + $this->createTalk([ + 'field_event_date' => Carbon::today()->subDays(10)->getTimestamp(), + ]); $talkIds = (new Collection(views_get_view_result('talks'))) ->map(fn(ResultRow $row) => (int) $row->_entity->id());