Re-add opdavies_talks module

This commit is contained in:
Oliver Davies 2024-04-11 18:00:32 +01:00
parent c4f976fcf7
commit 01398280c7
34 changed files with 7411 additions and 3 deletions

View file

@ -21,6 +21,7 @@
"drupal/core-composer-scaffold": "^10.2",
"drupal/core-project-message": "^10.2",
"drupal/core-recommended": "^10.2",
"drupal/hook_event_dispatcher": "^4.0",
"drupal/inline_entity_form": "^3.0@RC",
"drupal/layout_builder_modal": "^1.2",
"drupal/markdown": "^3.0",
@ -32,7 +33,9 @@
"drupal/speakerdeck_field": "^2.0",
"drupal/video_embed_field": "^2.5",
"drush/drush": "^12.5",
"league/commonmark": "^1.0"
"illuminate/collections": "^11.3",
"league/commonmark": "^1.0",
"nesbot/carbon": "^3.2"
},
"conflict": {
"drupal/drupal": "*"
@ -112,5 +115,8 @@
"SubformState incorrect interface error": "./tools/patches/drupal/markdown/3409277-29.diff"
}
}
},
"require-dev": {
"drupal/core-dev": "^10.2"
}
}

5933
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@ module:
config: 0
contact: 0
contextual: 0
core_event_dispatcher: 0
datetime: 0
dblog: 0
dynamic_page_cache: 0
@ -25,6 +26,7 @@ module:
filter: 0
help: 0
history: 0
hook_event_dispatcher: 0
image: 0
inline_entity_form: 0
layout_builder: 0
@ -36,6 +38,7 @@ module:
menu_ui: 0
mysql: 0
node: 0
opdavies_talks: 0
options: 0
page_cache: 0
path: 0

View file

@ -0,0 +1,5 @@
name: opdavies Talks
description: Custom code for talks pages.
type: module
core_version_requirement: ^10
package: Custom

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Install, update and uninstall functions for opdavies_talks.
*/
declare(strict_types=1);
use Drupal\opdavies_talks\Repository\TalkRepository;
/**
* Set talk type for all existing talks.
*/
function opdavies_talks_update_8001(): void {
$talkRepository = \Drupal::service(TalkRepository::class);
foreach ($talkRepository->getAll() as $talk) {
$talk->set('field_type', 'talk');
$talk->save();
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Custom code for talks pages.
*/
declare(strict_types=1);
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\opdavies_talks\Service\TalkCounter;
use Drupal\opdavies_talks\Service\TalkDateUpdater;
/**
* Implements hook_cron().
*/
function opdavies_talks_cron(): void {
$dateUpdater = Drupal::service(TalkDateUpdater::class);
$dateUpdater->__invoke();
}
/**
* Implements hook_views_data_alter().
*/
function opdavies_talks_views_data_alter(array &$data): void {
$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' => 'field_event_date_value',
'id' => 'event_sort',
],
];
}
/**
* Implements hook_token_info().
*/
function opdavies_talks_token_info(): array {
$info = [];
$info['types']['opdavies_talks'] = [
'name' => t('Oliver Davies Talks'),
'description' => t('Custom tokens for the Oliver Davies Talks module.'),
];
$info['tokens']['opdavies_talks']['talk_count'] = 'ddd';
return $info;
}
/**
* Implements hook_tokens().
*/
function opdavies_talks_tokens(string $type, array $tokens, array $data, array $options, BubbleableMetadata $bubbleableMetadata): array {
$replacements = [];
if ($type == 'opdavies_talks') {
/** @var TalkCounter $talkCounter */
$talkCounter = Drupal::service(TalkCounter::class);
foreach ($tokens as $name => $original) {
switch ($name) {
case 'talk_count':
$replacements[$original] = $talkCounter->getCount();
break;
}
}
}
return $replacements;
}

View file

