Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
75a2792
Issue #1345666: Add scheduler repeat submodule
Oct 20, 2020
406ddb8
Issue #1345666: Implement repeat logic invokation
Oct 26, 2020
5a98522
Issue #1345666: Test widget visibility
Oct 26, 2020
c3b0866
Merge pull request #3 from mikaelkundert/issue/1345666-scheduler-repeat
jonathan1055 Oct 26, 2020
ba7465c
Change Once to None, fix missing $max_length, add :void to test
jonathan1055 Oct 29, 2020
a80f288
add EventSubscriber and TenMinutes plugin for testing
jonathan1055 Oct 29, 2020
7c39543
PR 4
jonathan1055 Oct 29, 2020
d94cae4
Temporary fix for invalid render key
jonathan1055 Oct 29, 2020
13066be
first attempt at next
jonathan1055 Oct 31, 2020
cc4f54d
working version using "next" dates
jonathan1055 Nov 2, 2020
3287a25
rename field repeat to scheduler_repeat and remove all ddm() calls
jonathan1055 Nov 2, 2020
690b15d
short array syntax in EventSubscriber.php
jonathan1055 Nov 2, 2020
2c4457d
Scheduler Repeat (#5)
Nov 2, 2020
5c91855
Updates to align. (#6)
jonathan1055 Nov 3, 2020
277e9af
Remove "next" calculations from event unpublish()
jonathan1055 Nov 3, 2020
762892a
Merge 1 commit from 8.x-1.x - allow 10x StatementWrapper::fetchColumn…
jonathan1055 Nov 3, 2020
ec04063
Move FieldsDisplayTest and rename to tests/src/Functional/SchedulerRe…
jonathan1055 Nov 3, 2020
cd97442
Formatter (#7)
jonathan1055 Nov 3, 2020
7830ffa
Add plugins for Daily, Weekly, Monthly and Yearly, and sort by weight
jonathan1055 Nov 4, 2020
9d7ece1
Rename RepeatFieldsDisplayTest to SchedulerRepeatFormTest
jonathan1055 Nov 4, 2020
7333d57
Fixed SchedulerRepeatFormTest for new field name
jonathan1055 Nov 4, 2020
75e6743
Alter labels and add new options in generateSampleValue()
jonathan1055 Nov 4, 2020
7dd4749
64 coding standards fixed by PHPCBF in 19 files
jonathan1055 Nov 5, 2020
b1cff4f
Add tests for node creation, edit and unpublishing via cron
jonathan1055 Nov 7, 2020
382c2df
Fix 78 coding standards over 17 files
jonathan1055 Nov 8, 2020
e61e367
Remove one-line functions called only once
jonathan1055 Nov 9, 2020
83a4392
Fix coding standards in .module and Validation/Constraint x 2
jonathan1055 Nov 9, 2020
0f04a90
Fix last remaining coding standards faults
jonathan1055 Nov 9, 2020
c19b328
Fix two errors introduced while correcting coding standards
jonathan1055 Nov 9, 2020
8cdb619
Fix coding standard for blank line before @todo which fails at Coder …
jonathan1055 Nov 9, 2020
2acaff5
Correctly order use statements (upcoming coding standard)
jonathan1055 Nov 10, 2020
64a9754
Merge branch '8.x-1.x' into 1345666-repeat
jonathan1055 Nov 17, 2020
6fab0ba
Merge branch '8.x-1.x' into 1345666-repeat, clean 30 Dec 2021
jonathan1055 Dec 30, 2021
58c784c
Move two repeat tests into scheduler_repeat folder structure
jonathan1055 Dec 31, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scheduler_repeat/scheduler_repeat.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: Scheduler Repeat
type: module
description: 'Allows content editors specifying repeating schedule for publishing and unpublishing.'
core: 8.x
core_version_requirement: ^8 || ^9
dependencies:
- drupal:node
- scheduler:scheduler
219 changes: 219 additions & 0 deletions scheduler_repeat/scheduler_repeat.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

/**
* @file
* Scheduler Repeat provides options for repeat publishing and un-publishing.
*/

use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeInterface;
use Drupal\scheduler_repeat\SchedulerRepeaterInterface;

/**
* Implements hook_entity_base_field_info().
*/
function scheduler_repeat_entity_base_field_info(EntityTypeInterface $entity_type) {
if ($entity_type->id() === 'node') {
$fields['scheduler_repeat'] = BaseFieldDefinition::create('scheduler_repeater')
->setLabel(t('Scheduler Repeat'))
->setDisplayOptions('form', [
'type' => 'scheduler_repeater_widget',
'weight' => 31,
])
->setDisplayConfigurable('form', TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->addConstraint('SchedulerRepeat');

return $fields;
}
}

/**
* Implements hook_form_FORM_ID_alter() for node_form().
*/
function scheduler_repeat_form_node_form_alter(&$form, FormStateInterface $form_state) {
if (!isset($form['publish_on']) || !isset($form['unpublish_on'])) {
// Remove the repeat selection field.
// @todo What if scheduler_form_node_form_alter() is invoked AFTER this?
// @todo What if scheduler date fields are configured as hidden?
unset($form['scheduler_repeat']);
return;
}
// Move the repeat widget to the 'scheduler_settings' fieldset.
$form['scheduler_repeat']['#group'] = 'scheduler_settings';
}

/**
* Implements hook_ENTITY_TYPE_presave() for node entities.
*/
function scheduler_repeat_node_presave(EntityInterface $node) {
if (!$repeater = _scheduler_repeat_get_repeater($node)) {
_scheduler_repeat_clear_next_occurence($node);
return;
}

if (!$next_publish_on = _scheduler_repeat_next_publish_on($repeater, $node)) {
_scheduler_repeat_clear_next_occurence($node);
return;
}

if (!$next_unpublish_on = _scheduler_repeat_next_unpublish_on($repeater, $node)) {
_scheduler_repeat_clear_next_occurence($node);
return;
}

if ($next_publish_on > $next_unpublish_on) {
_scheduler_repeat_log_error(
'@repeater repeater calculated conflicting next occurence for node @nid: @from -> @to',
[
'@repeater' => $node->scheduler_repeat->plugin,
'@nid' => $node->id(),
'@from' => date("Y-m-d H:i:s", $next_publish_on),
'@to' => date("Y-m-d H:i:s", $next_unpublish_on),
]
);
return;
}

$node->set('scheduler_repeat', [
'plugin' => $node->scheduler_repeat->plugin,
'next_publish_on' => $next_publish_on,
'next_unpublish_on' => $next_unpublish_on,
]);
}

/**
* Remove the repeat plugin and both 'next' date values.
*
* @param \Drupal\node\NodeInterface $node
* The node object to update.
*/
function _scheduler_repeat_clear_next_occurence(NodeInterface &$node) {
$node->set('scheduler_repeat', [
'plugin' => NULL,
'next_publish_on' => NULL,
'next_unpublish_on' => NULL,
]);
}

/**
* Get the next publish_on date.
*
* If the existing publish_on date is empty, return the stored next_publish_on
* date if availbale. If publish_on date is set, use $repeater to calculate the
* next occurence until the value is in the future.
*
* @param \Drupal\scheduler_repeat\SchedulerRepeaterInterface $repeater
* The repeater plugin.
* @param \Drupal\node\NodeInterface $node
* The node to use.
*
* @return mixed|null
* The next publish_on date
*/
function _scheduler_repeat_next_publish_on(SchedulerRepeaterInterface $repeater, NodeInterface $node) {
if ($node->get('publish_on')->isEmpty()) {
return !empty($node->get('scheduler_repeat')->next_publish_on) ? $node->get('scheduler_repeat')->next_publish_on : NULL;
}

$next_publish_on = $repeater->calculateNextPublishedOn($node->get('publish_on')->value);
$request_time = \Drupal::time()->getRequestTime();
while ($next_publish_on < $request_time) {
$next_publish_on = $repeater->calculateNextPublishedOn($next_publish_on);
}
return $next_publish_on;
}

/**
* Get the next unpublish_on date.
*
* If the existing unpublish_on date is empty, return the stored
* next_unpublish_on date if available. If unpublish_on date is set, use
* $repeater to calculate the next occurence until the value is in the future.
*
* @param \Drupal\scheduler_repeat\SchedulerRepeaterInterface $repeater
* The repeater plugin.
* @param \Drupal\node\NodeInterface $node
* The node to use.
*
* @return mixed|null
* The next unpublish_on date
*/
function _scheduler_repeat_next_unpublish_on(SchedulerRepeaterInterface $repeater, NodeInterface $node) {
if ($node->get('unpublish_on')->isEmpty()) {
return !empty($node->get('scheduler_repeat')->next_unpublish_on) ? $node->get('scheduler_repeat')->next_unpublish_on : NULL;
}

$next_unpublish_on = $repeater->calculateNextUnpublishedOn($node->get('unpublish_on')->value);
$request_time = \Drupal::time()->getRequestTime();
while ($next_unpublish_on < $request_time) {
$next_unpublish_on = $repeater->calculateNextUnpublishedOn($next_unpublish_on);
}
return $next_unpublish_on;
}

/**
* Get the repeat plugin if one is defined for the node.
*
* @param \Drupal\node\NodeInterface $node
* The node object to check.
*
* @return \Drupal\scheduler_repeat\SchedulerRepeaterInterface|null
* The repeat plugin instance.
*/
function _scheduler_repeat_get_repeater(NodeInterface $node) {
if (empty($node->scheduler_repeat->plugin)) {
return NULL;
}
// @todo When we cater for optional associated data, the id can be extracted,
// and the other values added into $plugin_data.
$plugin_id = $node->scheduler_repeat->plugin;
$plugin_data = ['node' => $node];

/** @var \Drupal\scheduler_repeat\SchedulerRepeaterManager $scheduler_repeater_manager */
$scheduler_repeater_manager = \Drupal::service('plugin.manager.scheduler_repeat.repeater');

/** @var \Drupal\scheduler_repeat\SchedulerRepeaterInterface $repeater */
try {
$repeater = $scheduler_repeater_manager->createInstance($plugin_id, $plugin_data);
}
catch (PluginException $e) {
_scheduler_repeat_log_warning('Could not create scheduler repeater instance: @message', ['@message' => $e->getMessage()]);
return NULL;
}

return $repeater;
}

/**
* Write an error to the db log.
*
* @param string $message
* The message text.
* @param array $context
* Context variables for substitution.
*
* @todo This is only called once. Move into scheduler_repeat_node_presave?.
*/
function _scheduler_repeat_log_error(string $message, array $context) {
\Drupal::logger('scheduler_repeat')->error($message, $context);
}

/**
* Write a warning to the db log.
*
* @param string $message
* The message text.
* @param array $context
* Context variables for substitution.
*
* @todo This is only called once. Move into _scheduler_repeat_get_repeater?.
*/
function _scheduler_repeat_log_warning(string $message, array $context) {
\Drupal::logger('scheduler_repeat')->warning($message, $context);
}
9 changes: 9 additions & 0 deletions scheduler_repeat/scheduler_repeat.services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
plugin.manager.scheduler_repeat.repeater:
class: Drupal\scheduler_repeat\SchedulerRepeaterManager
parent: default_plugin_manager
arguments: ['@entity_type.manager', '@config.factory']
scheduler_repeat.event_subscriber:
class: Drupal\scheduler_repeat\EventSubscriber
tags:
- { name: event_subscriber }
47 changes: 47 additions & 0 deletions scheduler_repeat/src/Annotation/SchedulerRepeater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Drupal\scheduler_repeat\Annotation;

use Drupal\Component\Annotation\Plugin;

/**
* Defines repeater plugin.
*
* @code
* @SchedulerRepeater(
* id = "example",
* label = @Translation("Example Label"),
* weight = 1
* )
* @endcode
*
* @Annotation
*/
class SchedulerRepeater extends Plugin {

/**
* ID of the repeat plugin.
*
* @var string
*/
public $id;

/**
* Label of the repeat plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;

/**
* Weight of the repeat plugin.
*
* This is used for sorting the plugins in the form selction list.
*
* @var int
*/
public $weight;

}
55 changes: 55 additions & 0 deletions scheduler_repeat/src/EventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Drupal\scheduler_repeat;

use Drupal\scheduler\SchedulerEvent;
use Drupal\scheduler\SchedulerEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* These events allow modules to react to the Scheduler process being performed.
*
* They are all triggered during Scheduler cron processing with the exception of
* 'pre_publish_immediately' and 'publish_immediately' which are triggered from
* scheduler_node_presave().
*/
class EventSubscriber implements EventSubscriberInterface {

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// The values in the arrays give the function names below.
$events = [];
$events[SchedulerEvents::UNPUBLISH][] = ['unpublish'];
return $events;
}

/**
* Operations to perform after Scheduler unpublishes a node.
*
* @param \Drupal\scheduler\SchedulerEvent $event
* The scheduler event object.
*/
public function unpublish(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();

// The content has now been unpublished so get the stored dates for the next
// period. We do not want to check if a repeat plugin exists, we only need
// to check if the two 'next' dates are available. In future we could set a
// 'stop after' date which would remove the repeat plugin but leave the last
// pair of 'next' dates for use here.
$next_publish_on = $node->get('scheduler_repeat')->next_publish_on;
$next_unpublish_on = $node->get('scheduler_repeat')->next_unpublish_on;
if (empty($next_publish_on) || empty($next_unpublish_on)) {
// Do not have both dates, so cannot set the next period.
return;
}
// Set the new period publish_on and unpublish_on values.
$node->set('publish_on', $next_publish_on);
$node->set('unpublish_on', $next_unpublish_on);
$event->setNode($node);
}

}
13 changes: 13 additions & 0 deletions scheduler_repeat/src/InvalidPluginTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Drupal\scheduler_repeat;

/**
* Defines an exception for the wrong type of repeater plugin.
*
* This is thrown when the Scheduler Repeat manager returns a wrong type of
* repeater plugin.
*
* @see SchedulerRepeatConstraintValidator::initializeRepeaterWithPlugin()
*/
class InvalidPluginTypeException extends \Exception {}
13 changes: 13 additions & 0 deletions scheduler_repeat/src/MissingOptionNodeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Drupal\scheduler_repeat;

/**
* Defines an exception for missing node object.
*
* This is thrown when the node is not passed in the $options parameter.
*
* @see SchedulerRepeaterBase::__construct()
*/
class MissingOptionNodeException extends \Exception {
}
Loading