Move all files to old/drupal/
This commit is contained in:
parent
8203e983d5
commit
7d76bf2968
468 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,8 @@
|
|||
opdavies_blog.settings:
|
||||
type: config_object
|
||||
label: 'Blog module configuration'
|
||||
mapping:
|
||||
integromat_webhook_url:
|
||||
type: string
|
||||
post_tweet_webhook_url:
|
||||
type: string
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
name: Oliver Davies blog
|
||||
type: module
|
||||
core_version_requirement: ^8 || ^9
|
||||
package: Custom
|
||||
dependencies:
|
||||
- drupal:node
|
||||
- hook_event_dispatcher:hook_event_dispatcher
|
||||
- paragraphs:paragraphs
|
||||
22
old/drupal/web/modules/custom/blog/opdavies_blog.install
Normal file
22
old/drupal/web/modules/custom/blog/opdavies_blog.install
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for opdavies_blog.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Drupal\opdavies_blog\Repository\PostRepository;
|
||||
|
||||
/**
|
||||
* Mark existing blog posts as sent to social media.
|
||||
*/
|
||||
function opdavies_blog_update_8001(): void {
|
||||
$posts = \Drupal::service(PostRepository::class)->getAll();
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$post->markAsSentToSocialMedia();
|
||||
$post->save();
|
||||
}
|
||||
}
|
||||
49
old/drupal/web/modules/custom/blog/opdavies_blog.module
Normal file
49
old/drupal/web/modules/custom/blog/opdavies_blog.module
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Custom blog code.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\NodeInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_node_links_alter().
|
||||
*/
|
||||
function opdavies_blog_node_links_alter(array &$links, NodeInterface $node): void {
|
||||
if (!method_exists($node, 'getExternalLink')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($link = $node->getExternalLink()) {
|
||||
$links['node']['#links']['node-readmore']['url'] = Url::fromUri($link['uri']);
|
||||
$links['node']['#links']['node-readmore']['title'] = t('Read more<span class="visually-hidden"> about @title</span> (<span class="visually-hidden">on </span>@domain)', [
|
||||
'@domain' => $link['title'],
|
||||
'@title' => $node->label(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK().
|
||||
*/
|
||||
function opdavies_blog_preprocess_block(array &$variables): void {
|
||||
// Add the 'markup' class to blocks.
|
||||
if (in_array($variables['plugin_id'], ['views_block:featured_blog_posts-block_1'])) {
|
||||
$variables['attributes']['class'][] = 'markup';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK().
|
||||
*/
|
||||
function opdavies_blog_preprocess_node(array &$variables): void {
|
||||
if (!method_exists($variables['node'], 'getExternalLink')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($link = $variables['node']->getExternalLink()) {
|
||||
$variables['url'] = $link['uri'];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
parameters:
|
||||
container.autowiring.strict_mode: true
|
||||
|
||||
services:
|
||||
Drupal\Core\Config\ConfigFactoryInterface:
|
||||
alias: config.factory
|
||||
|
||||
Drupal\Core\Entity\EntityTypeManagerInterface:
|
||||
alias: entity_type.manager
|
||||
|
||||
Drupal\Core\Queue\QueueFactory:
|
||||
alias: queue
|
||||
|
||||
GuzzleHttp\ClientInterface:
|
||||
alias: http_client
|
||||
105
old/drupal/web/modules/custom/blog/src/Entity/Node/Post.php
Normal file
105
old/drupal/web/modules/custom/blog/src/Entity/Node/Post.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Entity\Node;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class Post {
|
||||
|
||||
public const FIELD_EXTERNAL_LINK = 'field_external_link';
|
||||
public const FIELD_HAS_TWEET = 'field_has_tweet';
|
||||
public const FIELD_SEND_TO_SOCIAL_MEDIA = 'field_send_to_social_media';
|
||||
public const FIELD_SENT_TO_SOCIAL_MEDIA = 'field_sent_to_social_media';
|
||||
public const FIELD_TAGS = 'field_tags';
|
||||
|
||||
private NodeInterface $node;
|
||||
|
||||
public function __construct(EntityInterface $node) {
|
||||
$this->node = $node;
|
||||
}
|
||||
|
||||
public function bundle(): string {
|
||||
return 'post';
|
||||
}
|
||||
|
||||
public function get(string $name): FieldItemListInterface {
|
||||
return $this->node->get($name);
|
||||
}
|
||||
|
||||
public function getExternalLink(): ?array {
|
||||
return ($link = $this->get(self::FIELD_EXTERNAL_LINK)->get(0))
|
||||
? $link->getValue()
|
||||
: NULL;
|
||||
}
|
||||
|
||||
public function getNode(): NodeInterface {
|
||||
|
||||
return $this->node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|Term[]
|
||||
*/
|
||||
public function getTags(): Collection {
|
||||
return new Collection($this->get(self::FIELD_TAGS)->referencedEntities());
|
||||
}
|
||||
|
||||
public function hasBeenSentToSocialMedia(): bool {
|
||||
return (bool) $this->get(self::FIELD_SENT_TO_SOCIAL_MEDIA)->getString();
|
||||
}
|
||||
|
||||
public function hasTweet(): bool {
|
||||
return (bool) $this->get(self::FIELD_HAS_TWEET)->getString();
|
||||
}
|
||||
|
||||
public function id(): int {
|
||||
return (int) $this->node->id();
|
||||
}
|
||||
|
||||
public function isExternalPost(): bool {
|
||||
return (bool) $this->getExternalLink();
|
||||
}
|
||||
|
||||
public function label(): string {
|
||||
return $this->node->label();
|
||||
}
|
||||
|
||||
public function markAsSentToSocialMedia(): self {
|
||||
$this->set(self::FIELD_SENT_TO_SOCIAL_MEDIA, TRUE);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function save(): void {
|
||||
$this->node->save();
|
||||
}
|
||||
|
||||
public function set(string $name, $value): void {
|
||||
$this->node->set($name, $value);
|
||||
}
|
||||
|
||||
public function setTags(array $tags): void {
|
||||
$this->set(self::FIELD_TAGS, $tags);
|
||||
}
|
||||
|
||||
public function shouldSendToSocialMedia(): bool {
|
||||
return (bool) $this->get(self::FIELD_SEND_TO_SOCIAL_MEDIA)->getString();
|
||||
}
|
||||
|
||||
public function url(string $type, array $options = []): string {
|
||||
return $this->node->url($type, $options);
|
||||
}
|
||||
|
||||
public static function createFromNode(EntityInterface $node): self {
|
||||
// TODO: ensure that this is a node and a `post` type.
|
||||
return new self($node);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Queue\QueueFactory;
|
||||
use Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent;
|
||||
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class PushPostToSocialMediaOnceItIsPublished implements EventSubscriberInterface {
|
||||
|
||||
private QueueFactory $queueFactory;
|
||||
|
||||
public function __construct(QueueFactory $queueFactory) {
|
||||
$this->queueFactory = $queueFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
return [
|
||||
HookEventDispatcherInterface::ENTITY_INSERT => 'pushPost',
|
||||
HookEventDispatcherInterface::ENTITY_UPDATE => 'pushPost',
|
||||
];
|
||||
}
|
||||
|
||||
public function pushPost(AbstractEntityEvent $event): void {
|
||||
$entity = $event->getEntity();
|
||||
|
||||
if ($entity->getEntityTypeId() != 'node') {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Post $entity */
|
||||
if ($entity->bundle() != 'post') {
|
||||
return;
|
||||
}
|
||||
|
||||
$queue = $this->queueFactory->get('opdavies_blog.push_post_to_social_media');
|
||||
$queue->createQueue();
|
||||
|
||||
$queue->createItem([
|
||||
'post' => $entity,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\EventSubscriber;
|
||||
|
||||
use Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent;
|
||||
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\taxonomy\TermInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class SortTagsAlphabeticallyWhenPostIsSaved implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
return [
|
||||
HookEventDispatcherInterface::ENTITY_PRE_SAVE => 'sortTags',
|
||||
];
|
||||
}
|
||||
|
||||
public function sortTags(AbstractEntityEvent $event): void {
|
||||
$entity = $event->getEntity();
|
||||
|
||||
if ($entity->getEntityTypeId() != 'node') {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->bundle() != 'post') {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = Post::createFromNode($entity);
|
||||
|
||||
$sortedTags = $post->getTags()
|
||||
->sortBy(fn(TermInterface $tag) => $tag->label());
|
||||
|
||||
$post->setTags($sortedTags->toArray());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
final class OpdaviesBlogServiceProvider implements ServiceProviderInterface {
|
||||
|
||||
public function register(ContainerBuilder $container): void {
|
||||
foreach (['EventSubscriber', 'Repository', 'Service', 'UseCase'] as $directory) {
|
||||
$files = Finder::create()
|
||||
->in(__DIR__ . '/' . $directory)
|
||||
->files()
|
||||
->name('*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$class = 'Drupal\opdavies_blog\\' . $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\opdavies_blog\Plugin\Block;
|
||||
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\CurrentRouteMatch;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\opdavies_blog\Repository\RelatedPostsRepository;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @Block(
|
||||
* id = "opdavies_blog_related_posts",
|
||||
* admin_label = @Translation("Related Posts"),
|
||||
* category = @Translation("Blog")
|
||||
* )
|
||||
*/
|
||||
class RelatedPostsBlock extends BlockBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
private CurrentRouteMatch $currentRouteMatch;
|
||||
|
||||
private RelatedPostsRepository $relatedPostsRepository;
|
||||
|
||||
public function __construct(
|
||||
array $configuration,
|
||||
string $pluginId,
|
||||
array $pluginDefinition,
|
||||
CurrentRouteMatch $currentRouteMatch,
|
||||
RelatedPostsRepository $relatedPostsRepository
|
||||
) {
|
||||
parent::__construct($configuration, $pluginId, $pluginDefinition);
|
||||
|
||||
$this->currentRouteMatch = $currentRouteMatch;
|
||||
$this->relatedPostsRepository = $relatedPostsRepository;
|
||||
}
|
||||
|
||||
public static function create(
|
||||
ContainerInterface $container,
|
||||
array $configuration,
|
||||
$pluginId,
|
||||
$pluginDefinition
|
||||
): self {
|
||||
return new self(
|
||||
$configuration,
|
||||
$pluginId,
|
||||
$pluginDefinition,
|
||||
$container->get('current_route_match'),
|
||||
$container->get(RelatedPostsRepository::class)
|
||||
);
|
||||
}
|
||||
|
||||
public function build(): array {
|
||||
$currentPost = $this->currentRouteMatch->getParameter('node');
|
||||
|
||||
/** @var Collection|Post[] $relatedPosts */
|
||||
$relatedPosts = $this->relatedPostsRepository->getFor($currentPost);
|
||||
|
||||
if ($relatedPosts->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$build['content'] = [
|
||||
'#items' => $relatedPosts
|
||||
->sortByDesc(fn(Post $post) => $post->getNode()->getCreatedTime())
|
||||
->map(fn(Post $post) => $this->generateLinkToPost($post))
|
||||
->slice(0, 3)
|
||||
->toArray(),
|
||||
'#theme' => 'item_list',
|
||||
];
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
public function getCacheMaxAge(): int {
|
||||
return 604800;
|
||||
}
|
||||
|
||||
public function getCacheContexts(): array {
|
||||
return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
|
||||
}
|
||||
|
||||
public function getCacheTags(): array {
|
||||
/** @var Post $post */
|
||||
$post = $this->currentRouteMatch->getParameter('node');
|
||||
|
||||
return Cache::mergeTags(parent::getCacheTags(), ["node:{$post->id()}"]);
|
||||
}
|
||||
|
||||
private function generateLinkToPost(Post $post): Link {
|
||||
return Link::createFromRoute(
|
||||
$post->label(),
|
||||
'entity.node.canonical',
|
||||
['node' => $post->id()]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Plugin\QueueWorker;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Queue\QueueWorkerBase;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\opdavies_blog\Service\PostPusher\IftttPostPusher;
|
||||
use Drupal\opdavies_blog\Service\PostPusher\IntegromatPostPusher;
|
||||
use Drupal\opdavies_blog\Service\PostPusher\PostPusher;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* @QueueWorker(
|
||||
* id = "opdavies_blog.push_post_to_social_media",
|
||||
* title = "Push a blog post to social media",
|
||||
* cron = {"time": 30}
|
||||
* )
|
||||
*/
|
||||
final class PostPusherQueueWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
private EntityStorageInterface $nodeStorage;
|
||||
|
||||
/**
|
||||
* @var array|PostPusher[]
|
||||
*/
|
||||
private array $postPushers;
|
||||
|
||||
public function __construct(
|
||||
array $configuration,
|
||||
string $pluginId,
|
||||
array $pluginDefinition,
|
||||
EntityStorageInterface $nodeStorage,
|
||||
array $postPushers
|
||||
) {
|
||||
parent::__construct($configuration, $pluginId, $pluginDefinition);
|
||||
|
||||
$this->nodeStorage = $nodeStorage;
|
||||
$this->postPushers = $postPushers;
|
||||
}
|
||||
|
||||
public static function create(
|
||||
ContainerInterface $container,
|
||||
array $configuration,
|
||||
$pluginId,
|
||||
$pluginDefinition
|
||||
) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$pluginId,
|
||||
$pluginDefinition,
|
||||
$container->get('entity_type.manager')->getStorage('node'),
|
||||
[
|
||||
$container->get(IftttPostPusher::class),
|
||||
$container->get(IntegromatPostPusher::class),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function processItem($data): void {
|
||||
/** @var Post $post */
|
||||
['post' => $post] = $data;
|
||||
|
||||
if (!$this->shouldBePushed($post)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$post->getNode()->isLatestRevision()) {
|
||||
$node = $this->nodeStorage->load($post->id());
|
||||
$post = Post::createFromNode($node);
|
||||
|
||||
if (!$this->shouldBePushed($post)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->postPushers as $pusher) {
|
||||
$pusher->push($post);
|
||||
}
|
||||
|
||||
$post->set(Post::FIELD_SENT_TO_SOCIAL_MEDIA, TRUE);
|
||||
$post->save();
|
||||
}
|
||||
|
||||
private function shouldBePushed(Post $post): bool {
|
||||
if ($post->isExternalPost()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!$post->getNode()->isPublished()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!$post->shouldSendToSocialMedia()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ($post->hasBeenSentToSocialMedia()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Repository;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class PostRepository {
|
||||
|
||||
private EntityStorageInterface $nodeStorage;
|
||||
|
||||
public function __construct(EntityTypeManagerInterface $entityTypeManager) {
|
||||
$this->nodeStorage = $entityTypeManager->getStorage('node');
|
||||
}
|
||||
|
||||
public function getAll(): Collection {
|
||||
return new Collection(
|
||||
$this->nodeStorage->loadByProperties([
|
||||
'type' => 'post',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Repository;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryInterface;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\taxonomy\TermInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class RelatedPostsRepository {
|
||||
|
||||
private EntityStorageInterface $nodeStorage;
|
||||
|
||||
public function __construct(
|
||||
EntityTypeManagerInterface $entityTypeManager
|
||||
) {
|
||||
$this->nodeStorage = $entityTypeManager->getStorage('node');
|
||||
}
|
||||
|
||||
public function getFor(Post $post): Collection {
|
||||
$tags = $post->get('field_tags')->referencedEntities();
|
||||
|
||||
if (!$tags) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
$tagIds = (new Collection($tags))
|
||||
->map(fn(TermInterface $tag) => $tag->id())
|
||||
->values();
|
||||
|
||||
/** @var array $postIds */
|
||||
$postIds = $this->query($post, $tagIds)->execute();
|
||||
|
||||
$posts = $this->nodeStorage->loadMultiple($postIds);
|
||||
|
||||
return new Collection(array_values($posts));
|
||||
}
|
||||
|
||||
private function query(Post $post, Collection $tagIds): QueryInterface {
|
||||
$query = $this->nodeStorage->getQuery();
|
||||
|
||||
// Ensure that the current node ID is not returned as a related post.
|
||||
$query->condition('nid', $post->id(), '!=');
|
||||
|
||||
// Only return posts with the same tags.
|
||||
$query->condition('field_tags', $tagIds->toArray(), 'IN');
|
||||
|
||||
$query->condition('status', NodeInterface::PUBLISHED);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Service\PostPusher;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\opdavies_blog\UseCase\ConvertPostToTweet;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class IftttPostPusher extends WebhookPostPusher {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
private ConfigFactoryInterface $configFactory;
|
||||
|
||||
public function __construct(
|
||||
ConvertPostToTweet $convertPostToTweet,
|
||||
ClientInterface $client,
|
||||
ConfigFactoryInterface $configFactory
|
||||
) {
|
||||
$this->convertPostToTweet = $convertPostToTweet;
|
||||
$this->configFactory = $configFactory;
|
||||
|
||||
parent::__construct($convertPostToTweet, $client);
|
||||
}
|
||||
|
||||
public function push(Post $post): void {
|
||||
$url = $this->configFactory
|
||||
->get('opdavies_blog.settings')
|
||||
->get('post_tweet_webhook_url');
|
||||
|
||||
Assert::notNull($url, 'Cannot push the post if there is no URL.');
|
||||
|
||||
$this->client->post($url, [
|
||||
'form_params' => [
|
||||
'value1' => $this->t('Blogged: @text', ['@text' => ($this->convertPostToTweet)($post)])
|
||||
->render(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Service\PostPusher;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\opdavies_blog\UseCase\ConvertPostToTweet;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final class IntegromatPostPusher extends WebhookPostPusher {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
private ConfigFactoryInterface $configFactory;
|
||||
|
||||
public function __construct(
|
||||
ConvertPostToTweet $convertPostToTweet,
|
||||
ClientInterface $client,
|
||||
ConfigFactoryInterface $configFactory
|
||||
) {
|
||||
$this->convertPostToTweet = $convertPostToTweet;
|
||||
$this->configFactory = $configFactory;
|
||||
|
||||
parent::__construct($convertPostToTweet, $client);
|
||||
}
|
||||
|
||||
public function push(Post $post): void {
|
||||
$url = $this->configFactory
|
||||
->get('opdavies_blog.settings')
|
||||
->get('integromat_webhook_url');
|
||||
|
||||
Assert::notNull($url, 'Cannot push the post if there is no URL.');
|
||||
|
||||
$this->client->post($url, [
|
||||
'form_params' => [
|
||||
'text' => $this->t('@text', ['@text' => ($this->convertPostToTweet)($post)])
|
||||
->render(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Service\PostPusher;
|
||||
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
|
||||
final class NullPostPusher implements PostPusher {
|
||||
|
||||
public function push(Post $post): void {}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Service\PostPusher;
|
||||
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
|
||||
interface PostPusher {
|
||||
|
||||
public function push(Post $post): void;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\Service\PostPusher;
|
||||
|
||||
use Drupal\opdavies_blog\UseCase\ConvertPostToTweet;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
|
||||
abstract class WebhookPostPusher implements PostPusher {
|
||||
|
||||
protected ConvertPostToTweet $convertPostToTweet;
|
||||
|
||||
protected ClientInterface $client;
|
||||
|
||||
public function __construct(
|
||||
ConvertPostToTweet $convertPostToTweet,
|
||||
ClientInterface $client
|
||||
) {
|
||||
|
||||
$this->convertPostToTweet = $convertPostToTweet;
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog\UseCase;
|
||||
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class ConvertPostToTweet {
|
||||
|
||||
private Post $post;
|
||||
|
||||
public function __invoke(Post $post): string {
|
||||
$this->post = $post;
|
||||
|
||||
$parts = [
|
||||
$post->label(),
|
||||
$post->url('canonical', ['absolute' => TRUE]),
|
||||
$this->convertTermsToHashtags(),
|
||||
];
|
||||
|
||||
return implode(PHP_EOL . PHP_EOL, $parts);
|
||||
}
|
||||
|
||||
private function convertTermsToHashtags(): string {
|
||||
return $this->post
|
||||
->getTags()
|
||||
->filter(fn(Term $term) => !$this->tagsToRemove()
|
||||
->contains($term->label()))
|
||||
->map(fn(Term $term) => $this->convertTermToHashtag($term))
|
||||
->implode(' ');
|
||||
}
|
||||
|
||||
private function tagsToRemove(): Collection {
|
||||
// TODO: Move these values into configuration/settings.php.
|
||||
return new Collection([
|
||||
'Drupal Planet',
|
||||
]);
|
||||
}
|
||||
|
||||
private function convertTermToHashtag(Term $tag): string {
|
||||
return '#' . (new Collection(explode(' ', $tag->label())))
|
||||
->map(fn(string $word): string => ucfirst($word))
|
||||
->implode('');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_external_link
|
||||
- node.type.post
|
||||
module:
|
||||
- link
|
||||
id: node.post.field_external_link
|
||||
field_name: field_external_link
|
||||
entity_type: node
|
||||
bundle: post
|
||||
label: 'External link'
|
||||
description: ''
|
||||
required: false
|
||||
translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
link_type: 16
|
||||
title: 2
|
||||
field_type: link
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_has_tweet
|
||||
- node.type.post
|
||||
id: node.post.field_has_tweet
|
||||
field_name: field_has_tweet
|
||||
entity_type: node
|
||||
bundle: post
|
||||
label: 'Has tweet'
|
||||
description: 'Check to include Twitter''s widget.js script for this page.'
|
||||
required: false
|
||||
translatable: false
|
||||
default_value:
|
||||
-
|
||||
value: 0
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
on_label: 'Yes'
|
||||
off_label: 'No'
|
||||
field_type: boolean
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_send_to_social_media
|
||||
- node.type.post
|
||||
id: node.post.field_send_to_social_media
|
||||
field_name: field_send_to_social_media
|
||||
entity_type: node
|
||||
bundle: post
|
||||
label: 'Send to social media'
|
||||
description: 'Automatically send this post to Twitter and LinkedIn.'
|
||||
required: false
|
||||
translatable: false
|
||||
default_value:
|
||||
-
|
||||
value: 1
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
on_label: 'On'
|
||||
off_label: 'Off'
|
||||
field_type: boolean
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_sent_to_social_media
|
||||
- node.type.post
|
||||
id: node.post.field_sent_to_social_media
|
||||
field_name: field_sent_to_social_media
|
||||
entity_type: node
|
||||
bundle: post
|
||||
label: 'Sent to social media'
|
||||
description: ''
|
||||
required: false
|
||||
translatable: false
|
||||
default_value:
|
||||
-
|
||||
value: 0
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
on_label: 'On'
|
||||
off_label: 'Off'
|
||||
field_type: boolean
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- field.storage.node.field_tags
|
||||
- node.type.post
|
||||
- taxonomy.vocabulary.tags
|
||||
id: node.post.field_tags
|
||||
field_name: field_tags
|
||||
entity_type: node
|
||||
bundle: post
|
||||
label: Tags
|
||||
description: ''
|
||||
required: false
|
||||
translatable: false
|
||||
default_value: { }
|
||||
default_value_callback: ''
|
||||
settings:
|
||||
handler: 'default:taxonomy_term'
|
||||
handler_settings:
|
||||
target_bundles:
|
||||
tags: tags
|
||||
sort:
|
||||
field: name
|
||||
direction: asc
|
||||
auto_create: true
|
||||
auto_create_bundle: ''
|
||||
field_type: entity_reference
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- link
|
||||
- node
|
||||
id: node.field_external_link
|
||||
field_name: field_external_link
|
||||
entity_type: node
|
||||
type: link
|
||||
settings: { }
|
||||
module: link
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
id: node.field_has_tweet
|
||||
field_name: field_has_tweet
|
||||
entity_type: node
|
||||
type: boolean
|
||||
settings: { }
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
id: node.field_send_to_social_media
|
||||
field_name: field_send_to_social_media
|
||||
entity_type: node
|
||||
type: boolean
|
||||
settings: { }
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
id: node.field_sent_to_social_media
|
||||
field_name: field_sent_to_social_media
|
||||
entity_type: node
|
||||
type: boolean
|
||||
settings: { }
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: 1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
- taxonomy
|
||||
id: node.field_tags
|
||||
field_name: field_tags
|
||||
entity_type: node
|
||||
type: entity_reference
|
||||
settings:
|
||||
target_type: taxonomy_term
|
||||
module: core
|
||||
locked: false
|
||||
cardinality: -1
|
||||
translatable: true
|
||||
indexes: { }
|
||||
persist_with_no_fields: false
|
||||
custom_storage: false
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
third_party_settings: { }
|
||||
name: 'Blog post'
|
||||
type: post
|
||||
description: 'A single blog post.'
|
||||
help: ''
|
||||
new_revision: true
|
||||
preview_mode: 1
|
||||
display_submitted: true
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
name: Tags
|
||||
vid: tags
|
||||
description: 'Tags for categorising blog posts.'
|
||||
weight: 0
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
name: Oliver Davies Posts Test
|
||||
type: module
|
||||
core_version_requirement: ^8 || ^9
|
||||
hidden: true
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
parameters:
|
||||
container.autowiring.strict_mode: true
|
||||
|
||||
services:
|
||||
Drupal\Core\Entity\EntityTypeManagerInterface:
|
||||
alias: entity_type.manager
|
||||
|
||||
Drupal\opdavies_blog\Service\PostPusher\PostPusher:
|
||||
class: Drupal\opdavies_blog\Service\PostPusher\NullPostPusher
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog_test\Factory;
|
||||
|
||||
use Assert\Assert;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\TermInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class PostFactory {
|
||||
|
||||
private EntityStorageInterface $termStorage;
|
||||
|
||||
private Collection $tags;
|
||||
|
||||
private string $title = 'This is a test blog post';
|
||||
|
||||
public function __construct(
|
||||
EntityTypeManagerInterface $entityTypeManager
|
||||
) {
|
||||
$this->termStorage = $entityTypeManager->getStorage('taxonomy_term');
|
||||
|
||||
$this->tags = new Collection();
|
||||
}
|
||||
|
||||
public function create(array $overrides = []): Post {
|
||||
$this->tags->each(function (TermInterface $tag): void {
|
||||
Assert::that($tag->bundle())->same('tags');
|
||||
});
|
||||
|
||||
$values = [
|
||||
'title' => $this->title,
|
||||
'type' => 'post',
|
||||
Post::FIELD_TAGS => $this->tags->toArray(),
|
||||
];
|
||||
|
||||
$post = Node::create($values + $overrides);
|
||||
|
||||
return Post::createFromNode($post);
|
||||
}
|
||||
|
||||
public function setTitle(string $title): self {
|
||||
Assert::that($title)->notEmpty();
|
||||
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withTags(array $tags): self {
|
||||
$this->tags = new Collection();
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
Assert::that($tag)->notEmpty()->string();
|
||||
|
||||
$this->tags->push($this->createOrReferenceTag($tag));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function createOrReferenceTag(string $tag): EntityInterface {
|
||||
$existingTags = $this->termStorage->loadByProperties(['name' => $tag]);
|
||||
|
||||
if ($existingTags) {
|
||||
return reset($existingTags);
|
||||
}
|
||||
|
||||
return Term::create(['vid' => 'tags', 'name' => $tag]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_blog_test;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
final class OpdaviesBlogTestServiceProvider implements ServiceProviderInterface {
|
||||
|
||||
public function register(ContainerBuilder $container): void {
|
||||
foreach (['Factory'] as $directory) {
|
||||
$files = Finder::create()
|
||||
->in(__DIR__ . '/' . $directory)
|
||||
->files()
|
||||
->name('*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$class = 'Drupal\opdavies_blog_test\\' . $directory . '\\' .
|
||||
str_replace('/', '\\', substr($file->getRelativePathname(), 0, -4));
|
||||
|
||||
if ($container->hasDefinition($class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$definition = new Definition($class);
|
||||
$definition->setAutowired(TRUE);
|
||||
$container->setDefinition($class, $definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
|
||||
|
||||
namespace Drupal\Tests\opdavies_blog\Kernel\Entity\Node;
|
||||
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\opdavies_blog_test\Factory\PostFactory;
|
||||
|
||||
final class PostTest extends EntityKernelTestBase {
|
||||
|
||||
public static $modules = [
|
||||
// Core.
|
||||
'node',
|
||||
'link',
|
||||
'taxonomy',
|
||||
|
||||
// Custom.
|
||||
'opdavies_blog',
|
||||
'opdavies_blog_test',
|
||||
];
|
||||
|
||||
private PostFactory $postFactory;
|
||||
|
||||
/** @test */
|
||||
public function it_can_determine_if_a_post_contains_a_tweet(): void {
|
||||
$post = $this->postFactory->create();
|
||||
$post->save();
|
||||
|
||||
$this->assertFalse($post->hasTweet());
|
||||
|
||||
$post = $this->postFactory->create([Post::FIELD_HAS_TWEET => TRUE]);
|
||||
$post->save();
|
||||
|
||||
$this->assertTrue($post->hasTweet());
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->postFactory = $this->container->get(PostFactory::class);
|
||||
|
||||
$this->installEntitySchema('taxonomy_term');
|
||||
|
||||
$this->installConfig(['opdavies_blog_test']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\opdavies_blog\Kernel;
|
||||
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
use Drupal\Tests\node\Traits\NodeCreationTrait;
|
||||
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
|
||||
|
||||
abstract class PostTestBase extends EntityKernelTestBase {
|
||||
|
||||
use NodeCreationTrait;
|
||||
use TaxonomyTestTrait;
|
||||
|
||||
public static $modules = [
|
||||
// Core.
|
||||
'node',
|
||||
'taxonomy',
|
||||
'link',
|
||||
|
||||
// Contrib.
|
||||
'hook_event_dispatcher',
|
||||
'core_event_dispatcher',
|
||||
|
||||
// Custom.
|
||||
'opdavies_blog_test',
|
||||
'opdavies_blog',
|
||||
];
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installConfig([
|
||||
'filter',
|
||||
'opdavies_blog_test',
|
||||
]);
|
||||
|
||||
$this->installEntitySchema('taxonomy_vocabulary');
|
||||
$this->installEntitySchema('taxonomy_term');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
|
||||
|
||||
namespace Drupal\Tests\opdavies_blog\Kernel;
|
||||
|
||||
use Drupal\Core\Queue\QueueInterface;
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
use Drupal\Tests\node\Traits\NodeCreationTrait;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
|
||||
final class PushToSocialMediaTest extends EntityKernelTestBase {
|
||||
|
||||
use NodeCreationTrait;
|
||||
|
||||
public static $modules = [
|
||||
// Core.
|
||||
'node',
|
||||
'taxonomy',
|
||||
'link',
|
||||
|
||||
// Contrib.
|
||||
'hook_event_dispatcher',
|
||||
'core_event_dispatcher',
|
||||
|
||||
// Custom.
|
||||
'opdavies_blog',
|
||||
'opdavies_blog_test',
|
||||
];
|
||||
|
||||
private QueueInterface $queue;
|
||||
|
||||
/** @test */
|
||||
public function it_queues_a_post_when_it_is_created(): void {
|
||||
$this->assertSame(0, $this->queue->numberOfItems());
|
||||
|
||||
$this->createNode([
|
||||
'title' => 'Ignoring PHPCS sniffs within PHPUnit tests',
|
||||
'type' => 'post',
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $this->queue->numberOfItems());
|
||||
|
||||
$item = $this->queue->claimItem();
|
||||
|
||||
/** @var Post $post */
|
||||
$post = $item->data['post'];
|
||||
|
||||
$this->assertNotNull($post);
|
||||
$this->assertInstanceOf(NodeInterface::class, $post);
|
||||
$this->assertSame('post', $post->bundle());
|
||||
$this->assertSame('Ignoring PHPCS sniffs within PHPUnit tests', $post->getTitle());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_queues_a_post_when_it_is_updated(): void {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_pushes_a_post_when_the_queue_is_processed(): void {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installConfig(['filter', 'opdavies_blog_test']);
|
||||
|
||||
$this->installSchema('node', ['node_access']);
|
||||
|
||||
$this->queue = $this->container->get('queue')
|
||||
->get('opdavies_blog.push_post_to_social_media');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
|
||||
|
||||
namespace Drupal\Tests\opdavies_blog\Kernel;
|
||||
|
||||
use Drupal\opdavies_blog\Repository\RelatedPostsRepository;
|
||||
use Drupal\opdavies_blog_test\Factory\PostFactory;
|
||||
|
||||
final class RelatedPostsTest extends PostTestBase {
|
||||
|
||||
private PostFactory $postFactory;
|
||||
|
||||
private RelatedPostsRepository $relatedPostsRepository;
|
||||
|
||||
/** @test */
|
||||
public function it_returns_related_posts(): void {
|
||||
$postA = $this->postFactory
|
||||
->setTitle('Post A')
|
||||
->withTags(['Drupal 8'])
|
||||
->create();
|
||||
$postA->save();
|
||||
|
||||
$postB = $this->postFactory
|
||||
->setTitle('Post B')
|
||||
->withTags(['Drupal 8'])
|
||||
->create();
|
||||
$postB->save();
|
||||
|
||||
$relatedPosts = $this->relatedPostsRepository->getFor($postA);
|
||||
|
||||
$this->assertCount(1, $relatedPosts);
|
||||
$this->assertSame('Post B', $relatedPosts->first()->label());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function unpublished_posts_are_not_returned(): void {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_an_empty_collection_if_there_are_no_related_posts(): void {
|
||||
$postA = $this->postFactory
|
||||
->setTitle('Drupal 8 post')
|
||||
->withTags(['Drupal 8'])
|
||||
->create();
|
||||
$postA->save();
|
||||
|
||||
$postB = $this->postFactory
|
||||
->setTitle('Drupal 9 post')
|
||||
->withTags(['Drupal 9'])
|
||||
->create();
|
||||
$postB->save();
|
||||
|
||||
$relatedPosts = $this->relatedPostsRepository->getFor($postA);
|
||||
|
||||
$this->assertEmpty($relatedPosts);
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->postFactory = $this->container->get(PostFactory::class);
|
||||
$this->relatedPostsRepository = $this->container->get(RelatedPostsRepository::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
// phpcs:disable Drupal.Commenting.DocComment, Drupal.NamingConventions.ValidFunctionName
|
||||
|
||||
namespace Drupal\Tests\opdavies_blog\Kernel;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\opdavies_blog\Entity\Node\Post;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\taxonomy\TermInterface;
|
||||
use Drupal\taxonomy\VocabularyInterface;
|
||||
|
||||
final class ReorderBlogTagsTest extends PostTestBase {
|
||||
|
||||
/** @test */
|
||||
public function it_reorders_tags_on_blog_posts_to_be_arranged_alphabetically(): void {
|
||||
/** @var VocabularyInterface $vocabulary */
|
||||
$vocabulary = Vocabulary::load('tags');
|
||||
|
||||
$this->createTerm($vocabulary, ['name' => 'Drupal']);
|
||||
$this->createTerm($vocabulary, ['name' => 'PHP']);
|
||||
$this->createTerm($vocabulary, ['name' => 'Symfony']);
|
||||
|
||||
$post = $this->createNode([
|
||||
'type' => 'post',
|
||||
Post::FIELD_TAGS => [3, 1, 2],
|
||||
]);
|
||||
|
||||
$node = Node::load($post->id());
|
||||
$post = Post::createFromNode($node);
|
||||
|
||||
$this->assertSame(
|
||||
['Drupal', 'PHP', 'Symfony'],
|
||||
$post->getTags()
|
||||
->map(fn(TermInterface $tag) => $tag->label())
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\opdavies_blog\Kernel\UseCase;
|
||||
|
||||
use Drupal\opdavies_blog\UseCase\ConvertPostToTweet;
|
||||
use Drupal\opdavies_blog_test\Factory\PostFactory;
|
||||
use Drupal\Tests\opdavies_blog\Kernel\PostTestBase;
|
||||
|
||||
final class ConvertPostToTweetTest extends PostTestBase {
|
||||
|
||||
private ConvertPostToTweet $convertPostToTweet;
|
||||
|
||||
public function testConvertPostToTweet(): void {
|
||||
$post = $this->postFactory
|
||||
->setTitle('Creating a custom PHPUnit command for DDEV')
|
||||
->withTags(['Automated testing', 'DDEV', 'Drupal', 'Drupal 8', 'PHP'])
|
||||
->create();
|
||||
|
||||
$post->save();
|
||||
|
||||
$expected = <<<EOF
|
||||
Creating a custom PHPUnit command for DDEV
|
||||
|
||||
http://localhost/node/1
|
||||
|
||||
#AutomatedTesting #DDEV #Drupal #Drupal8 #PHP
|
||||
EOF;
|
||||
|
||||
$this->assertSame($expected, ($this->convertPostToTweet)($post));
|
||||
}
|
||||
|
||||
public function testCertainTermsAreNotAddedAsHashtags(): void {
|
||||
$post = $this->postFactory
|
||||
->setTitle('Drupal Planet should not be added as a hashtag')
|
||||
->withTags(['Drupal', 'Drupal Planet', 'PHP'])
|
||||
->create();
|
||||
|
||||
$post->save();
|
||||
|
||||
$expected = <<<EOF
|
||||
Drupal Planet should not be added as a hashtag
|
||||
|
||||
http://localhost/node/1
|
||||
|
||||
#Drupal #PHP
|
||||
EOF;
|
||||
|
||||
$this->assertSame($expected, ($this->convertPostToTweet)($post));
|
||||
}
|
||||
|
||||
public function testSomeTagsAreNotAutomaticallyCapitalised(): void {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->convertPostToTweet = $this->container->get(ConvertPostToTweet::class);
|
||||
$this->postFactory = $this->container->get(PostFactory::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
name: Oliver Davies Recommendations
|
||||
description: Custom code for recommendations.
|
||||
type: module
|
||||
core_version_requirement: ^8 || ^9
|
||||
package: Oliver Davies
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Oliver Davies Recommendations module.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_image_style().
|
||||
*/
|
||||
function opdavies_recommendations_preprocess_image_style(array &$variables): void {
|
||||
if ($variables['style_name'] == 'recommendation') {
|
||||
$image = &$variables['image'];
|
||||
|
||||
$image['#attributes']['class'][] = 'bg-gray-200';
|
||||
$image['#attributes']['height'] = 100;
|
||||
$image['#attributes']['loading'] = 'lazy';
|
||||
$image['#attributes']['width'] = 100;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
name: Oliver Davies Talks
|
||||
description: Custom code for talks pages.
|
||||
type: module
|
||||
core_version_requirement: ^8 || ^9
|
||||
package: Custom
|
||||
22
old/drupal/web/modules/custom/talks/opdavies_talks.install
Normal file
22
old/drupal/web/modules/custom/talks/opdavies_talks.install
Normal 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();
|
||||
}
|
||||
}
|
||||
74
old/drupal/web/modules/custom/talks/opdavies_talks.module
Normal file
74
old/drupal/web/modules/custom/talks/opdavies_talks.module
Normal 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
services:
|
||||
Drupal\Component\Datetime\TimeInterface:
|
||||
alias: datetime.time
|
||||
|
||||
Drupal\Core\Entity\EntityTypeManagerInterface:
|
||||
alias: entity_type.manager
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
98
old/drupal/web/modules/custom/talks/src/Entity/Node/Talk.php
Normal file
98
old/drupal/web/modules/custom/talks/src/Entity/Node/Talk.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\opdavies_talks\EventSubscriber;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent;
|
||||
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
|
||||
use Drupal\opdavies_talks\Entity\Node\Talk;
|
||||
use Drupal\paragraphs\ParagraphInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Update a talk node before it's saved.
|
||||
*/
|
||||
final class UpdateTalkNodeBeforeSave implements EventSubscriberInterface {
|
||||
|
||||
public static function getSubscribedEvents() {
|
||||
return [
|
||||
HookEventDispatcherInterface::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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
name: Talk
|
||||
type: talk
|
||||
description: ''
|
||||
help: ''
|
||||
new_revision: true
|
||||
preview_mode: 1
|
||||
display_submitted: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
id: event
|
||||
label: Event
|
||||
icon_uuid: null
|
||||
icon_default: null
|
||||
description: ''
|
||||
behavior_plugins: { }
|
||||
|
|
@ -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: { }
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
name: Custom Test
|
||||
type: module
|
||||
core_version_requirement: ^8 || ^9
|
||||
hidden: true
|
||||
|
|
@ -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() {
|
||||
parent::setUp();
|
||||
|
||||
$this->talkCounter = $this->container->get(TalkCounter::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installConfig(['filter']);
|
||||
|
||||
$this->talkRepository = $this->container->get(TalkRepository::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installEntitySchema('paragraph');
|
||||
$this->installSchema('node', ['node_access']);
|
||||
|
||||
$this->installConfig(['opdavies_talks_test']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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]),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEqual($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());
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue