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
This commit is contained in:
Oliver Davies 2020-08-24 02:00:22 +01:00
parent bdf225b05d
commit 6d9ecd8df0
17 changed files with 340 additions and 21 deletions

View file

@ -4,6 +4,7 @@ status: true
dependencies: dependencies:
config: config:
- field.field.node.talk.body - field.field.node.talk.body
- field.field.node.talk.field_event_date
- field.field.node.talk.field_events - field.field.node.talk.field_events
- field.field.node.talk.field_excerpt - field.field.node.talk.field_excerpt
- field.field.node.talk.field_slides - field.field.node.talk.field_slides
@ -143,4 +144,5 @@ content:
region: content region: content
settings: { } settings: { }
third_party_settings: { } third_party_settings: { }
hidden: { } hidden:
field_event_date: true

View file

@ -4,6 +4,7 @@ status: true
dependencies: dependencies:
config: config:
- field.field.node.talk.body - field.field.node.talk.body
- field.field.node.talk.field_event_date
- field.field.node.talk.field_events - field.field.node.talk.field_events
- field.field.node.talk.field_excerpt - field.field.node.talk.field_excerpt
- field.field.node.talk.field_slides - field.field.node.talk.field_slides
@ -66,4 +67,5 @@ content:
settings: { } settings: { }
third_party_settings: { } third_party_settings: { }
hidden: hidden:
field_event_date: true
field_excerpt: true field_excerpt: true

View file

@ -5,6 +5,7 @@ dependencies:
config: config:
- core.entity_view_mode.node.teaser - core.entity_view_mode.node.teaser
- field.field.node.talk.body - field.field.node.talk.body
- field.field.node.talk.field_event_date
- field.field.node.talk.field_events - field.field.node.talk.field_events
- field.field.node.talk.field_excerpt - field.field.node.talk.field_excerpt
- field.field.node.talk.field_slides - field.field.node.talk.field_slides
@ -32,6 +33,7 @@ content:
third_party_settings: { } third_party_settings: { }
hidden: hidden:
body: true body: true
field_event_date: true
field_events: true field_events: true
field_slides: true field_slides: true
field_type: true field_type: true

View file

@ -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

View file

@ -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

View file

@ -144,7 +144,7 @@ display:
sorts: sorts:
event_sort: event_sort:
id: event_sort id: event_sort
table: node_field_data table: node__field_event_date
field: event_sort field: event_sort
relationship: none relationship: none
group_type: group group_type: group
@ -154,7 +154,6 @@ display:
expose: expose:
label: '' label: ''
granularity: second granularity: second
entity_type: node
plugin_id: event_sort plugin_id: event_sort
title: Talks title: Talks
header: { } header: { }

View file

@ -30,6 +30,14 @@ class Talk extends Node implements ContentEntityBundleInterface {
->referencedEntities()); ->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. * Find the date for the latest event.
* *
@ -42,4 +50,8 @@ class Talk extends Node implements ContentEntityBundleInterface {
->max(); ->max();
} }
public function setNextDate(int $date): void {
$this->set('field_event_date', $date);
}
} }

View file

@ -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

View file

@ -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

View file

@ -143,7 +143,7 @@ display:
sorts: sorts:
event_sort: event_sort:
id: event_sort id: event_sort
table: node_field_data table: node__field_event_date
field: event_sort field: event_sort
relationship: none relationship: none
group_type: group group_type: group
@ -153,7 +153,6 @@ display:
expose: expose:
label: '' label: ''
granularity: second granularity: second
entity_type: node
plugin_id: event_sort plugin_id: event_sort
title: Talks title: Talks
header: { } header: { }

View file