@ -0,0 +1,13 @@
services:
Drupal\opdavies_talks\Repository\TalkRepository:
autowire: true
Drupal\opdavies_talks\Service\TalkCounter:
autowire: true
Drupal\opdavies_talks\Service\TalkDateUpdater:
autowire: true
Drupal\opdavies_talks\EventSubscriber\UpdateTalkNodeBeforeSave:
tags:
- { name: event_subscriber }

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\Collection;
use Drupal\node\NodeInterface;
use Drupal\opdavies_talks\Entity\Node\Talk;
use Drupal\paragraphs\ParagraphInterface;
use Illuminate\Support\Collection;
final class TalkCollection extends Collection {
/**
* Return the events for the talks in the Collection.
*
* @return Collection|ParagraphInterface[]
*/
public function getEvents(): Collection {
return $this
->flatMap(fn(Talk $talk): Collection => $talk->getEvents());
}
}

View file

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\Entity\Node;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\paragraphs\ParagraphInterface;
use Illuminate\Support\Collection;
final class Talk {
public const FIELD_EVENTS = 'field_events';
public const FIELD_EVENT_DATE = 'field_event_date';
private NodeInterface $node;
public function __construct(EntityInterface $node) {
$this->node = $node;
}
public function addEvent(ParagraphInterface $event): void {
$this->set(
self::FIELD_EVENTS,
$this->getEvents()->push($event)->toArray()
);
}
/**
* Find the date for the latest event.
*
* @return string|null
*/
public function findLatestEventDate(): ?string {
return $this->getEvents()
->map(fn(ParagraphInterface $event) => $event->get('field_date')
->getString())
->max();
}
public function get(string $name): FieldItemListInterface {
return $this->node->get($name);
}
public function getCreatedTime(): int {
return (int) $this->node->getCreatedTime();
}
public function getEvents(): Collection {
return Collection::make($this->get(self::FIELD_EVENTS)
->referencedEntities());
}
public function getNextDate(): ?int {
if ($this->get(self::FIELD_EVENT_DATE)->isEmpty()) {
return NULL;
}
return (int) $this->get(self::FIELD_EVENT_DATE)->getString();
}
public function id(): int {
return (int) $this->node->id();
}
public function label(): string {
return $this->node->label();
}
public function save(): void {
$this->node->save();
}
public function set(string $name, $value): void {
$this->node->set($name, $value);
}
public function setCreatedTime(int $timestamp): void {
$this->node->setCreatedTime($timestamp);
}
public function setEvents(array $events): void {
$this->set(self::FIELD_EVENTS, $events);
}
public function setNextDate(int $date): void {
$this->set(self::FIELD_EVENT_DATE, $date);
}
public static function createFromNode(EntityInterface $node): self {
// TODO: ensure that this is a node and a `talk` type.
return new self($node);
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\EventSubscriber;
use Carbon\Carbon;
use Drupal\core_event_dispatcher\EntityHookEvents;
use Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent;
use Drupal\opdavies_talks\Entity\Node\Talk;
use Drupal\paragraphs\ParagraphInterface;
use Illuminate\Support\Collection;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Update a talk node before it's saved.
*/
final class UpdateTalkNodeBeforeSave implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [
EntityHookEvents::ENTITY_PRE_SAVE => 'onEntityPreSave',
];
}
public function onEntityPreSave(AbstractEntityEvent $event): void {
if ($event->getEntity()->getEntityTypeId() != 'node') {
return;
}
if ($event->getEntity()->bundle() != 'talk') {
return;
}
$node = $event->getEntity();
$talk = Talk::createFromNode($node);
$this->reorderEvents($talk);
$this->updateCreatedDate($talk);
}
private function reorderEvents(Talk $talk): void {
$events = $talk->getEvents();
$eventsByDate = $this->sortEventsByDate($events);
// If the original event IDs don't match the sorted event IDs, update the event field to use the sorted ones.
if ($events->map->id() != $eventsByDate->map->id()) {
$talk->setEvents($eventsByDate->toArray());
}
}
private function sortEventsByDate(Collection $events): Collection {
return $events
->sortBy(fn(ParagraphInterface $event) => $event->get('field_date')
->getString())
->values();
}
private function updateCreatedDate(Talk $talk): void {
if (!$eventDate = $talk->findLatestEventDate()) {
return;
}
$talkDate = Carbon::parse($eventDate)->getTimestamp();
if ($talkDate == $talk->getCreatedTime()) {
return;
}
$talk->setCreatedTime($talkDate);
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Finder\Finder;
final class OpdaviesTalksServiceProvider implements ServiceProviderInterface {
public function register(ContainerBuilder $container): void {
foreach (['EventSubscriber', 'Repository', 'Service'] as $directory) {
$files = Finder::create()
->in(__DIR__ . '/' . $directory)
->files()
->name('*.php');
foreach ($files as $file) {
$class = 'Drupal\opdavies_talks\\' . $directory . '\\' .
str_replace('/', '\\', substr($file->getRelativePathname(), 0, -4));
if ($container->hasDefinition($class)) {
continue;
}
$definition = new Definition($class);
$definition->setAutowired(TRUE);
if ($directory == 'EventSubscriber') {
$definition->addTag('event_subscriber');
}
$container->setDefinition($class, $definition);
}
}
}
}

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\Plugin\views\sort;
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,
string $pluginId,
array $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(): void {
$this->ensureMyTable();
$currentDate = Carbon::parse('today')->getTimestamp();
$dateAlias = "$this->tableAlias.$this->realField";
// Is this event in the past?
$this->query->addOrderBy(
NULL,
sprintf("%d > %s", $currentDate, $dateAlias),
$this->options['order'],
"in_past"
);
// How far in the past/future is this event?
$this->query->addOrderBy(
NULL,
sprintf('ABS(%s - %d)', $dateAlias, $currentDate),
$this->options['order'],
"distance_from_now"
);
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\Repository;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\node\NodeInterface;
use Drupal\opdavies_talks\Collection\TalkCollection;
use Drupal\opdavies_talks\Entity\Node\Talk;
final class TalkRepository {
private EntityStorageInterface $nodeStorage;
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
$this->nodeStorage = $entityTypeManager->getStorage('node');
}
public function findAll(): TalkCollection {
$talks = $this->nodeStorage->loadByProperties($this->defaultProperties());
return (new TalkCollection($talks))
->map(fn(NodeInterface $node): Talk => Talk::createFromNode($node));
}
public function findAllPublished(): TalkCollection {
$talks = $this->nodeStorage->loadByProperties(array_merge(
$this->defaultProperties(),
[
'status' => NodeInterface::PUBLISHED,
],
));
return (new TalkCollection($talks))
->map(fn(NodeInterface $node): Talk => Talk::createFromNode($node));
}
private function defaultProperties(): array {
return [
'type' => 'talk',
];
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\Service;
use Carbon\Carbon;
use Drupal\opdavies_talks\Repository\TalkRepository;
use Drupal\paragraphs\ParagraphInterface;
final class TalkCounter {
private TalkRepository $talkRepository;
public function __construct(TalkRepository $talkRepository) {
$this->talkRepository = $talkRepository;
}
public function getCount(): int {
$today = Carbon::today()->format('Y-m-d H:i:s');
return $this->talkRepository
->findAllPublished()
->getEvents()
->filter(fn(ParagraphInterface $event) => $event->get('field_date')
->getString() <= $today)
->count();
}
}

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\opdavies_talks\Service;
use Carbon\Carbon;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\opdavies_talks\Entity\Node\Talk;
use Drupal\opdavies_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->findAll() as $talk) {
$this->updateNextEventDate($talk);
}
}
private function updateNextEventDate(Talk $talk): void {
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,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,30 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_events
- node.type.talk
- paragraphs.paragraphs_type.event
module:
- entity_reference_revisions
id: node.talk.field_events
field_name: field_events
entity_type: node
bundle: talk
label: Events
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:paragraph'
handler_settings:
negate: 0
target_bundles:
event: event
target_bundles_drag_drop:
event:
enabled: true
weight: 2
field_type: entity_reference_revisions

View file

@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
config:
- field.storage.paragraph.field_date
- paragraphs.paragraphs_type.event
module:
- datetime
id: paragraph.event.field_date
field_name: field_date
entity_type: paragraph
bundle: event
label: Date
description: ''
required: true
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: datetime

View file

@ -0,0 +1,18 @@
langcode: en
status: true
dependencies:
config:
- field.storage.paragraph.field_name
- paragraphs.paragraphs_type.event
id: paragraph.event.field_name
field_name: field_name
entity_type: paragraph
bundle: event
label: 'Event name'
description: ''
required: true
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string

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

@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- entity_reference_revisions
- node
- paragraphs
id: node.field_events
field_name: field_events
entity_type: node
type: entity_reference_revisions
settings:
target_type: paragraph
module: entity_reference_revisions
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- datetime
- paragraphs
id: paragraph.field_date
field_name: field_date
entity_type: paragraph
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

@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- paragraphs
id: paragraph.field_name
field_name: field_name
entity_type: paragraph
type: string
settings:
max_length: 255
is_ascii: false
case_sensitive: false
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false

View file

@ -0,0 +1,10 @@
langcode: en
status: true
dependencies: { }
name: Talk
type: talk
description: ''
help: ''
new_revision: true
preview_mode: 1
display_submitted: false

View file

@ -0,0 +1,9 @@
langcode: en
status: true
dependencies: { }
id: event
label: Event
icon_uuid: null
icon_default: null
description: ''
behavior_plugins: { }

View file

@ -0,0 +1,196 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- node.type.talk
- system.menu.main
module:
- node
- opdavies_talks
- user
id: talks
label: Talks
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: 0
offset: 0
style:
type: html_list
options:
row_class: ''
default_row_class: true
uses_fields: false
type: ul
wrapper_class: ''
class: space-y-8
row:
type: 'entity:node'
options:
view_mode: teaser
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
operator_limit_selection: false
operator_list: { }
group: 1
type:
id: type
table: node_field_data
field: type
value:
talk: talk
entity_type: node
entity_field: type
plugin_id: bundle
expose:
operator_limit_selection: false
operator_list: { }
sorts:
event_sort:
id: event_sort
table: node__field_event_date
field: event_sort
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
granularity: second
plugin_id: event_sort
title: Talks
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: talks
menu:
type: normal
title: Talks
description: ''
expanded: false
parent: ''
weight: -48
context: '0'
menu_name: main
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }

View file

@ -0,0 +1,4 @@
name: Custom Test
type: module
core_version_requirement: ^8 || ^9
hidden: true

View file

@ -0,0 +1,66 @@
<?php
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
namespace Drupal\Tests\opdavies_talks\Kernel;
use Carbon\Carbon;
use Drupal\node\NodeInterface;
use Drupal\opdavies_talks\Service\TalkCounter;
use PHPUnit\Framework\Assert;
final class CountPreviousTalksTest extends TalksTestBase {
private TalkCounter $talkCounter;
/** @test */
public function previous_talks_are_counted(): void {
$this->createTalk([
'field_events' => [
$this->createEvent(),
$this->createEvent(),
],
]);
$this->createTalk([
'field_events' => [
$this->createEvent(),
],
]);
Assert::assertSame(3, $this->talkCounter->getCount());
}
/** @test */
public function future_talks_are_not_counted(): void {
$this->createTalk([
'field_events' => [
$this->createEvent([
'field_date' => Carbon::now()->subDay(),
]),
$this->createEvent([
'field_date' => Carbon::now()->addDay(),
]),
],
]);
Assert::assertSame(1, $this->talkCounter->getCount());
}
/** @test */
public function unpublished_talks_are_not_counted(): void {
$this->createTalk([
'field_events' => [$this->createEvent()],
'status' => NodeInterface::NOT_PUBLISHED,
]);
Assert::assertSame(0, $this->talkCounter->getCount());
}
protected function setUp(): void {
parent::setUp();
$this->talkCounter = $this->container->get(TalkCounter::class);
}
}

View file

@ -0,0 +1,115 @@
<?php
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
namespace Drupal\Tests\opdavies_talks\Kernel;
use Carbon\Carbon;
use Drupal\paragraphs\ParagraphInterface;
final class ReorderEventsTest extends TalksTestBase {
/** @test */
public function the_events_are_ordered_by_date_when_a_talk_is_created(): void {
$events = [
$this->createEvent([
'field_date' => Carbon::today()->addWeeks(2),
'field_name' => 'Drupal Bristol',
]),
$this->createEvent([
'field_date' => Carbon::yesterday(),
'field_name' => 'DrupalCamp London',
]),
$this->createEvent([
'field_date' => Carbon::tomorrow(),
'field_name' => 'PHP UK conference',
]),
$this->createEvent([
'field_date' => Carbon::today()->addMonths(3),
'field_name' => 'CMS Philly',
]),
$this->createEvent([
'field_date' => Carbon::today()->subYear(),
'field_name' => 'PHP South Wales',
]),
];
$talk = $this->createTalk([
'field_events' => $events,
]);
$this->assertSame(
[
'PHP South Wales',
'DrupalCamp London',
'PHP UK conference',
'Drupal Bristol',
'CMS Philly',
],
$talk->getEvents()
->map(fn(ParagraphInterface $event) => $event->get('field_name')
->getString())
->toArray()
);
}
/** @test */
public function the_events_are_ordered_by_date_when_a_talk_is_updated(): void {
$events = [
$this->createEvent([
'field_date' => Carbon::today()->addWeeks(2),
'field_name' => 'Drupal Bristol',
]),
$this->createEvent([
'field_date' => Carbon::yesterday(),
'field_name' => 'DrupalCamp London',
]),
$this->createEvent([
'field_date' => Carbon::today()->addMonths(3),
'field_name' => 'CMS Philly',
]),
$this->createEvent([
'field_date' => Carbon::today()->subYear(),
'field_name' => 'PHP South Wales',
]),
];
$talk = $this->createTalk([
'field_events' => $events,
]);
$this->assertSame(
[
'PHP South Wales',
'DrupalCamp London',
'Drupal Bristol',
'CMS Philly',
],
$talk->getEvents()
->map(fn(ParagraphInterface $event) => $event->get('field_name')
->getString())
->toArray()
);
$talk->addEvent($this->createEvent([
'field_date' => Carbon::tomorrow(),
'field_name' => 'PHP UK conference',
]));
$talk->save();
$this->assertSame(
[
'PHP South Wales',
'DrupalCamp London',
'PHP UK conference',
'Drupal Bristol',
'CMS Philly',
],
$talk->getEvents()
->map(fn(ParagraphInterface $event) => $event->get('field_name')
->getString())
->toArray()
);
}
}

View file

@ -0,0 +1,73 @@
<?php
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
namespace Drupal\Tests\opdavies_talks\Kernel\Repository;
use Drupal\node\NodeInterface;
use Drupal\opdavies_talks\Entity\Node\Talk;
use Drupal\opdavies_talks\Repository\TalkRepository;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\opdavies_talks\Kernel\TalksTestBase;
final class TalkRepositoryTest extends TalksTestBase {
use NodeCreationTrait;
private TalkRepository $talkRepository;
/** @test */
public function get_all_talks(): void {
$this->createTalk(['title' => 'TDD - Test Driven Drupal']);
$this->createTalk(['title' => 'Taking Flight with Tailwind CSS']);
$this->createTalk(['title' => 'Upgrading to Drupal 9']);
$talks = $this->talkRepository->findAll();
$this->assertCount(3, $talks);
$this->assertSame(
[
1 => 'TDD - Test Driven Drupal',
2 => 'Taking Flight with Tailwind CSS',
3 => 'Upgrading to Drupal 9',
],
$talks->map(fn(Talk $talk) => $talk->label())->toArray()
);
}
/** @test */
public function get_all_published_talks(): void {
$this->createTalk([
'title' => 'TDD - Test Driven Drupal',
'status' => NodeInterface::PUBLISHED,
]);
$this->createTalk([
'title' => 'Taking Flight with Tailwind CSS',
'status' => NodeInterface::NOT_PUBLISHED,
]);
$talks = $this->talkRepository->findAllPublished();
$this->assertCount(1, $talks);
$this->assertSame('TDD - Test Driven Drupal', $talks->first()->label());
}
/** @test */
public function it_only_returns_talk_nodes(): void {
$this->createNode(['type' => 'page']);
$talks = $this->talkRepository->findAll();
$this->assertEmpty($talks);
}
protected function setUp(): void {
parent::setUp();
$this->installConfig(['filter']);
$this->talkRepository = $this->container->get(TalkRepository::class);
}
}

View file

@ -0,0 +1,111 @@
<?php
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
namespace Drupal\Tests\opdavies_talks\Kernel;
use Carbon\Carbon;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\node\Entity\Node;
use Drupal\opdavies_talks\Entity\Node\Talk;
use Drupal\opdavies_talks\Service\TalkDateUpdater;
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();
$node = Node::load($talk->id());
$talk = Talk::createFromNode($node);
$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();
$node = Node::load($talk->id());
$talk = Talk::createFromNode($node);
$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();
$node = Node::load($talk->id());
$talk = Talk::createFromNode($node);
$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

@ -0,0 +1,41 @@
<?php
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
namespace Drupal\Tests\opdavies_talks\Kernel;
use Carbon\Carbon;
use Drupal\views\ResultRow;
use Illuminate\Support\Collection;
final class TalksPageSortTest extends TalksTestBase {
public static $modules = [
'views',
'opdavies_talks',
];
/**
* @test
*/
public function upcoming_talks_are_shown_first_followed_by_past_talks_and_ordered_by_distance(): void {
$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());
$this->assertSame([3, 1, 2, 4], $talkIds->toArray());
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace Drupal\Tests\opdavies_talks\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\opdavies_talks\Entity\Node\Talk;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\paragraphs\ParagraphInterface;
abstract class TalksTestBase extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
// Core.
'node',
'file',
'datetime',
// Contrib.
'entity_reference_revisions',
'paragraphs',
'hook_event_dispatcher',
'core_event_dispatcher',
// Custom.
'opdavies_talks',
'opdavies_talks_test',
];
protected $strictConfigSchema = FALSE;
protected function createEvent(array $overrides = []): ParagraphInterface {
/** @var \Drupal\paragraphs\ParagraphInterface $event */
$event = Paragraph::create(array_merge([
'type' => 'event',
], $overrides));
$event->save();
return $event;
}
protected function createTalk(array $overrides = []): Talk {
$node = Node::create(array_merge([
'title' => 'Test Driven Drupal',
'type' => 'talk',
], $overrides));
$node->save();
return Talk::createFromNode($node);
}
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('paragraph');
$this->installSchema('node', ['node_access']);
$this->installConfig(['opdavies_talks_test']);
}
}

