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

View file

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

View file

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

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:
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: { }

View file

@ -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);
}
}

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:
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: { }

View file

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

View file

@ -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',
]
];

View file

@ -1,3 +1,6 @@
services:
Drupal\opd_talks\Repository\TalkRepository:
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;
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"
);
}
}

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
*/
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());