@ -12,10 +12,8 @@ use Drupal\paragraphs\ParagraphInterface;
abstract class TalksTestBase extends EntityKernelTestBase { abstract class TalksTestBase extends EntityKernelTestBase {
protected $strictConfigSchema = FALSE;
/** /**
* {@inheritDoc} * {@inheritdoc}
*/ */
public static $modules = [ public static $modules = [
// Core. // Core.
@ -32,8 +30,11 @@ abstract class TalksTestBase extends EntityKernelTestBase {
// Custom. // Custom.
'custom', 'custom',
'custom_test', 'custom_test',
'opd_talks',
]; ];
protected $strictConfigSchema = FALSE;
protected function createEvent(array $overrides = []): ParagraphInterface { protected function createEvent(array $overrides = []): ParagraphInterface {
/** @var \Drupal\paragraphs\ParagraphInterface $event */ /** @var \Drupal\paragraphs\ParagraphInterface $event */
$event = Paragraph::create(array_merge([ $event = Paragraph::create(array_merge([

View file

@ -11,12 +11,12 @@ declare(strict_types=1);
* Implements hook_views_data_alter(). * Implements hook_views_data_alter().
*/ */
function opd_talks_views_data_alter(array &$data): void { 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'), 'title' => t('Custom event sort'),
'group' => t('Content'), 'group' => t('Content'),
'help' => t('Sort events by past/future, then distance from now.'), 'help' => t('Sort events by past/future, then distance from now.'),
'sort' => [ 'sort' => [
'field' => 'created', 'field' => 'field_event_date_value',
'id' => 'event_sort', 'id' => 'event_sort',
] ]
]; ];

View file

@ -1,3 +1,6 @@
services: services:
Drupal\opd_talks\Repository\TalkRepository: Drupal\opd_talks\Repository\TalkRepository:
autowire: true autowire: true
Drupal\opd_talks\Service\TalkDateUpdater:
autowire: true

View file

@ -4,24 +4,55 @@ declare(strict_types=1);
namespace Drupal\opd_talks\Plugin\views\sort; 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\Annotation\ViewsSort;
use Drupal\views\Plugin\views\sort\Date;
use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* @ViewsSort("event_sort") * @ViewsSort("event_sort")
*/ */
final class Event extends Date { 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() { public function query() {
$this->ensureMyTable(); $this->ensureMyTable();
$currentTime = time(); $currentDate = Carbon::parse('today')->getTimestamp();
$dateAlias = "$this->tableAlias.$this->realField"; $dateAlias = "$this->tableAlias.$this->realField";
// Is this event in the past? // Is this event in the past?
$this->query->addOrderBy( $this->query->addOrderBy(
NULL, NULL,
sprintf("%d > %s", $currentTime, $dateAlias), sprintf("%d > %s", $currentDate, $dateAlias),
$this->options['order'], $this->options['order'],
"in_past" "in_past"
); );
@ -29,10 +60,11 @@ final class Event extends Date {
// How far in the past/future is this event? // How far in the past/future is this event?
$this->query->addOrderBy( $this->query->addOrderBy(
NULL, NULL,
sprintf('ABS(%s - %d)', $dateAlias, $currentTime), sprintf('ABS(%s - %d)', $dateAlias, $currentDate),
$this->options['order'], $this->options['order'],
"distance_from_now" "distance_from_now"
); );
} }
} }

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\opd_talks\Service;
use Carbon\Carbon;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\custom\Entity\Node\Talk;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\opd_talks\Repository\TalkRepository;
use Drupal\paragraphs\ParagraphInterface;
final class TalkDateUpdater {
private TalkRepository $talkRepository;
private TimeInterface $time;
public function __construct(
TalkRepository $talkRepository,
TimeInterface $time
) {
$this->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();
}
}

View file

@ -0,0 +1,104 @@
<?php
namespace Drupal\Tests\opd_talks\Kernel;
use Carbon\Carbon;
use Drupal\custom\Entity\Node\Talk;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\node\Entity\Node;
use Drupal\opd_talks\Service\TalkDateUpdater;
use Drupal\Tests\custom\Kernel\TalksTestBase;
final class TalkEventDateTest extends TalksTestBase {
/** @test */
public function talk_event_dates_are_set_to_the_next_future_date(): void {
$dateFormat = DateTimeItemInterface::DATE_STORAGE_FORMAT;
$talk = $this->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());
}
}

View file

@ -20,10 +20,18 @@ final class TalksPageSortTest extends TalksTestBase {
* @test * @test
*/ */
public function upcoming_talks_are_shown_first_followed_by_past_talks_and_ordered_by_distance() { 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([
$this->createTalk(['created' => Carbon::parse('-2 days')->getTimestamp()]); 'field_event_date' => Carbon::today()->addDays(4)->getTimestamp(),
$this->createTalk(['created' => Carbon::parse('+1 days')->getTimestamp()]); ]);
$this->createTalk(['created' => Carbon::parse('-10 days')->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'))) $talkIds = (new Collection(views_get_view_result('talks')))
->map(fn(ResultRow $row) => (int) $row->_entity->id()); ->map(fn(ResultRow $row) => (int) $row->_entity->id());