View file

@ -0,0 +1,47 @@
<?php
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
namespace Drupal\Tests\opdavies_talks\Kernel;
use Carbon\Carbon;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
final class UpdatesTalkCreatedDateTest extends TalksTestBase {
/** @test */
public function the_date_is_updated_when_a_talk_node_is_created(): void {
$eventDate = Carbon::today()->addWeek();
$eventDateFormat = $eventDate
->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
$eventDateTimestamp = $eventDate->getTimestamp();
$talk = $this->createTalk([
'field_events' => [
$this->createEvent(['field_date' => $eventDateFormat]),
],
]);
self::assertSame($eventDateTimestamp, $talk->getCreatedTime());
}
/** @test */
public function the_date_is_updated_when_a_talk_node_is_updated(): void {
$talk = $this->createTalk();
$originalCreatedTime = $talk->getCreatedTime();
$eventDate = Carbon::today()->addWeek();
$eventDateFormat = $eventDate
->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
$eventDateTimestamp = $eventDate->getTimestamp();
$talk->addEvent(
$this->createEvent(['field_date' => $eventDateFormat])
);
$talk->save();
$this->assertNotSame($originalCreatedTime, $talk->getCreatedTime());
$this->assertSame($eventDateTimestamp, $talk->getCreatedTime());
}
}