Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save szeidler/03a9f3a5e8b5e0c507e929c815b16c97 to your computer and use it in GitHub Desktop.
Save szeidler/03a9f3a5e8b5e0c507e929c815b16c97 to your computer and use it in GitHub Desktop.
diff --git a/.travis.yml b/.travis.yml
index 623d2f8..575bec6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -51,7 +51,7 @@ before_script:
# 24 October 2020 this bumped the version to Composer 2. Drupal Core 8.8 has
# plugins that only run with composer-plugin-api ^1.0 so revert to Composer 1.
- if [ "$DRUPAL_CORE" == "8.8.x" ]; then
- travis_retry composer self-update --1;
+ travis_retry composer self-update --1;
fi
- composer --version
@@ -116,9 +116,9 @@ script:
# Run the PHPUnit tests.
- cd $DRUPAL_ROOT
- if [ "$RULES" == "NO" ]; then
- ./vendor/bin/phpunit -c ./core/phpunit.xml.dist --debug ./modules/$MODULE/tests/ --filter '/^((?!rules).)*$/i';
+ ./vendor/bin/phpunit -c ./core/phpunit.xml.dist --debug ./modules/$MODULE/tests/ --filter '/^((?!rules).)*$/i';
else
- ./vendor/bin/phpunit -c ./core/phpunit.xml.dist --debug ./modules/$MODULE/tests/;
+ ./vendor/bin/phpunit -c ./core/phpunit.xml.dist --debug ./modules/$MODULE/tests/;
fi
# Check for coding standards. First show the versions.
diff --git a/.tugboat/config.yml b/.tugboat/config.yml
index f648f4e..530a439 100644
--- a/.tugboat/config.yml
+++ b/.tugboat/config.yml
@@ -21,8 +21,9 @@ services:
composer config repositories.tugboat vcs $TUGBOAT_ROOT
# Now we can require this module, specifing the branch name we created
# above that uses the $TUGBOAT_REPO_ID environment variable.
- composer require drupal/scheduler:dev-$TUGBOAT_REPO_ID --dev
+ composer require drupal/scheduler:dev-$TUGBOAT_REPO_ID
composer require drupal/devel_generate
+ composer require drupal/commerce
# Install Drupal on the site.
vendor/bin/drush \
--yes \
@@ -35,17 +36,22 @@ services:
chgrp -R www-data $DRUPAL_DOCROOT/sites/default/files
chmod 2775 $DRUPAL_DOCROOT/sites/default/files
chmod -R g+w $DRUPAL_DOCROOT/sites/default/files
+
# Enable modules.
+ vendor/bin/drush --yes pm:enable media
vendor/bin/drush --yes pm:enable scheduler devel devel_generate
+ vendor/bin/drush --yes pm:enable commerce_product
composer show drupal/scheduler | egrep 'name |vers'
vendor/bin/drush pml | grep scheduler
composer show drupal/devel | egrep 'name |vers'
vendor/bin/drush pml | grep devel
- # Scheduler settings
- vendor/bin/drush config-set scheduler.settings allow_date_only 1 -y
- vendor/bin/drush config-set scheduler.settings default_time '22:06:00' -y
+ # Scheduler general settings.
+ vendor/bin/drush -y config-set scheduler.settings allow_date_only 1
+ vendor/bin/drush -y config-set scheduler.settings default_time '22:06:00'
vendor/bin/drush config-get scheduler.settings
+
+ # Scheduler content settings.
vendor/bin/drush -y config-set node.type.article third_party_settings.scheduler.publish_enable 1
vendor/bin/drush -y config-set node.type.article third_party_settings.scheduler.unpublish_enable 1
vendor/bin/drush -y config-set node.type.article third_party_settings.scheduler.expand_fieldset 'always'
@@ -53,24 +59,67 @@ services:
vendor/bin/drush -y config-set node.type.article third_party_settings.scheduler.publish_past_date 'schedule'
vendor/bin/drush config-get node.type.article third_party_settings
- # Create roles for each of the two scheduler user permissions.
+ # Scheduler media settings.
+ vendor/bin/drush -y config-set media.type.image third_party_settings.scheduler.publish_enable 1
+ vendor/bin/drush -y config-set media.type.image third_party_settings.scheduler.unpublish_enable 1
+ vendor/bin/drush -y config-set media.type.image third_party_settings.scheduler.expand_fieldset 'always'
+ vendor/bin/drush -y config-set media.type.image third_party_settings.scheduler.fields_display_mode 'fieldset'
+ vendor/bin/drush -y config-set media.type.image third_party_settings.scheduler.publish_past_date 'schedule'
+ vendor/bin/drush config-get media.type.image third_party_settings
+
+ # Scheduler commerce product settings.
+ vendor/bin/drush -y config-set commerce_product.commerce_product_type.default third_party_settings.scheduler.publish_enable 1
+ vendor/bin/drush -y config-set commerce_product.commerce_product_type.default third_party_settings.scheduler.unpublish_enable 1
+ vendor/bin/drush -y config-set commerce_product.commerce_product_type.default third_party_settings.scheduler.publish_past_date 'schedule'
+ vendor/bin/drush -y config-set commerce_product.commerce_product_type.default third_party_settings.scheduler.expand_fieldset 'always'
+ vendor/bin/drush -y config-set commerce_product.commerce_product_type.default third_party_settings.scheduler.fields_display_mode 'fieldset'
+ vendor/bin/drush config-get commerce_product.commerce_product_type.default third_party_settings
+
+ # Media settings.
+ vendor/bin/drush -y config-set media.settings standalone_url 1
+ vendor/bin/drush config-get media.settings
+ vendor/bin/drush -y config-set field.field.media.image.field_media_image required 0
+ vendor/bin/drush config-get field.field.media.image.field_media_image
+
+ # Create roles for each of the scheduler user permissions.
vendor/bin/drush role-create 'content_editor' 'Content Editor'
vendor/bin/drush role-add-perm 'content_editor' 'schedule publishing of nodes'
vendor/bin/drush role-create 'content_viewer' 'Content Viewer'
vendor/bin/drush role-add-perm 'content_viewer' 'view scheduled content'
- # Add permissions for all users.
+ vendor/bin/drush role-create 'media_editor' 'Media Editor'
+ vendor/bin/drush role-add-perm 'media_editor' 'schedule publishing of media'
+ vendor/bin/drush role-create 'media_viewer' 'Media Viewer'
+ vendor/bin/drush role-add-perm 'media_viewer' 'view scheduled media'
+ vendor/bin/drush role-create 'product_editor' 'Product Editor'
+ vendor/bin/drush role-add-perm 'product_editor' 'schedule publishing of commerce_product'
+ vendor/bin/drush role-create 'product_viewer' 'Product Viewer'
+ vendor/bin/drush role-add-perm 'product_viewer' 'view scheduled commerce_product'
+
+ # Add some permissions for all authenticated users.
vendor/bin/drush role-add-perm 'authenticated' "create article content, edit any article content, delete any article content, access content overview, view own unpublished content, switch users"
+ vendor/bin/drush role-add-perm 'authenticated' "create media, update any media, delete any media, access media overview, view own unpublished media"
+ vendor/bin/drush role-add-perm 'authenticated' "create default commerce_product, update any default commerce_product, delete any default commerce_product, access commerce_product overview, view own unpublished commerce_product, administer commerce_store"
# Create users.
- vendor/bin/drush user-create 'Eddy the editor'
- vendor/bin/drush user-add-role 'content_editor' 'Eddy the editor'
- vendor/bin/drush user-create 'Vera the viewer'
- vendor/bin/drush user-add-role 'content_viewer' 'Vera the viewer'
+ vendor/bin/drush user-create 'Eddy the content editor'
+ vendor/bin/drush user-add-role 'content_editor' 'Eddy the content editor'
+ vendor/bin/drush user-create 'Vera the content viewer'
+ vendor/bin/drush user-add-role 'content_viewer' 'Vera the content viewer'
+ vendor/bin/drush user-create 'Madeline the media editor'
+ vendor/bin/drush user-add-role 'media_editor' 'Madeline the media editor'
+ vendor/bin/drush user-create 'Marvin the media viewer'
+ vendor/bin/drush user-add-role 'media_viewer' 'Marvin the media viewer'
+ vendor/bin/drush user-create 'Prodie the product editor'
+ vendor/bin/drush user-add-role 'product_editor' 'Madeline the media editor'
+ vendor/bin/drush user-create 'Proctor the product viewer'
+ vendor/bin/drush user-add-role 'product_viewer' 'Proctor the product viewer'
# Generate content.
- vendor/bin/drush devel-generate-content 5 --bundles=article --authors=1,2 --verbose
+ vendor/bin/drush devel-generate-content 4 --bundles=article --authors=1,2 --verbose
+ vendor/bin/drush devel-generate-media 4 --media-types=image --verbose
# @todo Place the 'Switch users' block in first sidebar.
+ # @todo Add 'content overview' and 'media overview' to tools menu.
build: |
set -eux
diff --git a/README.md b/README.md
index 0e65c3a..9892e84 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,20 @@
[![Build Status](https://travis-ci.org/jonathan1055/scheduler.svg?branch=8.x-1.x)](https://travis-ci.org/jonathan1055/scheduler)
-Scheduler gives content editors the ability to schedule nodes to be published
+Scheduler gives website editors the ability to schedule content to be published
and unpublished at specified dates and times in the future.
-Scheduler provides hooks and events for third-party modules to interact with
-the processing during node edit and during cron publishing and unpublishing.
+Scheduler provides hooks and events for third-party modules to interact with the
+process during content editing and during cron publishing and unpublishing.
-For a fuller description of the module, visit the [project page on Drupal.org](https://drupal.org/project/scheduler)
+A plugin system allows support for any Drupal entity type that has the concept
+of a 'published' status. As at version 8.x-1.4 Node content, Media entities and
+Commerce Products are supported.
## Requirements
- * Scheduler uses the following Drupal 8 Core components:
- Actions, Datetime, Field, Node, Text, Filter, User, System, Views.
+ * Scheduler uses the following Drupal 8 Core components: Actions, Datetime,
+ Field, Node, Media (optional), Text, Filter, User, System, Views.
* There are no special requirements outside core.
@@ -34,44 +36,60 @@ For a fuller description of the module, visit the [project page on Drupal.org](h
If you use core Content Moderation then you should also install this
sub-module, contributed by the folks at [Thunder](https://www.drupal.org/thunder)
+ * [Media](https://www.drupal.org/docs/8/core/modules/media):
+ Core Media items can be scheduled for publishing and unpublishing.
+
+ * [Commerce](https://www.drupal.org/project/commerce):
+ Commerce products can be scheduled for publishing and unpublishing.
+
## Installation
* Install as you would normally install a contributed Drupal module. See:
https://drupal.org/documentation/install/modules-themes/modules-8
for further information.
+ * The [Scheduler project page on Drupal.org](https://drupal.org/project/scheduler)
+ has information regarding versions and Core compatibility.
+
## Configuration
* Configure user permissions via url /admin/people/permissions#module-scheduler
or Administration » People » Permissions
- - View scheduled content list
+ - "Schedule publishing and unpublishing of {type}"
+
+ Users with this permission can enter dates and times for publishing and/or
+ unpublishing, when editing content of types which are Scheduler-enabled.
+
+ - "View scheduled {type}"
Users can always see their own scheduled content, via a tab on their user
page. This permissions grants additional authority to see the full list of
scheduled content by any author, providing the user also has the core
- permission 'access content overview'.
-
- - Schedule content publication
+ permission 'access content overview' and/or 'access media overview'.
- Users with this permission can enter dates and times for publishing and/or
- unpublishing, when editing nodes of types which are Scheduler-enabled.
-
- - Administer scheduler
+ - "Administer scheduler"
This permission allows the user to alter all Scheduler settings. It should
therefore only be given to trusted admin roles.
* Configure the Scheduler global options via /admin/config/content/scheduler
- or Administration » Configuration » Content Authoring
+ or Administration » Configuration » Content Authoring » Scheduler
+
+ - Basic settings: allow a date only and set a default time.
- - Basic settings for date format, allowing date only, setting default time.
+ - Lightweight Cron: This gives sites admins the granularity to run
+ Scheduler's functions only on more frequent crontab jobs than the full
+ Drupal cron run.
- - Lightweight Cron, which gives sites admins the granularity to run
- Scheduler's functions only, on more frequent crontab jobs.
+ * Configure the Scheduler settings per entity type:
+ - Administration » Structure » Content Types » Edit
+ - Administration » Structure » Media Types » Edit
+ - Administration » Commerce » Configuration » Product Types » Edit
- * Configure the Scheduler settings per content type via /admin/structure/types
- or Administration » Structure » Content Types » Edit
+ * The system status report at /admin/reports/status has a Scheduler Timecheck
+ section, giving details of the server time, default site time and current
+ user time.
## Troubleshooting
diff --git a/composer.json b/composer.json
index 7bddcde..fdb0270 100644
--- a/composer.json
+++ b/composer.json
@@ -7,7 +7,8 @@
"require-dev": {
"drupal/rules": "^3",
"drush/drush": "^9.0 || ^10",
- "drupal/devel_generate": "^2.0 || 4.x-dev"
+ "drupal/devel_generate": "^4.0",
+ "drupal/commerce": "^2.0"
},
"repositories": {
"drupal": {
diff --git a/config/install/views.view.scheduler_scheduled_content.yml b/config/install/views.view.scheduler_scheduled_content.yml
index 342784a..327c112 100644
--- a/config/install/views.view.scheduler_scheduled_content.yml
+++ b/config/install/views.view.scheduler_scheduled_content.yml
@@ -1,22 +1,31 @@
langcode: en
status: true
dependencies:
+ config:
+ - system.menu.admin
module:
- node
- - scheduler
- user
+ enforced:
+ module:
+ - scheduler
id: scheduler_scheduled_content
-label: 'Scheduled content'
+label: 'Scheduled Content'
module: views
description: 'Find and manage scheduled content.'
tag: ''
base_table: node_field_revision
base_field: vid
display:
+# ------------------------------------------------------------------------------
+# Default display
+# ------------------------------------------------------------------------------
default:
display_options:
access:
- type: scheduler
+ type: perm
+ options:
+ perm: 'view scheduled content'
cache:
type: tag
options: { }
@@ -985,17 +994,20 @@ display:
- 'languages:language_interface'
- url
- url.query_args
- - user
- 'user.node_grants:view'
+ - user.permissions
max-age: 0
tags: { }
+# ------------------------------------------------------------------------------
+# Overview
+# ------------------------------------------------------------------------------
overview:
display_options:
path: admin/content/scheduled
menu:
type: normal
- title: Scheduled
- description: 'Content scheduled for publishing and unpublishing'
+ title: 'Scheduled Content'
+ description: 'Content that is scheduled for publishing or unpublishing'
expanded: false
parent: system.admin_content
weight: -10
@@ -1020,10 +1032,13 @@ display:
- 'languages:language_interface'
- url
- url.query_args
- - user
- 'user.node_grants:view'
+ - user.permissions
max-age: 0
tags: { }
+# ------------------------------------------------------------------------------
+# User page
+# ------------------------------------------------------------------------------
user_page:
display_options:
path: user/%user/scheduled
@@ -1031,7 +1046,7 @@ display:
type: tab
title: Scheduled
description: ''
- parent: system.admin_content
+ parent: user.page
weight: -10
context: '0'
menu_name: admin
@@ -1047,7 +1062,7 @@ display:
filters: true
filter_groups: true
arguments: false
- access: true
+ access: false
empty: false
arguments:
uid:
@@ -1104,8 +1119,12 @@ display:
tokenize: true
content: 'No scheduled content for user {{ arguments.uid }}'
plugin_id: text_custom
+ access:
+ type: none
+ options: { }
+ display_comment: 'Access to the user view is controllled via a custom RouteSubscriber'
display_plugin: page
- display_title: 'User profile tab'
+ display_title: User
id: user_page
position: 2
cache_metadata:
@@ -1114,7 +1133,6 @@ display:
- 'languages:language_interface'
- url
- url.query_args
- - user
- 'user.node_grants:view'
max-age: 0
tags: { }
diff --git a/config/optional/views.view.scheduler_scheduled_commerce_product.yml b/config/optional/views.view.scheduler_scheduled_commerce_product.yml
new file mode 100644
index 0000000..050e408
--- /dev/null
+++ b/config/optional/views.view.scheduler_scheduled_commerce_product.yml
@@ -0,0 +1,968 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - system.menu.admin
+ enforced:
+ module:
+ - scheduler
+ module:
+ - commerce
+ - commerce_product
+ - commerce_store
+ - user
+id: scheduler_scheduled_commerce_product
+label: 'Scheduled Products'
+module: views
+description: 'Find and manage scheduled commerce products.'
+tag: ''
+base_table: commerce_product_field_data
+base_field: product_id
+display:
+# ------------------------------------------------------------------------------
+# Default display
+# ------------------------------------------------------------------------------
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'view scheduled commerce_product'
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ disable_sql_rewrite: false
+ distinct: true
+ replica: false
+ query_comment: ''
+ query_tags: { }
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Filter
+ reset_button: true
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: full
+ options:
+ items_per_page: 50
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: '‹ previous'
+ next: 'next ›'
+ first: '« first'
+ last: 'last »'
+ style:
+ type: table
+ options:
+ grouping:
+ -
+ field: stores_target_id
+ rendered: true
+ rendered_strip: false
+ row_class: ''
+ default_row_class: true
+ override: true
+ sticky: true
+ caption: ''
+ summary: ''
+ description: ''
+ columns:
+ commerce_product_bulk_form: commerce_product_bulk_form
+ title: title
+ type: type
+ status: status
+ publish_on: publish_on
+ unpublish_on: unpublish_on
+ operations: operations
+ stores_target_id: stores_target_id
+ info:
+ commerce_product_bulk_form:
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ title:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ type:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ status:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ publish_on:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ unpublish_on:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ operations:
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ stores_target_id:
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ default: publish_on
+ empty_table: true
+ row:
+ type: fields
+ fields:
+ commerce_product_bulk_form:
+ id: commerce_product_bulk_form
+ table: commerce_product
+ field: commerce_product_bulk_form
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Bulk update'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ action_title: Action
+ include_exclude: exclude
+ selected_actions: { }
+ entity_type: commerce_product
+ plugin_id: bulk_form
+ title:
+ id: title
+ table: commerce_product_field_data
+ field: title
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Title'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: false
+ ellipsis: false
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ settings:
+ link_to_entity: true
+ 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
+ entity_type: commerce_product
+ entity_field: title
+ plugin_id: field
+ type:
+ id: type
+ table: commerce_product_field_data
+ field: type
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Type
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: target_id
+ type: entity_reference_label
+ settings:
+ link: true
+ group_column: target_id
+ 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
+ hide_single_bundle: true
+ entity_type: commerce_product
+ entity_field: type
+ plugin_id: commerce_entity_bundle
+ status:
+ id: status
+ table: commerce_product_field_data
+ field: status
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Status
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: boolean
+ settings:
+ format: custom
+ format_custom_true: Published
+ format_custom_false: Unpublished
+ 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
+ entity_type: commerce_product
+ entity_field: status
+ plugin_id: field
+ publish_on:
+ id: publish_on
+ table: commerce_product_field_data
+ field: publish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Publish on'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: timestamp
+ settings:
+ date_format: short
+ custom_date_format: ''
+ timezone: ''
+ 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
+ entity_type: commerce_product
+ entity_field: publish_on
+ plugin_id: field
+ unpublish_on:
+ id: unpublish_on
+ table: commerce_product_field_data
+ field: unpublish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Unpublish on'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: timestamp
+ settings:
+ date_format: short
+ custom_date_format: ''
+ timezone: ''
+ 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
+ entity_type: commerce_product
+ entity_field: unpublish_on
+ plugin_id: field
+ operations:
+ id: operations
+ table: commerce_product
+ field: operations
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Operations
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ destination: false
+ entity_type: commerce_product
+ plugin_id: entity_operations
+ stores_target_id:
+ id: stores_target_id
+ table: commerce_product__stores
+ field: stores_target_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Stores
+ exclude: true
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: target_id
+ type: entity_reference_label
+ settings:
+ link: true
+ group_column: target_id
+ 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
+ entity_type: commerce_product
+ entity_field: stores
+ plugin_id: field
+ filters:
+ type:
+ id: type
+ table: commerce_product_field_data
+ field: type
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: type_op
+ label: 'Type'
+ description: ''
+ use_operator: false
+ operator: type_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: type
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ hide_single_bundle: true
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: commerce_product
+ entity_field: type
+ plugin_id: commerce_entity_bundle
+ title:
+ id: title
+ table: commerce_product_field_data
+ field: title
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: contains
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: title_op
+ label: 'Title'
+ description: ''
+ use_operator: false
+ operator: title_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: title
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: commerce_product
+ entity_field: title
+ plugin_id: string
+ status:
+ id: status
+ table: commerce_product_field_data
+ field: status
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: '0'
+ group: 1
+ exposed: true
+ expose:
+ operator_id: ''
+ label: Status
+ description: ''
+ use_operator: false
+ operator: status_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: status
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: true
+ group_info:
+ label: 'Published status'
+ description: ''
+ identifier: status
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items:
+ 1:
+ title: Published
+ operator: '='
+ value: '1'
+ 2:
+ title: Unpublished
+ operator: '='
+ value: '0'
+ entity_type: commerce_product
+ entity_field: status
+ plugin_id: boolean
+ publish_on:
+ id: publish_on
+ table: commerce_product_field_data
+ field: publish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: 'not empty'
+ value:
+ min: ''
+ max: ''
+ value: ''
+ type: date
+ group: 2
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: ''
+ min_placeholder: ''
+ max_placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: commerce_product
+ entity_field: publish_on
+ plugin_id: date
+ unpublish_on:
+ id: unpublish_on
+ table: commerce_product_field_data
+ field: unpublish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: 'not empty'
+ value:
+ min: ''
+ max: ''
+ value: ''
+ type: date
+ group: 2
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: null
+ min_placeholder: null
+ max_placeholder: null
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: commerce_product
+ entity_field: unpublish_on
+ plugin_id: date
+ sorts: { }
+ header: { }
+ footer: { }
+ empty:
+ area_text_custom:
+ id: area_text_custom
+ table: views
+ field: area_text_custom
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ tokenize: false
+ content: 'No scheduled products.'
+ plugin_id: text_custom
+ arguments: { }
+ display_extenders: { }
+ filter_groups:
+ operator: AND
+ groups:
+ 1: AND
+ 2: OR
+ title: 'Scheduled Products'
+ relationships:
+ stores_target_id:
+ id: stores_target_id
+ table: commerce_product__stores
+ field: stores_target_id
+ relationship: none
+ group_type: group
+ admin_label: Store
+ required: false
+ entity_type: commerce_product
+ entity_field: stores
+ plugin_id: standard
+ group_by: true
+ cache_metadata:
+ max-age: 0
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ tags: { }
+# ------------------------------------------------------------------------------
+# Overview
+# ------------------------------------------------------------------------------
+ overview:
+ display_plugin: page
+ id: overview
+ display_title: 'Products Overview'
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: admin/commerce/products/scheduled
+ display_description: 'Overview of all scheduled products, via main commerce product page'
+ menu:
+ type: normal
+ title: 'Scheduled Products'
+ description: 'Commerce products that are scheduled for publishing or unpublishing'
+ expanded: false
+ parent: entity.commerce_product.collection
+ weight: 0
+ context: '0'
+ menu_name: admin
+ tab_options:
+ type: none
+ title: ''
+ description: ''
+ weight: 0
+ cache_metadata:
+ max-age: 0
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ tags: { }
diff --git a/config/optional/views.view.scheduler_scheduled_media.yml b/config/optional/views.view.scheduler_scheduled_media.yml
new file mode 100644
index 0000000..60825d1
--- /dev/null
+++ b/config/optional/views.view.scheduler_scheduled_media.yml
@@ -0,0 +1,1095 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - system.menu.admin
+ module:
+ - media
+ - user
+ enforced:
+ module:
+ - scheduler
+id: scheduler_scheduled_media
+label: 'Scheduled Media'
+module: views
+description: 'Find and manage scheduled media.'
+tag: ''
+base_table: media_field_revision
+base_field: vid
+display:
+# ------------------------------------------------------------------------------
+# Default display
+# ------------------------------------------------------------------------------
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'view scheduled media'
+ 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: Filter
+ reset_button: true
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: full
+ options:
+ items_per_page: 50
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: '‹ previous'
+ next: 'next ›'
+ first: '« first'
+ last: 'last »'
+ style:
+ type: table
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ override: true
+ sticky: true
+ caption: ''
+ summary: ''
+ description: ''
+ columns:
+ media_bulk_form: media_bulk_form
+ name: name
+ bundle: bundle
+ uid: uid
+ status: status
+ publish_on: publish_on
+ unpublish_on: unpublish_on
+ operations: operations
+ info:
+ media_bulk_form:
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ name:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ bundle:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ uid:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ status:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ publish_on:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ unpublish_on:
+ sortable: true
+ default_sort_order: asc
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ operations:
+ align: ''
+ separator: ''
+ empty_column: false
+ responsive: ''
+ default: publish_on
+ empty_table: true
+ row:
+ type: fields
+ fields:
+ media_bulk_form:
+ id: media_bulk_form
+ table: media
+ field: media_bulk_form
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Bulk update'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ action_title: Action
+ include_exclude: exclude
+ selected_actions: { }
+ entity_type: media
+ plugin_id: bulk_form
+ name:
+ id: name
+ table: media_field_revision
+ field: name
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Media Name'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: false
+ ellipsis: false
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ settings:
+ link_to_entity: true
+ 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
+ entity_type: media
+ entity_field: name
+ plugin_id: field
+ bundle:
+ id: bundle
+ table: media_field_data
+ field: bundle
+ relationship: mid
+ group_type: group
+ admin_label: ''
+ label: 'Media type'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: target_id
+ type: entity_reference_label
+ settings:
+ link: true
+ group_column: target_id
+ 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
+ entity_type: media
+ entity_field: bundle
+ plugin_id: field
+ uid:
+ id: uid
+ table: media_field_revision
+ field: uid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Author
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: target_id
+ type: entity_reference_label
+ settings:
+ link: true
+ group_column: target_id
+ 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
+ entity_type: media
+ entity_field: uid
+ plugin_id: field
+ status:
+ id: status
+ table: media_field_revision
+ field: status
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Status
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: boolean
+ settings:
+ format: custom
+ format_custom_true: Published
+ format_custom_false: Unpublished
+ 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
+ entity_type: media
+ entity_field: status
+ plugin_id: field
+ publish_on:
+ id: publish_on
+ table: media_field_revision
+ field: publish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Publish on'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: timestamp
+ settings:
+ date_format: short
+ custom_date_format: ''
+ timezone: ''
+ 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
+ entity_type: media
+ entity_field: publish_on
+ plugin_id: field
+ unpublish_on:
+ id: unpublish_on
+ table: media_field_revision
+ field: unpublish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: 'Unpublish on'
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: timestamp
+ settings:
+ date_format: short
+ custom_date_format: ''
+ timezone: ''
+ 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
+ entity_type: media
+ entity_field: unpublish_on
+ plugin_id: field
+ operations:
+ id: operations
+ table: media_revision
+ field: operations
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: Operations
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: 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_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ destination: false
+ entity_type: media
+ plugin_id: entity_operations
+ filters:
+ latest_revision:
+ id: latest_revision
+ table: media_revision
+ field: latest_revision
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: ''
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: media
+ plugin_id: latest_revision
+ name:
+ id: name
+ table: media_field_revision
+ field: name
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: contains
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: name_op
+ label: 'Media name'
+ description: ''
+ use_operator: false
+ operator: name_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: name
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: media
+ entity_field: name
+ plugin_id: string
+ bundle:
+ id: bundle
+ table: media_field_data
+ field: bundle
+ relationship: mid
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: bundle_op
+ label: Type
+ description: ''
+ use_operator: false
+ operator: bundle_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: bundle
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: media
+ entity_field: bundle
+ plugin_id: bundle
+ status:
+ id: status
+ table: media_field_revision
+ field: status
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: ''
+ label: Status
+ description: ''
+ use_operator: false
+ operator: status_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: status
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: true
+ group_info:
+ label: 'Published status'
+ description: ''
+ identifier: status
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items:
+ 1:
+ title: Published
+ operator: '='
+ value: '1'
+ 2:
+ title: Unpublished
+ operator: '='
+ value: '0'
+ entity_type: media
+ entity_field: status
+ plugin_id: boolean
+ langcode:
+ id: langcode
+ table: media_field_revision
+ field: langcode
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: langcode_op
+ label: Language
+ description: ''
+ use_operator: false
+ operator: langcode_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: langcode
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: media
+ entity_field: langcode
+ plugin_id: language
+ publish_on:
+ id: publish_on
+ table: media_field_revision
+ field: publish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: 'not empty'
+ value:
+ min: ''
+ max: ''
+ value: ''
+ type: date
+ group: 2
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: ''
+ min_placeholder: ''
+ max_placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: media
+ entity_field: publish_on
+ plugin_id: date
+ unpublish_on:
+ id: unpublish_on
+ table: media_field_revision
+ field: unpublish_on
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: 'not empty'
+ value:
+ min: ''
+ max: ''
+ value: ''
+ type: date
+ group: 2
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: null
+ min_placeholder: null
+ max_placeholder: null
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ entity_type: media
+ entity_field: unpublish_on
+ plugin_id: date
+ sorts: { }
+ title: 'Scheduled Media'
+ header: { }
+ footer: { }
+ empty:
+ area_text_custom:
+ id: area_text_custom
+ table: views
+ field: area_text_custom
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ tokenize: false
+ content: 'No scheduled media.'
+ plugin_id: text_custom
+ arguments: { }
+ relationships:
+ mid:
+ id: mid
+ table: media_field_revision
+ field: mid
+ relationship: none
+ group_type: group
+ admin_label: 'Media Field'
+ required: false
+ entity_type: media
+ entity_field: mid
+ plugin_id: standard
+ display_extenders: { }
+ filter_groups:
+ operator: AND
+ groups:
+ 1: AND
+ 2: OR
+ cache_metadata:
+ max-age: 0
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ tags: { }
+# ------------------------------------------------------------------------------
+# Overview
+# ------------------------------------------------------------------------------
+ overview:
+ display_plugin: page
+ id: overview
+ display_title: 'Media Overview'
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: admin/content/media/scheduled
+ display_description: 'Overview of all scheduled media, via main admin content page'
+ menu:
+ type: normal
+ title: 'Scheduled Media'
+ description: 'Media items that are scheduled for publishing or unpublishing'
+ expanded: false
+ parent: 'system.admin_content'
+ weight: 0
+ context: '0'
+ menu_name: admin
+ cache_metadata:
+ max-age: 0
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ - user.permissions
+ tags: { }
+# ------------------------------------------------------------------------------
+# User page
+# ------------------------------------------------------------------------------
+ user_page:
+ display_plugin: page
+ id: user_page
+ display_title: User
+ position: 2
+ display_options:
+ display_extenders: { }
+ display_description: 'Scheduled media on user profile, showing just that user''s scheduled media'
+ path: user/%user/scheduled_media
+ menu:
+ type: tab
+ title: 'Scheduled Media'
+ description: 'Scheduled Media by this user'
+ expanded: false
+ parent: user.page
+ weight: -1
+ context: '0'
+ menu_name: account
+ empty:
+ area_text_custom:
+ id: area_text_custom
+ table: views
+ field: area_text_custom
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ tokenize: true
+ content: 'No scheduled media for user {{ arguments.uid }}'
+ plugin_id: text_custom
+ defaults:
+ empty: false
+ arguments: false
+ access: false
+ arguments:
+ uid:
+ id: uid
+ table: media_field_revision
+ field: uid
+ entity_type: media
+ entity_field: uid
+ plugin_id: numeric
+ access:
+ type: none
+ options: { }
+ display_comment: 'Access to the user view is controllled via a custom RouteSubscriber'
+ cache_metadata:
+ max-age: 0
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
diff --git a/config/schema/scheduler.schema.yml b/config/schema/scheduler.schema.yml
index ed5386d..c565f94 100644
--- a/config/schema/scheduler.schema.yml
+++ b/config/schema/scheduler.schema.yml
@@ -18,43 +18,43 @@ scheduler.settings:
label: 'Date part of the full format'
default_expand_fieldset:
type: string
- label: 'Default value for nodetype setting expand_fieldset'
+ label: 'Default value for entity type setting expand_fieldset'
default_fields_display_mode:
type: string
- label: 'Default value for nodetype setting fields_display_mode'
+ label: 'Default value for entity type setting fields_display_mode'
default_publish_enable:
type: boolean
- label: 'Default value for nodetype setting publish_enable'
+ label: 'Default value for entity type setting publish_enable'
default_publish_past_date:
type: string
- label: 'Default value for nodetype setting publish_past_date'
+ label: 'Default value for entity type setting publish_past_date'
default_publish_past_date_created:
type: boolean
- label: 'Default value for nodetype setting publish_past_date_created'
+ label: 'Default value for entity type setting publish_past_date_created'
default_publish_required:
type: boolean
- label: 'Default value for nodetype setting publish_required'
+ label: 'Default value for entity type setting publish_required'
default_publish_revision:
type: boolean
- label: 'Default value for nodetype setting publish_revision'
+ label: 'Default value for entity type setting publish_revision'
default_publish_touch:
type: boolean
- label: 'Default value for nodetype setting publish_touch'
+ label: 'Default value for entity type setting publish_touch'
default_show_message_after_update:
type: boolean
- label: 'Default value for nodetype setting show_message_after_update'
+ label: 'Default value for entity type setting show_message_after_update'
default_time:
type: string
label: 'Default Scheduling Time. This is used with the option to allow date only'
default_unpublish_enable:
type: boolean
- label: 'Default value for nodetype setting unpublish_enable'
+ label: 'Default value for entity type setting unpublish_enable'
default_unpublish_required:
type: boolean
- label: 'Default value for nodetype setting unpublish_required'
+ label: 'Default value for entity type setting unpublish_required'
default_unpublish_revision:
type: boolean
- label: 'Default value for nodetype setting unpublish_revision'
+ label: 'Default value for entity type setting unpublish_revision'
hide_seconds:
type: boolean
label: 'Hide the seconds on the input control when entering a time'
@@ -71,25 +71,25 @@ scheduler.settings:
type: string
label: 'Time part of the full format'
-node.type.*.third_party.scheduler:
+node.type.*.third_party.scheduler: &third_party_settings_alias
type: mapping
- label: 'Scheduler content type settings'
+ label: 'Scheduler entity type settings'
mapping:
expand_fieldset:
type: string
- label: 'Conditions under which to expand the date input fieldset or vertical tab'
+ label: 'Conditions under which to expand the date input fieldset or vertical tab ("when_required" or "always")'
fields_display_mode:
type: string
- label: 'The way the scheduling fields are displayed in the node form'
+ label: 'The way the scheduling fields are displayed in the edit form ("vertical_tab" or "fieldset")'
publish_enable:
type: boolean
label: 'Enable scheduled publishing'
publish_past_date:
type: string
- label: 'Action to be taken for publication dates in the past'
+ label: 'Action to be taken for publication dates in the past ("error", "publish" or "schedule")'
publish_past_date_created:
type: boolean
- label: 'Change content creation date for past dates to avoid "changed" being earlier than "created"'
+ label: 'Change entity creation date for past dates to avoid "changed" being earlier than "created"'
publish_required:
type: boolean
label: 'Require scheduled publishing'
@@ -98,10 +98,10 @@ node.type.*.third_party.scheduler:
label: 'Create a new revision on publishing'
publish_touch:
type: boolean
- label: 'Change content creation time to match the scheduled publish time'
+ label: 'Change entity creation time to match the scheduled publish time'
show_message_after_update:
type: boolean
- label: 'Show a message after updating content which is scheduled'
+ label: 'Show a message after updating an entity which is scheduled'
unpublish_enable:
type: boolean
label: 'Enable scheduled unpublishing'
@@ -111,3 +111,12 @@ node.type.*.third_party.scheduler:
unpublish_revision:
type: boolean
label: 'Create a new revision on unpublishing'
+
+# Use the saved alias to repeat the same schema for media.type and
+# commerce_product.commerce_product_type. Adding a separate comment betweeen the
+# two definitions below causes a "cannot parse" error.
+media.type.*.third_party.scheduler:
+ *third_party_settings_alias
+
+commerce_product.commerce_product_type.*.third_party.scheduler:
+ *third_party_settings_alias
diff --git a/js/scheduler_default_time.js b/js/scheduler_default_time.js
index cc82a7c..ef9d925 100644
--- a/js/scheduler_default_time.js
+++ b/js/scheduler_default_time.js
@@ -10,7 +10,7 @@
/**
* Provide default time if schedulerDefaultTime is set.
*
- * schedulerDefaultTime is defined in scheduler_form_node_form_alter when the
+ * schedulerDefaultTime is defined in _scheduler_entity_form_alter when the
* user is allowed to enter just a date. The values need to be pre-filled here
* to avoid the browser validation 'please fill in this field' pop-up error
* which is produced before the date widget valueCallback() can set the value.
@@ -18,7 +18,15 @@
*/
Drupal.behaviors.setSchedulerDefaultTime = {
attach: function (context) {
- if (typeof drupalSettings.schedulerDefaultTime !== "undefined") {
+
+ // Drupal.behaviors are called many times per page. Using .once() adds the
+ // class onto the matched DOM element and uses this to prevent it running
+ // on subsequent calls.
+ const $default_time = $(context)
+ .find('#edit-scheduler-settings')
+ .once('default-time-done');
+
+ if ($default_time.length && typeof drupalSettings.schedulerDefaultTime !== "undefined") {
var operations = ["publish", "unpublish"];
operations.forEach(function (value) {
var element = $("input#edit-" + value + "-on-0-value-time", context);
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 1a86e6e..489c4e9 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -24,6 +24,13 @@
<exclude name="Drupal.Commenting.InlineComment.SpacingAfter"/>
</rule>
+ <!-- This rule is disabled in Coder 8.3.10, but undefined variables will -->
+ <!-- be reported when using earlier versions. Hence re-enable the rule so -->
+ <!-- we do not get surprises when testing with other versions. -->
+ <rule ref="DrupalPractice.CodeAnalysis.VariableAnalysis.UndefinedVariable">
+ <severity>5</severity>
+ </rule>
+
<!-- Ignore all files that match these patterns. By default the full file -->
<!-- path is checked, unless type=relative is used. There is an implied * -->
<!-- wildcard at each end and periods and slashes must be escaped using \ -->
diff --git a/scheduler.admin.inc b/scheduler.admin.inc
deleted file mode 100644
index a1b9b0f..0000000
--- a/scheduler.admin.inc
+++ /dev/null
@@ -1,207 +0,0 @@
-<?php
-
-/**
- * @file
- * Administration forms for the Scheduler module.
- */
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\node\NodeTypeInterface;
-
-/**
- * Helper function for the real hook_form_node_type_form_alter().
- *
- * @see scheduler_form_node_type_form_alter()
- */
-function _scheduler_form_node_type_form_alter(array &$form, FormStateInterface $form_state) {
- $config = \Drupal::config('scheduler.settings');
-
- /** @var \Drupal\node\NodeTypeInterface $type */
- $type = $form_state->getFormObject()->getEntity();
-
- $form['#attached']['library'][] = 'scheduler/admin';
- $form['#attached']['library'][] = 'scheduler/vertical-tabs';
-
- $form['scheduler'] = [
- '#type' => 'details',
- '#title' => t('Scheduler'),
- '#weight' => 35,
- '#group' => 'additional_settings',
- ];
-
- // Publishing options.
- $form['scheduler']['publish'] = [
- '#type' => 'details',
- '#title' => t('Publishing'),
- '#weight' => 1,
- '#group' => 'scheduler',
- '#open' => TRUE,
- ];
- $form['scheduler']['publish']['scheduler_publish_enable'] = [
- '#type' => 'checkbox',
- '#title' => t('Enable scheduled publishing for this content type'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable')),
- ];
- $form['scheduler']['publish']['scheduler_publish_touch'] = [
- '#type' => 'checkbox',
- '#title' => t('Change content creation time to match the scheduled publish time'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_touch', $config->get('default_publish_touch')),
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
- ],
- ],
- ];
- $form['scheduler']['publish']['scheduler_publish_required'] = [
- '#type' => 'checkbox',
- '#title' => t('Require scheduled publishing'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_required', $config->get('default_publish_required')),
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
- ],
- ],
- ];
- $form['scheduler']['publish']['scheduler_publish_revision'] = [
- '#type' => 'checkbox',
- '#title' => t('Create a new revision on publishing'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_revision', $config->get('default_publish_revision')),
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
- ],
- ],
- ];
- $form['scheduler']['publish']['advanced'] = [
- '#type' => 'details',
- '#title' => t('Advanced options'),
- '#open' => FALSE,
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
- ],
- ],
- ];
- $form['scheduler']['publish']['advanced']['scheduler_publish_past_date'] = [
- '#type' => 'radios',
- '#title' => t('Action to be taken for publication dates in the past'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_past_date', $config->get('default_publish_past_date')),
- '#options' => [
- 'error' => t('Display an error message - do not allow dates in the past'),
- 'publish' => t('Publish the content immediately after saving'),
- 'schedule' => t('Schedule the content for publication on the next cron run'),
- ],
- ];
- $form['scheduler']['publish']['advanced']['scheduler_publish_past_date_created'] = [
- '#type' => 'checkbox',
- '#title' => t("Change content creation time to match the published time for dates before the content was created"),
- '#description' => t("The created time will only be altered when the scheduled publishing time is earlier than the existing content creation time"),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_past_date_created', $config->get('default_publish_past_date_created')),
- // This option is not relevant if the full 'change creation time' option is
- // selected, or when past dates are not allowed. Hence only show it when
- // the main option is not checked and the past dates option is not 'error'.
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_publish_touch"]' => ['checked' => FALSE],
- ':input[name="scheduler_publish_past_date"]' => ['!value' => 'error'],
- ],
- ],
- ];
-
- // Unpublishing options.
- $form['scheduler']['unpublish'] = [
- '#type' => 'details',
- '#title' => t('Unpublishing'),
- '#weight' => 2,
- '#group' => 'scheduler',
- '#open' => TRUE,
- ];
- $form['scheduler']['unpublish']['scheduler_unpublish_enable'] = [
- '#type' => 'checkbox',
- '#title' => t('Enable scheduled unpublishing for this content type'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable')),
- ];
- $form['scheduler']['unpublish']['scheduler_unpublish_required'] = [
- '#type' => 'checkbox',
- '#title' => t('Require scheduled unpublishing'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'unpublish_required', $config->get('default_unpublish_required')),
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_unpublish_enable"]' => ['checked' => TRUE],
- ],
- ],
- ];
- $form['scheduler']['unpublish']['scheduler_unpublish_revision'] = [
- '#type' => 'checkbox',
- '#title' => t('Create a new revision on unpublishing'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'unpublish_revision', $config->get('default_unpublish_revision')),
- '#states' => [
- 'visible' => [
- ':input[name="scheduler_unpublish_enable"]' => ['checked' => TRUE],
- ],
- ],
- ];
-
- // The 'node_edit_layout' fieldset contains options to alter the layout of
- // node edit pages.
- $form['scheduler']['node_edit_layout'] = [
- '#type' => 'details',
- '#title' => t('Node edit page'),
- '#weight' => 3,
- '#group' => 'scheduler',
- // The #states processing only caters for AND and does not do OR. So to set
- // the state to visible if either of the boxes are ticked we use the fact
- // that logical 'X = A or B' is equivalent to 'not X = not A and not B'.
- '#states' => [
- '!visible' => [
- ':input[name="scheduler_publish_enable"]' => ['!checked' => TRUE],
- ':input[name="scheduler_unpublish_enable"]' => ['!checked' => TRUE],
- ],
- ],
- ];
- $form['scheduler']['node_edit_layout']['scheduler_fields_display_mode'] = [
- '#type' => 'radios',
- '#title' => t('Display scheduling options as'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'fields_display_mode', $config->get('default_fields_display_mode')),
- '#options' => [
- 'vertical_tab' => t('Vertical tab'),
- 'fieldset' => t('Separate fieldset'),
- ],
- '#description' => t('Use this option to specify how the scheduling options will be displayed when editing a node.'),
- ];
- $form['scheduler']['node_edit_layout']['scheduler_expand_fieldset'] = [
- '#type' => 'radios',
- '#title' => t('Expand fieldset or vertical tab'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'expand_fieldset', $config->get('default_expand_fieldset')),
- '#options' => [
- 'when_required' => t('Expand only when a scheduled date exists or when a date is required'),
- 'always' => t('Always open the fieldset or vertical tab'),
- ],
- ];
- $form['scheduler']['node_edit_layout']['scheduler_show_message_after_update'] = [
- '#type' => 'checkbox',
- '#prefix' => '<strong>' . t('Show message') . '</strong>',
- '#title' => t('Show a confirmation message when scheduled content is saved'),
- '#default_value' => $type->getThirdPartySetting('scheduler', 'show_message_after_update', $config->get('default_show_message_after_update')),
- ];
-
- $form['#entity_builders'][] = 'scheduler_form_node_type_form_builder';
-}
-
-/**
- * Entity builder for the node type form with scheduler options.
- */
-function scheduler_form_node_type_form_builder($entity_type, NodeTypeInterface $type, &$form, FormStateInterface $form_state) {
- $type->setThirdPartySetting('scheduler', 'expand_fieldset', $form_state->getValue('scheduler_expand_fieldset'));
- $type->setThirdPartySetting('scheduler', 'fields_display_mode', $form_state->getValue('scheduler_fields_display_mode'));
- $type->setThirdPartySetting('scheduler', 'publish_enable', $form_state->getValue('scheduler_publish_enable'));
- $type->setThirdPartySetting('scheduler', 'publish_past_date', $form_state->getValue('scheduler_publish_past_date'));
- $type->setThirdPartySetting('scheduler', 'publish_past_date_created', $form_state->getValue('scheduler_publish_past_date_created'));
- $type->setThirdPartySetting('scheduler', 'publish_required', $form_state->getValue('scheduler_publish_required'));
- $type->setThirdPartySetting('scheduler', 'publish_revision', $form_state->getValue('scheduler_publish_revision'));
- $type->setThirdPartySetting('scheduler', 'publish_touch', $form_state->getValue('scheduler_publish_touch'));
- $type->setThirdPartySetting('scheduler', 'show_message_after_update', $form_state->getValue('scheduler_show_message_after_update'));
- $type->setThirdPartySetting('scheduler', 'unpublish_enable', $form_state->getValue('scheduler_unpublish_enable'));
- $type->setThirdPartySetting('scheduler', 'unpublish_required', $form_state->getValue('scheduler_unpublish_required'));
- $type->setThirdPartySetting('scheduler', 'unpublish_revision', $form_state->getValue('scheduler_unpublish_revision'));
-}
diff --git a/scheduler.api.php b/scheduler.api.php
index 3304947..bbd6c65 100644
--- a/scheduler.api.php
+++ b/scheduler.api.php
@@ -3,6 +3,12 @@
/**
* @file
* API documentation for the Scheduler module.
+ *
+ * Each of these hook functions has a general version which is invoked for all
+ * entity types, and a specific variant with _{type}_ in the name, invoked when
+ * processing that specific entity type.
+ *
+ * phpcs:disable DrupalPractice.CodeAnalysis.VariableAnalysis.UndefinedVariable
*/
/**
@@ -11,229 +17,323 @@
*/
/**
- * Hook function to add node ids to the list being processed.
+ * Hook function to add entity ids to the list being processed.
*
- * This hook allows modules to add more node ids into the list being processed
+ * This hook allows modules to add more entity ids into the list being processed
* in the current cron run. It is invoked during cron runs only. This function
* is retained for backwards compatibility but is superceded by the more
- * flexible hook_scheduler_nid_list_alter().
+ * flexible hook_scheduler_list_alter().
*
- * @param string $action
- * The action being done to the node - 'publish' or 'unpublish'.
+ * @param string $process
+ * The process being done - 'publish' or 'unpublish'.
+ * @param string $entityTypeId
+ * The type of the entity being processed, for example 'node' or 'media'.
*
* @return array
- * Array of node ids to add to the existing list of nodes to be processed.
+ * Array of ids to add to the existing list to be processed. Duplicates are
+ * removed when all hooks have been invoked.
*/
-function hook_scheduler_nid_list($action) {
- $nids = [];
- // Do some processing to add new node ids into $nids.
- return $nids;
+function hook_scheduler_list($process, $entityTypeId) {
+ $ids = [];
+ // Do some processing to add ids to the $ids array.
+ return $ids;
}
/**
- * Hook function to manipulate the list of nodes being processed.
+ * Entity-type specific version of hook_scheduler_list().
*
- * This hook allows modules to add or remove node ids from the list being
- * processed in the current cron run. It is invoked during cron runs only. It
- * can do everything that hook_scheduler_nid_list() does, plus more.
+ * The parameters and return value match the general variant of this hook. The
+ * $entityTypeId parameter is included for ease and consistency, but is not
+ * strictly necessary as it will always match the TYPE in the function name.
+ */
+function hook_scheduler_TYPE_list($process, $entityTypeId) {
+}
+
+/**
+ * Hook function to manipulate the list of entity ids being processed.
*
- * @param array $nids
- * An array of node ids being processed.
- * @param string $action
- * The action being done to the node - 'publish' or 'unpublish'.
+ * This hook allows modules to add or remove entity ids from the list being
+ * processed in the current cron run. It is invoked during cron runs only.
*
- * @return array
- * The full array of node ids to process, adjusted as required.
+ * @param array $ids
+ * The array of entity ids being processed.
+ * @param string $process
+ * The process being done - 'publish' or 'unpublish'.
+ * @param string $entityTypeId
+ * The type of the entity being processed, for example 'node' or 'media'.
*/
-function hook_scheduler_nid_list_alter(array &$nids, $action) {
- // Do some processing to add or remove node ids.
- return $nids;
+function hook_scheduler_list_alter(array &$ids, $process, $entityTypeId) {
+ if ($process == 'publish' && $some_condition) {
+ // Set a publish_on date and add the id.
+ $entity->set('publish_on', \Drupal::time()->getRequestTime())->save();
+ $ids[] = $id;
+ }
+ if ($process == 'unpublish' && $some_other_condition) {
+ // Remove the id.
+ $ids = array_diff($ids, [$id]);
+ }
+ // No return is necessary because $ids is passed by reference. Duplicates are
+ // removed when all hooks have been invoked.
}
/**
- * Hook function to deny or allow a node to be published.
+ * Entity-type specific version of hook_scheduler_list_alter().
*
- * This hook gives modules the ability to prevent publication of a node at the
- * scheduled time. The node may be scheduled, and an attempt to publish it will
- * be made during the first cron run after the publishing time. If this hook
- * returns FALSE the node will not be published. Attempts at publishing will
- * continue on each subsequent cron run until this hook returns TRUE.
+ * The parameters match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_list_alter(array &$ids, $process, $entityTypeId) {
+}
+
+/**
+ * Hook function to deny publishing of an entity.
*
- * @param \Drupal\node\NodeInterface $node
- * The scheduled node that is about to be published.
+ * This hook gives modules the ability to prevent publication of an entity. The
+ * entity may be scheduled, and an attempt to publish it will be made during the
+ * first cron run after the publishing time. If any implementation of this hook
+ * function returns FALSE the entity will not be published. Attempts to publish
+ * will continue on each subsequent cron run, and the entity will be published
+ * when no hook prevents it.
*
- * @return bool
- * TRUE if the node can be published, FALSE if it should not be published.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The scheduled entity that is about to be published.
+ *
+ * @return bool|null
+ * FALSE if the entity should not be published. TRUE or NULL will not affect
+ * the outcome.
*/
-function hook_scheduler_allow_publishing(NodeInterface $node) {
- // If there is no 'approved' field do nothing to change the result.
- if (!isset($node->field_approved)) {
- $allowed = TRUE;
- }
- else {
- // Prevent publication of nodes that do not have the 'Approved for
- // publication by the CEO' checkbox ticked.
- $allowed = !empty($node->field_approved->value);
-
- // If publication is denied then inform the user why. This message will be
- // displayed during node edit and save.
- if (!$allowed) {
- \Drupal::messenger()->addMessage(t('The content will only be published after approval by the CEO.'), 'status', FALSE);
+function hook_scheduler_publishing_allowed(EntityInterface $entity) {
+ // Do some logic here ...
+ $allowed = !empty($entity->field_approved->value);
+ // If publication is denied then inform the user why. This message will be
+ // displayed during entity edit and save.
+ if (!$allowed) {
+ \Drupal::messenger()->addMessage(t('The content will only be published after approval.'), 'status', FALSE);
+ // If the time is in the past it means that the action has been prevented,
+ // so write a dblog message to show this.
+ if ($entity->publish_on->value <= \Drupal::time()->getRequestTime()) {
+ \Drupal::logger('scheduler_api_test')->warning('Publishing of "%title" is prevented until approved.', [
+ '%title' => $entity->label(),
+ 'link' => $entity->id() ? $entity->toLink(t('View'))->toString() : '',
+ ]);
}
}
-
return $allowed;
}
/**
- * Hook function to deny or allow a node to be unpublished.
+ * Entity-type specific version of hook_scheduler_publishing_allowed().
+ *
+ * The parameters and return match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_publishing_allowed(EntityInterface $entity) {
+}
+
+/**
+ * Hook function to deny unpublishing of an entity.
*
- * This hook gives modules the ability to prevent unpblication of a node at the
- * scheduled time. The node may be scheduled, and an attempt to unpublish it
- * will be made during the first cron run after the unpublishing time. If this
- * hook returns FALSE the node will not be unpublished. Attempts at unpublishing
- * will continue on each subsequent cron run until this hook returns TRUE.
+ * This hook gives modules the ability to prevent unpublication of an entity.
+ * The entity may be scheduled, and an attempt to unpublish it will be made
+ * during the first cron run after the unpublishing time. If any implementation
+ * of this hook function returns FALSE the entity will not be unpublished.
+ * Attempts to unpublish will continue on each subsequent cron run, and the
+ * entity will be unpublished when no hook prevents it.
*
- * @param \Drupal\node\NodeInterface $node
- * The scheduled node that is about to be unpublished.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The scheduled entity that is about to be unpublished.
*
- * @return bool
- * TRUE if the node can be unpublished, FALSE if it should not be unpublished.
+ * @return bool|null
+ * FALSE if the entity should not be unpublished. TRUE or NULL will not affect
+ * the outcome.
*/
-function hook_scheduler_allow_unpublishing(NodeInterface $node) {
+function hook_scheduler_unpublishing_allowed(EntityInterface $entity) {
$allowed = TRUE;
-
- // Prevent unpublication of competition entries if not all prizes have been
- // claimed.
- if ($node->getType() == 'competition' && $items = $node->field_competition_prizes->getValue()) {
+ // Prevent unpublication of competitions if not all prizes have been claimed.
+ if ($entity->getEntityTypeId() == 'competition' && $items = $entity->field_competition_prizes->getValue()) {
$allowed = (bool) count($items);
// If unpublication is denied then inform the user why. This message will be
- // displayed during node edit and save.
+ // displayed during entity edit and save.
if (!$allowed) {
- \Drupal::messenger()->addMessage(t('The competition will only be unpublished after all prizes have been claimed by the winners.'));
+ \Drupal::messenger()->addMessage(t('The competition will only be unpublished after all prizes have been claimed.'));
}
}
-
return $allowed;
}
+/**
+ * Entity-type specific version of hook_scheduler_unpublishing_allowed().
+ *
+ * The parameters and return match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_unpublishing_allowed(EntityInterface $entity) {
+}
+
/**
* Hook function to hide the Publish On field.
*
- * This hook is called from scheduler_form_node_form_alter(). It gives modules
- * the ability to hide the scheduler publish_on input field on the node edit
- * form. Note that it does not give the ability to force the field to be
- * displayed, as that could override a more significant setting. It can only be
- * used to hide the field.
+ * This hook is called from scheduler_form_alter() when adding or editing an
+ * entity. It gives modules the ability to hide the scheduler publish_on input
+ * field so that a date may not be entered or changed. Note that it does not
+ * give the ability to force the field to be displayed, as that could override a
+ * more significant setting. It can only be used to hide the field.
*
* This hook was introduced for scheduler_content_moderation_integration.
+ * See https://www.drupal.org/project/scheduler/issues/2798689
*
* @param array $form
* An associative array containing the structure of the form, as used in
* hook_form_alter().
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form, as used in hook_form_alter().
- * @param \Drupal\node\NodeInterface $node
- * The $node object of the node being editted.
- *
- * @see https://www.drupal.org/project/scheduler/issues/2798689
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object being added or edited.
*
* @return bool
* TRUE to hide the publish_on field.
* FALSE or NULL to leave the setting unchanged.
*/
-function hook_scheduler_hide_publish_on_field(array $form, FormStateInterface $form_state, NodeInterface $node) {
- return FALSE;
+function hook_scheduler_hide_publish_date(array $form, FormStateInterface $form_state, EntityInterface $entity) {
+ if ($some_condition) {
+ return TRUE;
+ }
+}
+
+/**
+ * Entity-type specific version of hook_scheduler_hide_publish_date().
+ *
+ * The parameters and return match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_hide_publish_date(array $form, FormStateInterface $form_state, EntityInterface $entity) {
}
/**
* Hook function to hide the Unpublish On field.
*
- * This hook is called from scheduler_form_node_form_alter(). It gives modules
- * the ability to hide the scheduler unpublish_on input field on the node edit
- * form. Note that it does not give the ability to force the field to be
- * displayed, as that could override a more significant setting. It can only be
- * used to hide the field.
+ * This hook is called from scheduler_form_alter() when adding or editing an
+ * entity. It gives modules the ability to hide the scheduler unpublish_on input
+ * field so that a date may not be entered or changed. Note that it does not
+ * give the ability to force the field to be displayed, as that could override a
+ * more significant setting. It can only be used to hide the field.
*
* This hook was introduced for scheduler_content_moderation_integration.
+ * See https://www.drupal.org/project/scheduler/issues/2798689
*
* @param array $form
* An associative array containing the structure of the form, as used in
* hook_form_alter().
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form, as used in hook_form_alter().
- * @param \Drupal\node\NodeInterface $node
- * The $node object of the node being editted.
- *
- * @see https://www.drupal.org/project/scheduler/issues/2798689
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object being added or edited.
*
* @return bool
* TRUE to hide the unpublish_on field.
* FALSE or NULL to leave the setting unchanged.
*/
-function hook_scheduler_hide_unpublish_on_field(array $form, FormStateInterface $form_state, NodeInterface $node) {
- return FALSE;
+function hook_scheduler_hide_unpublish_date(array $form, FormStateInterface $form_state, EntityInterface $entity) {
+ if ($some_condition) {
+ return TRUE;
+ }
}
/**
- * Hook function to process the publish action for a node.
+ * Entity-type specific version of hook_scheduler_hide_unpublish_date().
*
- * This hook is called from schedulerManger::publish() and allows oher modules
- * to process the publish action on a node during a cron run. The other module
+ * The parameters and return match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_hide_unpublish_date(array $form, FormStateInterface $form_state, EntityInterface $entity) {
+}
+
+/**
+ * Hook function to process the publish action for an entity.
+ *
+ * This hook is called from schedulerManger::publish() and allows other modules
+ * to process the publish action on the entity during a cron run. That module
* may require different functionality to be executed instead of the default
- * publish process. If none of the invoked hook functions return a TRUE value
- * then Scheduler will process the node using the default publish action, just
- * as if no other hooks had been called.
+ * publish action. If all of the invoked hook functions return 0 then Scheduler
+ * will process the entity using the default publish action, just as if no hook
+ * functions had been called.
*
* This hook was introduced for scheduler_content_moderation_integration.
+ * See https://www.drupal.org/project/scheduler/issues/2798689
*
- * @param \Drupal\node\NodeInterface $node
- * The $node object of the node being published.
- *
- * @see https://www.drupal.org/project/scheduler/issues/2798689
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The scheduled entity that is about to be published.
*
* @return int
- * 1 if this function has published the node or performed other such action
+ * 1 if this function has published the entity or performed other such action
* meaning that Scheduler should NOT process the default publish action.
* 0 if nothing has been done and Scheduler should process the default publish
* action just as if this hook function did not exist.
* -1 if an error has occurred and Scheduler should abandon processing this
- * node with no further action and move on to the next one.
+ * entity with no further action and move on to the next one.
*/
-function hook_scheduler_publish_action(NodeInterface $node) {
+function hook_scheduler_publish_process(EntityInterface $entity) {
+ if ($big_problem) {
+ // Throw an exception here.
+ return -1;
+ }
+ if ($some_condition) {
+ // Do the publish processing here on the $entity.
+ $entity->setSomeValue();
+ return 1;
+ }
return 0;
}
/**
- * Hook function to process the unpublish action for a node.
+ * Entity-type specific version of hook_scheduler_publish_process().
*
- * This hook is called from schedulerManger::unpublish() and allows oher modules
- * to process the unpublish action on a node during a cron run. The other module
- * may require different functionality to be executed instead of the default
- * unpublish process. If none of the invoked hook functions return a TRUE value
- * then Scheduler will process the node using the default unpublish action, just
- * as if no other hooks had been called.
+ * The parameters and return match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_publish_process(EntityInterface $entity) {
+}
+
+/**
+ * Hook function to process the unpublish action for an entity.
*
- * This hook was introduced for scheduler_content_moderation_integration.
+ * This hook is called from schedulerManger::unpublish() and allows other
+ * modules to process the unpublish action on the entity during a cron run. That
+ * module may require different functionality to be executed instead of the
+ * default unpublish action. If all of the invoked hook functions return 0 then
+ * Scheduler will process the entity using the default unpublish action, just as
+ * if no hook functions had been called.
*
- * @param \Drupal\node\NodeInterface $node
- * The $node object of the node being unpublished.
+ * This hook was introduced for scheduler_content_moderation_integration.
+ * See https://www.drupal.org/project/scheduler/issues/2798689
*
- * @see https://www.drupal.org/project/scheduler/issues/2798689
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The scheduled entity that is about to be unpublished.
*
* @return int
- * 1 if this function has published the node or performed other such action
- * meaning that Scheduler should NOT process the default publish action.
- * 0 if nothing has been done and Scheduler should process the default publish
- * action just as if this hook function did not exist.
+ * 1 if this function has unpublished the entity or performed other actions
+ * meaning that Scheduler should NOT process the default unpublish action.
+ * 0 if nothing has been done and Scheduler should process the default
+ * unpublish action just as if this hook function did not exist.
* -1 if an error has occurred and Scheduler should abandon processing this
- * node with no further action and move on to the next one.
+ * entity with no further action and move on to the next one.
*/
-function hook_scheduler_unpublish_action(NodeInterface $node) {
+function hook_scheduler_unpublish_process(EntityInterface $entity) {
+ if ($big_problem) {
+ // Throw an exception here.
+ return -1;
+ }
+ if ($some_condition) {
+ // Do the unpublish processing here on the $entity.
+ $entity->setSomeValue();
+ return 1;
+ }
return 0;
}
+/**
+ * Entity-type specific version of hook_scheduler_unpublish_process().
+ *
+ * The parameters and return match the general variant of this hook.
+ */
+function hook_scheduler_TYPE_unpublish_process(EntityInterface $entity) {
+}
+
/**
* @} End of "addtogroup hooks".
*/
diff --git a/scheduler.drush.inc b/scheduler.drush.inc
index 09ebbfe..505e1a5 100644
--- a/scheduler.drush.inc
+++ b/scheduler.drush.inc
@@ -2,7 +2,9 @@
/**
* @file
- * Drush commands for Scheduler.
+ * Drush 8 commands for Scheduler.
+ *
+ * For Drush 9+ the commands are in src/Commands/SchedulerCommands.php.
*/
/**
@@ -12,7 +14,7 @@ function scheduler_drush_command() {
$items = [];
$items['scheduler-cron'] = [
- 'description' => 'Lightweight cron to process scheduler tasks.',
+ 'description' => 'Lightweight cron to process Scheduler tasks.',
'core' => ['8+'],
'aliases' => ['sch-cron'],
'category' => 'scheduler',
@@ -21,6 +23,12 @@ function scheduler_drush_command() {
],
];
+ $items['scheduler-entity-update'] = [
+ 'description' => 'Entity Update - Add missing Scheduler db fields for entities covered by plugins.',
+ 'core' => ['8+'],
+ 'aliases' => ['sch-ent-upd'],
+ 'category' => 'scheduler',
+ ];
return $items;
}
@@ -32,3 +40,12 @@ function drush_scheduler_cron() {
$nomsg = drush_get_option('nomsg', NULL);
$nomsg ? NULL : \Drupal::messenger()->addMessage(t('Scheduler lightweight cron completed'));
}
+
+/**
+ * Call SchedulerManager entityUpdate() to add missing db fields.
+ */
+function drush_scheduler_entity_update() {
+ $result = \Drupal::service('scheduler.manager')->entityUpdate();
+ $updated = $result ? implode(', ', $result) : t('nothing to update');
+ \Drupal::messenger()->addMessage(t('Scheduler entity update - @updated', ['@updated' => $updated]));
+}
diff --git a/scheduler.info.yml b/scheduler.info.yml
index 26163ed..3bffb91 100644
--- a/scheduler.info.yml
+++ b/scheduler.info.yml
@@ -13,6 +13,8 @@ dependencies:
test_dependencies:
- rules:rules
- devel:devel_generate
+ - drupal:media
+ - commerce:commerce
libraries:
- scheduler/admin
- vertical-tabs
diff --git a/scheduler.install b/scheduler.install
index 3af878b..b9ceb36 100644
--- a/scheduler.install
+++ b/scheduler.install
@@ -78,14 +78,6 @@ function scheduler_install() {
->save();
}
-/**
- * Implements hook_uninstall().
- */
-function scheduler_uninstall() {
- // Delete the scheduled content view.
- \Drupal::configFactory()->getEditable('views.view.scheduler_scheduled_content')->delete();
-}
-
/**
* Reset date and time formats to default.
*/
@@ -127,3 +119,37 @@ function scheduler_update_8102() {
return t('The "Scheduled" tab is now a "Scheduled content" sub-task under the "Content" tab');
}
}
+
+/**
+ * Add date fields to any newly supported entity types.
+ */
+function scheduler_update_8103() {
+ // When the media module is already installed and Scheduler is then upgraded
+ // to a version which includes the entity plugins, this update function will
+ // add the missing db fields.
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+ if ($result = $scheduler_manager->entityUpdate()) {
+ return t('Added Scheduler date fields to the following entity types: %updated.', [
+ '%updated' => implode(', ', $result),
+ ]);
+ }
+ else {
+ return t('No database fields had to be added.');
+ }
+}
+
+/**
+ * Refresh views for supported entity types.
+ */
+function scheduler_update_8104() {
+ // The scheduled content view needs to be refreshed from source when upgrading
+ // to the entity plugin version of Scheduler. If the media or commerce modules
+ // are already enabled this will also load those new views from source.
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+ if ($result = $scheduler_manager->viewsUpdate()) {
+ return t('Updated views: %updated.', ['%updated' => implode(', ', $result)]);
+ }
+ else {
+ return t('No views require updating.');
+ }
+}
diff --git a/scheduler.libraries.yml b/scheduler.libraries.yml
index 861dca8..44533b1 100644
--- a/scheduler.libraries.yml
+++ b/scheduler.libraries.yml
@@ -9,3 +9,7 @@ default-time:
js/scheduler_default_time.js: {}
dependencies:
- core/jquery
+admin:
+ css:
+ component:
+ css/styling.css: { }
diff --git a/scheduler.links.task.yml b/scheduler.links.task.yml
index d1afd7e..fed60f7 100644
--- a/scheduler.links.task.yml
+++ b/scheduler.links.task.yml
@@ -25,3 +25,35 @@ scheduler.scheduled_content:
parent_id: system.admin_content
# Overview seems to have weight 0 and moderated content is weight 1.
weight: 5
+
+scheduler.media_overview:
+ # This is added so that we get an 'overview' sub-task link alongside the
+ # 'scheduled media' sub-task link.
+ title: 'Overview'
+ route_name: entity.media.collection
+ parent_id: entity.media.collection
+
+scheduler.scheduled_media:
+ title: 'Scheduled media'
+ route_name: view.scheduler_scheduled_media.overview
+ parent_id: entity.media.collection
+ weight: 5
+
+scheduler.commerce_products:
+ # This separate route for the parent_id is needed, because using
+ # entity.commerce_product.collection does not render the local task links.
+ route_name: scheduler.commerce_products
+ base_route: scheduler.commerce_products
+
+scheduler.commerce_product.collection:
+ # This is added so that we get an 'overview' sub-task link alongside the
+ # 'scheduled products' sub-task link.
+ title: 'Overview'
+ route_name: entity.commerce_product.collection
+ parent_id: scheduler.commerce_products
+
+scheduler.scheduled_products:
+ title: 'Scheduled Products'
+ route_name: view.scheduler_scheduled_commerce_product.overview
+ parent_id: scheduler.commerce_products
+ weight: 5
diff --git a/scheduler.module b/scheduler.module
index ff698b6..4e959da 100644
--- a/scheduler.module
+++ b/scheduler.module
@@ -2,7 +2,7 @@
/**
* @file
- * Scheduler publishes and unpublishes nodes on dates specified by the user.
+ * Scheduler publishes and unpublishes entities on dates specified by the user.
*/
use Drupal\Component\Utility\Xss;
@@ -13,9 +13,6 @@ use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
-use Drupal\node\Entity\NodeType;
-use Drupal\scheduler\SchedulerEvent;
-use Drupal\scheduler\SchedulerEvents;
/**
* Implements hook_help().
@@ -25,18 +22,13 @@ function scheduler_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.scheduler':
$output = '<h3>' . t('About') . '</h3>';
- $output .= '<p>' . t('The Scheduler module provides the functionality for automatic publishing and unpublishing of nodes at specified future dates.') . '</p>';
+ $output .= '<p>' . t('The Scheduler module provides the functionality for automatic publishing and unpublishing of entities, such and nodes and media items, at specified future dates.') . '</p>';
$output .= '<p>' . t('You can read more in the <a href="@readme">readme</a> file or our <a href="@project">project page on Drupal.org</a>.', [
'@readme' => $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'scheduler') . '/README.md',
'@project' => 'https://drupal.org/project/scheduler',
]) . '</p>';
break;
- case 'scheduler.admin_form':
- $output = '<p>' . t('Most of the Scheduler options are set for each different content type, and are accessed via the <a href="@link">admin content type</a> list.', ['@link' => Url::fromRoute('entity.node_type.collection')->toString()]) . '</br>';
- $output .= t('The options and settings below are common to all content types.') . '</p>';
- break;
-
case 'scheduler.cron_form':
$base_url = $GLOBALS['base_url'];
$access_key = \Drupal::config('scheduler.settings')->get('lightweight_cron_access_key');
@@ -54,54 +46,81 @@ function scheduler_help($route_name, RouteMatchInterface $route_match) {
}
/**
- * Implements hook_form_FORM_ID_alter() for node_type_form().
+ * Implements hook_form_alter().
*/
-function scheduler_form_node_type_form_alter(array &$form, FormStateInterface $form_state) {
- // Load the real code only when needed.
- module_load_include('inc', 'scheduler', 'scheduler.admin');
- _scheduler_form_node_type_form_alter($form, $form_state);
+function scheduler_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+
+ if (in_array($form_id, $scheduler_manager->getEntityFormIds())) {
+ _scheduler_entity_form_alter($form, $form_state, $form_id);
+ }
+ elseif (in_array($form_id, $scheduler_manager->getEntityTypeFormIds())) {
+ _scheduler_entity_type_form_alter($form, $form_state, $form_id);
+ }
+ elseif ($entityTypeId = array_search($form_id, $scheduler_manager->getDevelGenerateFormIds())) {
+ // Devel Generate forms are different from the other types above. There is
+ // only one form id per entity type, but also no direct way to get the
+ // entity from the form. Hence we add the entityTypeId as a key in the array
+ // of returned possible form ids, and pass that on to the helper function.
+ _scheduler_devel_generate_form_alter($form, $form_state, $form_id, $entityTypeId);
+ }
}
/**
- * Implements hook_form_FORM_ID_alter() for node_form().
+ * Form alter handling for entity forms - add and edit.
*/
-function scheduler_form_node_form_alter(&$form, FormStateInterface $form_state) {
+function _scheduler_entity_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ // Get the form display object. If this does not exist because the form is
+ // prevented from displaying, such as in Commerce Add Product before any store
+ // has been created, we can not (and do not need) to do anything, so exit.
+ if (!$display = $form_state->getFormObject()->getFormDisplay($form_state)) {
+ return;
+ }
+
$config = \Drupal::config('scheduler.settings');
- /** @var \Drupal\node\NodeTypeInterface $type */
- $type = $form_state->getFormObject()->getEntity()->type->entity;
- $publishing_enabled = $type->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'));
- $unpublishing_enabled = $type->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'));
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ $entity = $form_state->getFormObject()->getEntity();
+ $entityTypeId = $entity->getEntityTypeId();
+
+ $publishing_enabled = $scheduler_manager->getThirdPartySetting($entity, 'publish_enable', $config->get('default_publish_enable'));
+ $unpublishing_enabled = $scheduler_manager->getThirdPartySetting($entity, 'unpublish_enable', $config->get('default_unpublish_enable'));
+
+ // If neither publishing nor unpublishing are enabled then there is nothing to
+ // do so remove the fields from the form and exit early.
+ if (!$publishing_enabled && !$unpublishing_enabled) {
+ unset($form['publish_on']);
+ unset($form['unpublish_on']);
+ return;
+ }
// Determine if the scheduler fields have been set to hidden (disabled).
- $display = $form_state->getFormObject()->getFormDisplay($form_state);
$publishing_displayed = !empty($display->getComponent('publish_on'));
$unpublishing_displayed = !empty($display->getComponent('unpublish_on'));
- /** @var \Drupal\node\NodeInterface $node */
- $node = $form_state->getFormObject()->getEntity();
-
- // Invoke all implementations of hook_scheduler_hide_publish_on_field() to
- // allow other modules to hide the field on the node edit form.
+ // Invoke all implementations of hook_scheduler_hide_publish_date() and
+ // hook_scheduler_{type}_hide_publish_date() to allow other modules to hide
+ // the field on the entity edit form.
if ($publishing_enabled && $publishing_displayed) {
- $hook = 'scheduler_hide_publish_on_field';
- foreach (\Drupal::moduleHandler()->getImplementations($hook) as $module) {
- $function = $module . '_' . $hook;
- $publishing_displayed = ($function($form, $form_state, $node) !== TRUE) && $publishing_displayed;
+ $hook_implementations = $scheduler_manager->getHookImplementations('hide_publish_date', $entity);
+ foreach ($hook_implementations as $function) {
+ $publishing_displayed = ($function($form, $form_state, $entity) !== TRUE) && $publishing_displayed;
}
}
- // Invoke all implementations of hook_scheduler_hide_unpublish_on_field() to
- // allow other modules to hide the field on the node edit form.
+ // Invoke all implementations of hook_scheduler_hide_unpublish_date() and
+ // hook_scheduler_{type}_hide_unpublish_date() to allow other modules to hide
+ // the field on the entity edit form.
if ($unpublishing_enabled && $unpublishing_displayed) {
- $hook = 'scheduler_hide_unpublish_on_field';
- foreach (\Drupal::moduleHandler()->getImplementations($hook) as $module) {
- $function = $module . '_' . $hook;
- $unpublishing_displayed = ($function($form, $form_state, $node) !== TRUE) && $unpublishing_displayed;
+ $hook_implementations = $scheduler_manager->getHookImplementations('hide_unpublish_date', $entity);
+ foreach ($hook_implementations as $function) {
+ $unpublishing_displayed = ($function($form, $form_state, $entity) !== TRUE) && $unpublishing_displayed;
}
}
// If both publishing and unpublishing are either not enabled or are hidden
- // for this node type then the only thing to do is remove the fields from the
- // form, then exit.
+ // for this entity type then the only thing to do is remove the fields from
+ // the form, then exit.
if ((!$publishing_enabled || !$publishing_displayed) && (!$unpublishing_enabled || !$unpublishing_displayed)) {
unset($form['publish_on']);
unset($form['unpublish_on']);
@@ -111,24 +130,24 @@ function scheduler_form_node_form_alter(&$form, FormStateInterface $form_state)
$allow_date_only = $config->get('allow_date_only');
// A publish_on date is required if the content type option is set and the
- // node is being created or it is currently not published but has a
+ // entity is being created or it is currently not published but has a
// scheduled publishing date.
$publishing_required = $publishing_enabled
- && $type->getThirdPartySetting('scheduler', 'publish_required', $config->get('default_publish_required'))
- && ($node->isNew() || (!$node->isPublished() && !empty($node->publish_on->value)));
+ && $scheduler_manager->getThirdPartySetting($entity, 'publish_required', $config->get('default_publish_required'))
+ && ($entity->isNew() || (!$entity->isPublished() && !empty($entity->publish_on->value)));
// An unpublish_on date is required if the content type option is set and the
- // node is being created or the current status is published or the node is
+ // entity is being created or the current status is published or the entity is
// scheduled to be published.
$unpublishing_required = $unpublishing_enabled
- && $type->getThirdPartySetting('scheduler', 'unpublish_required', $config->get('default_unpublish_required'))
- && ($node->isNew() || $node->isPublished() || !empty($node->publish_on->value));
+ && $scheduler_manager->getThirdPartySetting($entity, 'unpublish_required', $config->get('default_unpublish_required'))
+ && ($entity->isNew() || $entity->isPublished() || !empty($entity->publish_on->value));
// Create a 'details' field group to wrap the scheduling fields, and expand it
// if publishing or unpublishing is required, if a date already exists or the
// fieldset is configured to be always expanded.
- $has_data = isset($node->publish_on->value) || isset($node->unpublish_on->value);
- $always_expand = $type->getThirdPartySetting('scheduler', 'expand_fieldset', $config->get('default_expand_fieldset')) === 'always';
+ $has_data = isset($entity->publish_on->value) || isset($entity->unpublish_on->value);
+ $always_expand = $scheduler_manager->getThirdPartySetting($entity, 'expand_fieldset', $config->get('default_expand_fieldset')) === 'always';
$expand_details = $publishing_required || $unpublishing_required || $has_data || $always_expand;
// Create the group for the fields.
@@ -146,7 +165,7 @@ function scheduler_form_node_form_alter(&$form, FormStateInterface $form_state)
$form['publish_on']['#group'] = 'scheduler_settings';
// Show the field group as a vertical tab if this option is enabled.
- $use_vertical_tabs = $type->getThirdPartySetting('scheduler', 'fields_display_mode', $config->get('default_fields_display_mode')) === 'vertical_tab';
+ $use_vertical_tabs = $scheduler_manager->getThirdPartySetting($entity, 'fields_display_mode', $config->get('default_fields_display_mode')) === 'vertical_tab';
if ($use_vertical_tabs) {
$form['scheduler_settings']['#group'] = 'advanced';
@@ -197,20 +216,23 @@ function scheduler_form_node_form_alter(&$form, FormStateInterface $form_state)
// When hiding the seconds on time input, we need to remove the seconds from
// the form value, as some browsers HTML5 rendering still show the seconds.
// We can use the same jQuery drupal behaviors file as for default time.
+ // This functionality is not covered by tests.
if ($config->get('hide_seconds')) {
// If there is a publish_on time, then use jQuery to remove the seconds.
- if (isset($node->publish_on->value)) {
+ if (isset($entity->publish_on->value)) {
$form['scheduler_settings']['#attached']['library'][] = 'scheduler/default-time';
- $form['scheduler_settings']['#attached']['drupalSettings']['schedulerHideSecondsPublishOn'] = date('H:i', $node->publish_on->value);
+ $form['scheduler_settings']['#attached']['drupalSettings']['schedulerHideSecondsPublishOn'] = date('H:i', $entity->publish_on->value);
}
// Likewise for the unpublish_on time.
- if (isset($node->unpublish_on->value)) {
+ if (isset($entity->unpublish_on->value)) {
$form['scheduler_settings']['#attached']['library'][] = 'scheduler/default-time';
- $form['scheduler_settings']['#attached']['drupalSettings']['schedulerHideSecondsUnpublishOn'] = date('H:i', $node->unpublish_on->value);
+ $form['scheduler_settings']['#attached']['drupalSettings']['schedulerHideSecondsUnpublishOn'] = date('H:i', $entity->unpublish_on->value);
}
}
- if (!\Drupal::currentUser()->hasPermission('schedule publishing of nodes')) {
+ // Check the permission for entering scheduled dates.
+ $permission = $scheduler_manager->permissionName($entityTypeId, 'schedule');
+ if (!\Drupal::currentUser()->hasPermission($permission)) {
// Do not show the scheduler fields for users who do not have permission.
// Setting #access to FALSE for 'scheduler_settings' is enough to hide the
// fields. Setting FALSE for the individual fields is necessary to keep any
@@ -242,11 +264,14 @@ function scheduler_form_node_form_alter(&$form, FormStateInterface $form_state)
foreach ($fields_to_check as $field) {
$actual_widget_id = $display->getComponent($field)['type'];
if ($actual_widget_id != $correct_widget_id) {
- \Drupal::messenger()->addMessage(t('The widget for field %field is incorrectly set to %wrong. This should be changed to %correct by an admin user via Field UI <a href="@link">content type form display</a> :not_available', [
+ $link = \Drupal::moduleHandler()->moduleExists('field_ui') ?
+ Url::fromRoute("entity.entity_form_display.$entityTypeId.default", ["{$entityTypeId}_type" => $entity->bundle()])->toString()
+ : '#';
+ \Drupal::messenger()->addMessage(t('The widget for field %field is incorrectly set to %wrong. This should be changed to %correct by an admin user via the <a href="@link">Field UI form display</a> :not_available', [
'%field' => (string) $form[$field]['widget']['#title'],
'%correct' => (string) $pluginDefinitions[$correct_widget_id]['label'],
'%wrong' => (string) $pluginDefinitions[$actual_widget_id]['label'],
- '@link' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('entity.entity_form_display.node.default', ['node_type' => $type->get('type')])->toString() : '#',
+ '@link' => $link,
':not_available' => \Drupal::moduleHandler()->moduleExists('field_ui') ? '' : ('(' . t('not available') . ')'),
]), 'warning', FALSE);
}
@@ -254,17 +279,229 @@ function scheduler_form_node_form_alter(&$form, FormStateInterface $form_state)
}
/**
- * Implements hook_form_FORM_ID_alter() for devel_generate_form_content.
+ * Form alter handling for entity type forms.
+ */
+function _scheduler_entity_type_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ $config = \Drupal::config('scheduler.settings');
+
+ /** @var \Drupal\Core\Entity\EntityTypeInterface $type */
+ $type = $form_state->getFormObject()->getEntity();
+
+ /** @var Drupal\Core\Entity\ContentEntityTypeInterface $ContentEntityType */
+ $contentEntityType = \Drupal::service('entity_type.manager')->getDefinition($type->getEntityType()->getBundleOf());
+
+ $params = [
+ '@type' => $type->label(),
+ '%type' => strtolower($type->label()),
+ '@singular' => $contentEntityType->getSingularLabel(),
+ '@plural' => $contentEntityType->getPluralLabel(),
+ ];
+
+ $form['#attached']['library'][] = 'scheduler/admin';
+ $form['#attached']['library'][] = 'scheduler/vertical-tabs';
+
+ $form['scheduler'] = [
+ '#type' => 'details',
+ '#title' => t('Scheduler'),
+ '#weight' => 35,
+ '#group' => 'additional_settings',
+ ];
+
+ // Publishing options.
+ $form['scheduler']['publish'] = [
+ '#type' => 'details',
+ '#title' => t('Publishing'),
+ '#weight' => 1,
+ '#group' => 'scheduler',
+ '#open' => TRUE,
+ ];
+ $form['scheduler']['publish']['scheduler_publish_enable'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Enable scheduled publishing for %type @plural', $params),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable')),
+ ];
+ $form['scheduler']['publish']['scheduler_publish_touch'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Change %type creation time to match the scheduled publish time', $params),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_touch', $config->get('default_publish_touch')),
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['scheduler']['publish']['scheduler_publish_required'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Require scheduled publishing'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_required', $config->get('default_publish_required')),
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ if ($contentEntityType->isRevisionable()) {
+ $form['scheduler']['publish']['scheduler_publish_revision'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Create a new revision on publishing'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_revision', $config->get('default_publish_revision')),
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ }
+ $form['scheduler']['publish']['advanced'] = [
+ '#type' => 'details',
+ '#title' => t('Advanced options'),
+ '#open' => FALSE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_publish_enable"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['scheduler']['publish']['advanced']['scheduler_publish_past_date'] = [
+ '#type' => 'radios',
+ '#title' => t('Action to be taken for publication dates in the past'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_past_date', $config->get('default_publish_past_date')),
+ '#options' => [
+ 'error' => t('Display an error message - do not allow dates in the past'),
+ 'publish' => t('Publish the %type @singular immediately after saving', $params),
+ 'schedule' => t('Schedule the %type @singular for publication on the next cron run', $params),
+ ],
+ ];
+ $form['scheduler']['publish']['advanced']['scheduler_publish_past_date_created'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Change %type creation time to match the published time, for dates before the %type was created', $params),
+ '#description' => t("The created time will only be altered when the scheduled publishing time is earlier than the existing creation time"),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'publish_past_date_created', $config->get('default_publish_past_date_created')),
+ // This option is not relevant if the full 'change creation time' option is
+ // selected, or when past dates are not allowed. Hence only show it when
+ // the main option is not checked and the past dates option is not 'error'.
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_publish_touch"]' => ['checked' => FALSE],
+ ':input[name="scheduler_publish_past_date"]' => ['!value' => 'error'],
+ ],
+ ],
+ ];
+
+ // Unpublishing options.
+ $form['scheduler']['unpublish'] = [
+ '#type' => 'details',
+ '#title' => t('Unpublishing'),
+ '#weight' => 2,
+ '#group' => 'scheduler',
+ '#open' => TRUE,
+ ];
+ $form['scheduler']['unpublish']['scheduler_unpublish_enable'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Enable scheduled unpublishing for %type @plural', $params),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable')),
+ ];
+ $form['scheduler']['unpublish']['scheduler_unpublish_required'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Require scheduled unpublishing'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'unpublish_required', $config->get('default_unpublish_required')),
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_unpublish_enable"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ if ($contentEntityType->isRevisionable()) {
+ $form['scheduler']['unpublish']['scheduler_unpublish_revision'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Create a new revision on unpublishing'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'unpublish_revision', $config->get('default_unpublish_revision')),
+ '#states' => [
+ 'visible' => [
+ ':input[name="scheduler_unpublish_enable"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ }
+
+ // The 'entity_edit_layout' fieldset contains options to alter the layout of
+ // entity edit pages.
+ $form['scheduler']['entity_edit_layout'] = [
+ '#type' => 'details',
+ '#title' => t('@type edit page', $params),
+ '#weight' => 3,
+ '#group' => 'scheduler',
+ // The #states processing only caters for AND and does not do OR. So to set
+ // the state to visible if either of the boxes are ticked we use the fact
+ // that logical 'X = A or B' is equivalent to 'not X = not A and not B'.
+ '#states' => [
+ '!visible' => [
+ ':input[name="scheduler_publish_enable"]' => ['!checked' => TRUE],
+ ':input[name="scheduler_unpublish_enable"]' => ['!checked' => TRUE],
+ ],
+ ],
+ ];
+ $form['scheduler']['entity_edit_layout']['scheduler_fields_display_mode'] = [
+ '#type' => 'radios',
+ '#title' => t('Display scheduling date input fields in'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'fields_display_mode', $config->get('default_fields_display_mode')),
+ '#options' => [
+ 'vertical_tab' => t('Vertical tab'),
+ 'fieldset' => t('Separate fieldset'),
+ ],
+ '#description' => t('Use this option to specify how the scheduler fields are displayed when editing %type @plural', $params),
+ ];
+ $form['scheduler']['entity_edit_layout']['scheduler_expand_fieldset'] = [
+ '#type' => 'radios',
+ '#title' => t('Expand fieldset or vertical tab'),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'expand_fieldset', $config->get('default_expand_fieldset')),
+ '#options' => [
+ 'when_required' => t('Expand only when a scheduled date exists or when a date is required'),
+ 'always' => t('Always open the fieldset or vertical tab'),
+ ],
+ ];
+ $form['scheduler']['entity_edit_layout']['scheduler_show_message_after_update'] = [
+ '#type' => 'checkbox',
+ '#prefix' => '<strong>' . t('Show message') . '</strong>',
+ '#title' => t('Show a confirmation message when a scheduled %type @singular is saved', $params),
+ '#default_value' => $type->getThirdPartySetting('scheduler', 'show_message_after_update', $config->get('default_show_message_after_update')),
+ ];
+
+ $form['#entity_builders'][] = '_scheduler_form_entity_type_form_builder';
+}
+
+/**
+ * Entity builder for the entity type form with scheduler options.
+ */
+function _scheduler_form_entity_type_form_builder($entity_type, $type, &$form, FormStateInterface $form_state) {
+ $type->setThirdPartySetting('scheduler', 'expand_fieldset', $form_state->getValue('scheduler_expand_fieldset'));
+ $type->setThirdPartySetting('scheduler', 'fields_display_mode', $form_state->getValue('scheduler_fields_display_mode'));
+ $type->setThirdPartySetting('scheduler', 'publish_enable', $form_state->getValue('scheduler_publish_enable'));
+ $type->setThirdPartySetting('scheduler', 'publish_past_date', $form_state->getValue('scheduler_publish_past_date'));
+ $type->setThirdPartySetting('scheduler', 'publish_past_date_created', $form_state->getValue('scheduler_publish_past_date_created'));
+ $type->setThirdPartySetting('scheduler', 'publish_required', $form_state->getValue('scheduler_publish_required'));
+ $type->setThirdPartySetting('scheduler', 'publish_revision', $form_state->getValue('scheduler_publish_revision'));
+ $type->setThirdPartySetting('scheduler', 'publish_touch', $form_state->getValue('scheduler_publish_touch'));
+ $type->setThirdPartySetting('scheduler', 'show_message_after_update', $form_state->getValue('scheduler_show_message_after_update'));
+ $type->setThirdPartySetting('scheduler', 'unpublish_enable', $form_state->getValue('scheduler_unpublish_enable'));
+ $type->setThirdPartySetting('scheduler', 'unpublish_required', $form_state->getValue('scheduler_unpublish_required'));
+ $type->setThirdPartySetting('scheduler', 'unpublish_revision', $form_state->getValue('scheduler_unpublish_revision'));
+}
+
+/**
+ * Form alter handling for Devel Generate forms.
*/
-function scheduler_form_devel_generate_form_content_alter(array &$form, FormStateInterface $form_state) {
- // Add an extra column to the node_types table to show which type are enabled
- // for scheduled publishing and unpublishing.
- $publishing_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types('publish'));
- $unpublishing_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types('unpublish'));
+function _scheduler_devel_generate_form_alter(array &$form, FormStateInterface $form_state, $form_id, $entityTypeId) {
+ // Add an extra column to the table to show which types are enabled for
+ // scheduled publishing and unpublishing.
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+ $publishing_enabled_types = $scheduler_manager->getEnabledTypes($entityTypeId, 'publish');
+ $unpublishing_enabled_types = $scheduler_manager->getEnabledTypes($entityTypeId, 'unpublish');
- $form['node_types']['#header']['scheduler'] = t('Scheduler settings');
+ $type_table = $entityTypeId . '_types';
+ $form[$type_table]['#header']['scheduler'] = t('Scheduler settings');
- foreach (array_keys($form['node_types']['#options']) as $type) {
+ foreach (array_keys($form[$type_table]['#options']) as $type) {
$items = [];
if (in_array($type, $publishing_enabled_types)) {
$items[] = t('Enabled for publishing');
@@ -283,16 +520,16 @@ function scheduler_form_devel_generate_form_content_alter(array &$form, FormStat
],
];
}
- $form['node_types']['#options'][$type]['scheduler'] = $scheduler_settings;
+ $form[$type_table]['#options'][$type]['scheduler'] = $scheduler_settings;
}
- // Add form items to specify what proportion of generated nodes should have a
- // publish-on and unpublish-on date assigned. See hook_node_presave() for the
- // code which sets the node values.
+ // Add form items to specify what proportion of generated entities should have
+ // a publish-on and/or unpublish-on date assigned. See hook_entity_presave()
+ // for the code that sets these values in the generated entity.
$form['scheduler_publishing'] = [
'#type' => 'number',
'#title' => t('Publishing date for Scheduler'),
- '#description' => t('Enter the percentage of randomly selected Scheduler-enabled nodes to be given a publish-on date. Enter 0 for none, 100 for all. The date and time will be random within the range starting at node creation date, up to a time in the future matching the same span as selected above for node creation date.'),
+ '#description' => t('Enter a percentage for randomly selecting Scheduler-enabled entities to be given a publish-on date. Enter 0 for none, 100 for all. The date and time will be random within the range starting at entity creation date, up to a time in the future matching the same span as selected above for creation date.'),
'#default_value' => 50,
'#required' => TRUE,
'#min' => 0,
@@ -301,7 +538,7 @@ function scheduler_form_devel_generate_form_content_alter(array &$form, FormStat
$form['scheduler_unpublishing'] = [
'#type' => 'number',
'#title' => t('Unpublishing date for Scheduler'),
- '#description' => t('Enter the percentage of randomly selected Scheduler-enabled nodes to be given an unpublish-on date. Enter 0 for none, 100 for all. The date and time will be random within the range starting at the later of node creation date and publish-on date, up to a time in the future matching the same span as selected above for node creation date.'),
+ '#description' => t('Enter a percentage for randomly selecting Scheduler-enabled entities to be given an unpublish-on date. Enter 0 for none, 100 for all. The date and time will be random within the range starting at the later of entity creation date and publish-on date, up to a time in the future matching the same span as selected above for creation date.'),
'#default_value' => 50,
'#required' => TRUE,
'#min' => 0,
@@ -314,43 +551,54 @@ function scheduler_form_devel_generate_form_content_alter(array &$form, FormStat
*/
function scheduler_form_language_content_settings_form_alter(array &$form, FormStateInterface $form_state) {
// Add our validation function for the translation field settings form at
- // admin/config/regional/content-language.
+ // admin/config/regional/content-language
+ // This hook function caters for all entity types, not just nodes.
$form['#validate'][] = '_scheduler_translation_validate';
}
/**
* Validation handler for language_content_settings_form.
*
- * If the content type is translatable and the field is enabled for Scheduler
+ * For each entity type, if it is translatable and also enabled for Scheduler,
* but the translation setting for the publish_on / unpublish_on field does not
* match the 'published status' field setting then throw a validation error.
*
* @see https://www.drupal.org/project/scheduler/issues/2871164
*/
function _scheduler_translation_validate($form, FormStateInterface $form_state) {
- $content_types = $form_state->getValues()['settings']['node'];
- $publishing_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types('publish'));
- $unpublishing_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types('unpublish'));
- $enabled = [];
- foreach ($content_types as $name => $settings) {
- $enabled['publish_on'] = in_array($name, $publishing_enabled_types);
- $enabled['unpublish_on'] = in_array($name, $unpublishing_enabled_types);
- if ($settings['translatable'] && ($enabled['publish_on'] || $enabled['unpublish_on'])) {
- $params = [
- '@type' => $form['settings']['node'][$name]['settings']['#label'],
- '@status' => $form['settings']['node'][$name]['fields']['status']['#label'],
- ];
- foreach (['publish_on', 'unpublish_on'] as $var) {
- $mismatch = $enabled[$var] && ($settings['fields'][$var] <> $settings['fields']['status']);
- if ($mismatch) {
- $params['@scheduler_field'] = $form['settings']['node'][$name]['fields'][$var]['#label'];
- $message = t("Content type '@type' - Translatable settings for status field '@status' and Scheduler field '@scheduler_field' should match, either both on or both off", $params);
- $form_state->setErrorByName("settings][node][$name][fields][status", $message);
- $form_state->setErrorByName("settings][node][$name][fields][$var", $message);
+ $settings = $form_state->getValues()['settings'];
+ /** @var \Drupal\scheduler\SchedulerManager $scheduler_manager */
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+ foreach ($settings as $entity_type => $content_types) {
+ $publishing_enabled_types = $scheduler_manager->getEnabledTypes($entity_type, 'publish');
+ $unpublishing_enabled_types = $scheduler_manager->getEnabledTypes($entity_type, 'unpublish');
+ if (empty($publishing_enabled_types) && empty($publishing_enabled_types)) {
+ continue;
+ }
+
+ $enabled = [];
+ foreach ($content_types as $name => $options) {
+ $enabled['publish_on'] = in_array($name, $publishing_enabled_types);
+ $enabled['unpublish_on'] = in_array($name, $unpublishing_enabled_types);
+ if ($options['translatable'] && ($enabled['publish_on'] || $enabled['unpublish_on'])) {
+ $params = [
+ '@entity' => $form['settings'][$entity_type]['#bundle_label'],
+ '@type' => $form['settings'][$entity_type][$name]['settings']['#label'],
+ '%status' => $form['settings'][$entity_type][$name]['fields']['status']['#label'],
+ ];
+ foreach (['publish_on', 'unpublish_on'] as $var) {
+ $mismatch = $enabled[$var] && ($options['fields'][$var] <> $options['fields']['status']);
+ if ($mismatch) {
+ $params['%scheduler_field'] = $form['settings'][$entity_type][$name]['fields'][$var]['#label'];
+ $message = t("There is a problem with @entity '@type' - The translatable settings for status field '%status' and Scheduler field '%scheduler_field' should match, either both on or both off", $params);
+ $form_state->setErrorByName("settings][$entity_type][$name][fields][status", $message);
+ $form_state->setErrorByName("settings][$entity_type][$name][fields][$var", $message);
+ }
}
}
}
}
+
}
/**
@@ -358,7 +606,9 @@ function _scheduler_translation_validate($form, FormStateInterface $form_state)
*/
function scheduler_entity_base_field_info(EntityTypeInterface $entity_type) {
$fields = [];
- if ($entity_type->id() === 'node') {
+ $entity_types = \Drupal::service('scheduler.manager')->getPluginEntityTypes();
+
+ if (in_array($entity_type->id(), $entity_types)) {
$fields['publish_on'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Publish on'))
->setDisplayOptions('form', [
@@ -392,174 +642,197 @@ function scheduler_views_data_alter(array &$data) {
// By default the 'is null' and 'is not null' operators are only added to the
// list of filter options if the view contains a relationship. We want them to
// be always available for the scheduler date fields.
- $data['node_field_data']['publish_on']['filter']['allow empty'] = TRUE;
- $data['node_field_data']['unpublish_on']['filter']['allow empty'] = TRUE;
- $data['node_field_revision']['publish_on']['filter']['allow empty'] = TRUE;
- $data['node_field_revision']['unpublish_on']['filter']['allow empty'] = TRUE;
+ $entity_types = \Drupal::service('scheduler.manager')->getPluginEntityTypes();
+ foreach ($entity_types as $entityTypeId) {
+ // Not every entity that has a plugin will have these tables, so only set
+ // the allow_empty filter if the top-level key exists.
+ if (isset($data["{$entityTypeId}_field_data"])) {
+ $data["{$entityTypeId}_field_data"]['publish_on']['filter']['allow empty'] = TRUE;
+ $data["{$entityTypeId}_field_data"]['unpublish_on']['filter']['allow empty'] = TRUE;
+ }
+ if (isset($data["{$entityTypeId}_field_revision"])) {
+ $data["{$entityTypeId}_field_revision"]['publish_on']['filter']['allow empty'] = TRUE;
+ $data["{$entityTypeId}_field_revision"]['unpublish_on']['filter']['allow empty'] = TRUE;
+ }
+ }
+
+ // Add a relationship from Media Field Revision back to Media Field Data.
+ // @todo This can be removed when the relationship is added to core.
+ // @see https://www.drupal.org/project/drupal/issues/3036192
+ // Replace the existing 'argument' item.
+ $data['media_field_revision']['mid']['argument'] = [
+ 'id' => 'media_mid',
+ 'numeric' => TRUE,
+ ];
+ // Add a 'relationship' item.
+ $data['media_field_revision']['mid']['relationship'] = [
+ 'id' => 'standard',
+ 'base' => 'media_field_data',
+ 'field' => 'mid',
+ 'base field' => 'mid',
+ 'title' => t('Media Field Data'),
+ 'help' => t('Relationship to access the Media fields that are not on Media Revision.'),
+ 'label' => t('Media Field'),
+ 'extra' => [
+ [
+ 'field' => 'langcode',
+ 'left_field' => 'langcode',
+ ],
+ ],
+ ];
}
/**
- * Implements hook_ENTITY_TYPE_view() for node entities.
+ * Implements hook_entity_view().
*/
-function scheduler_node_view(array &$build, EntityInterface $node, EntityViewDisplayInterface $display, $view_mode) {
- // If the node is going to be unpublished, then add this information to the
- // header for search engines. Only do this when the current page is the
- // full-page view of the node.
+function scheduler_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, string $view_mode) {
+ // If the entity is going to be unpublished, then add this information to the
+ // http header for search engines. Only do this when the current page is the
+ // full-page view of the entity.
// @see https://googleblog.blogspot.be/2007/07/robots-exclusion-protocol-now-with-even.html
- if (!empty($node->unpublish_on->value) && node_is_page($node)) {
- $unavailable_after = date(DATE_RFC850, $node->unpublish_on->value);
+ if ($view_mode == 'full' && isset($entity->unpublish_on->value)) {
+ $unavailable_after = date(DATE_RFC850, $entity->unpublish_on->value);
$build['#attached']['http_header'][] = ['X-Robots-Tag', 'unavailable_after: ' . $unavailable_after];
+
+ // Also add the information as a meta tag in the html head section.
+ $unavailable_meta_tag = [
+ '#tag' => 'meta',
+ '#attributes' => [
+ 'name' => 'robots',
+ 'content' => 'unavailable_after: ' . $unavailable_after,
+ ],
+ ];
+ // Any value seems to be OK for the second item, but it must not be omitted.
+ $build['#attached']['html_head'][] = [$unavailable_meta_tag, 'robots_unavailable_date'];
}
}
/**
- * Implements hook_ENTITY_TYPE_presave() for node entities.
+ * Implements hook_entity_presave().
*/
-function scheduler_node_presave(EntityInterface $node) {
+function scheduler_entity_presave(EntityInterface $entity) {
$config = \Drupal::config('scheduler.settings');
$scheduler_manager = \Drupal::service('scheduler.manager');
- $entity = $node->type->entity;
$request_time = \Drupal::time()->getRequestTime();
- $publish_message = FALSE;
- $unpublish_message = FALSE;
- // If there is no entity object or the class is incorrect then stop here. This
- // should not really happen but it has been observed, so better to be safe.
- // @see https://www.drupal.org/node/2902512
- if (is_null($entity) || !get_class($entity) == 'Drupal\node\Entity\NodeType') {
+ $publishing_enabled_types = $scheduler_manager->getEnabledTypes($entity->getEntityTypeId(), 'publish');
+ $unpublishing_enabled_types = $scheduler_manager->getEnabledTypes($entity->getEntityTypeId(), 'unpublish');
+ $publishing_enabled = in_array($entity->bundle(), $publishing_enabled_types);
+ $unpublishing_enabled = in_array($entity->bundle(), $unpublishing_enabled_types);
+
+ if (!$publishing_enabled && !$unpublishing_enabled) {
+ // Neither scheduled publishing nor unpublishing are enabled for this
+ // specific bundle/type, so end here.
return;
- };
+ }
- // If this node is being created via Devel Generate then set values for the
+ // If this entity is being created via Devel Generate then set values for the
// publish_on and unpublish_on dates as specified in the devel_generate form.
- if (isset($node->devel_generate)) {
- static $publishing_enabled_types;
- static $unpublishing_enabled_types;
+ if (isset($entity->devel_generate)) {
static $publishing_percent;
static $unpublishing_percent;
static $time_range;
- if (!isset($publishing_enabled_types)) {
- $publishing_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types('publish'));
- $unpublishing_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types('unpublish'));
+ if (!isset($publishing_percent)) {
// The values may not be set if calling via drush, so default to zero.
- $publishing_percent = @$node->devel_generate['scheduler_publishing'] ?: 0;
- $unpublishing_percent = @$node->devel_generate['scheduler_unpublishing'] ?: 0;
- // Reuse the selected 'node creation' time range for our future date span.
- $time_range = $node->devel_generate['time_range'];
+ $publishing_percent = @$entity->devel_generate['scheduler_publishing'] ?: 0;
+ $unpublishing_percent = @$entity->devel_generate['scheduler_unpublishing'] ?: 0;
+ // Reuse the selected 'creation' time range for our future date span.
+ $time_range = $entity->devel_generate['time_range'];
}
- if ($publishing_percent && in_array($node->getType(), $publishing_enabled_types)) {
+ if ($publishing_percent && $publishing_enabled) {
if (rand(1, 100) <= $publishing_percent) {
// Randomly assign a publish_on value in the range starting with the
// created date and up to the selected time range in the future.
- $node->set('publish_on', rand($node->created->value + 1, $request_time + $time_range));
+ $entity->set('publish_on', rand($entity->created->value + 1, $request_time + $time_range));
}
}
- if ($unpublishing_percent && in_array($node->getType(), $unpublishing_enabled_types)) {
+ if ($unpublishing_percent && $unpublishing_enabled) {
if (rand(1, 100) <= $unpublishing_percent) {
// Randomly assign an unpublish_on value in the range from the later of
// created date/publish_on date up to the time range in the future.
- $node->set('unpublish_on', rand(max($node->created->value, $node->publish_on->value), $request_time + $time_range));
+ $entity->set('unpublish_on', rand(max($entity->created->value, $entity->publish_on->value), $request_time + $time_range));
}
}
}
- // If the node type is enabled for scheduled publishing and has a publish_on
+ $publish_message = FALSE;
+ $unpublish_message = FALSE;
+
+ // If the entity type is enabled for scheduled publishing and has a publish_on
// date then check if publishing is allowed and if the content needs to be
// published immediately.
- if ($entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable')) && !empty($node->publish_on->value)) {
- // Check that other modules allow the action on this node.
- $publication_allowed = $scheduler_manager->isAllowed($node, 'publish');
+ if ($publishing_enabled && !empty($entity->publish_on->value)) {
+ // Check that other modules allow the action on this entity.
+ $publication_allowed = $scheduler_manager->isAllowed($entity, 'publish');
- // Publish the node immediately if the publication date is in the past.
- $publish_immediately = $entity->getThirdPartySetting('scheduler', 'publish_past_date', $config->get('default_publish_past_date')) == 'publish';
+ // Publish the entity immediately if the publication date is in the past.
+ $publish_immediately = $scheduler_manager->getThirdPartySetting($entity, 'publish_past_date', $config->get('default_publish_past_date')) == 'publish';
- if ($publication_allowed && $publish_immediately && $node->publish_on->value <= $request_time) {
- // Trigger the PRE_PUBLISH_INMEDIATELY event so that modules can react
- // before the node has been published.
- $event = new SchedulerEvent($node);
- $scheduler_manager->dispatch($event, SchedulerEvents::PRE_PUBLISH_IMMEDIATELY);
- $node = $event->getNode();
+ if ($publication_allowed && $publish_immediately && $entity->publish_on->value <= $request_time) {
+ // Trigger the PRE_PUBLISH_IMMEDIATELY event so that modules can react
+ // before the entity has been published.
+ $scheduler_manager->dispatchSchedulerEvent($entity, 'PRE_PUBLISH_IMMEDIATELY');
// Set the 'changed' timestamp to match what would have been done had this
// content been published via cron.
- $node->setChangedTime($node->publish_on->value);
+ $entity->setChangedTime($entity->publish_on->value);
// If required, set the created date to match published date.
- if ($entity->getThirdPartySetting('scheduler', 'publish_touch', $config->get('default_publish_touch')) ||
- ($node->getCreatedTime() > $node->publish_on->value && $entity->getThirdPartySetting('scheduler', 'publish_past_date_created', $config->get('default_publish_past_date_created')))) {
- $node->setCreatedTime($node->publish_on->value);
+ if ($scheduler_manager->getThirdPartySetting($entity, 'publish_touch', $config->get('default_publish_touch')) ||
+ ($entity->getCreatedTime() > $entity->publish_on->value && $scheduler_manager->getThirdPartySetting($entity, 'publish_past_date_created', $config->get('default_publish_past_date_created')))) {
+ $entity->setCreatedTime($entity->publish_on->value);
}
- $node->publish_on->value = NULL;
- $node->setPublished();
+ $entity->publish_on->value = NULL;
+ $entity->setPublished();
// Trigger the PUBLISH_IMMEDIATELY event so that modules can react after
- // the node has been published.
- $event = new SchedulerEvent($node);
- $scheduler_manager->dispatch($event, SchedulerEvents::PUBLISH_IMMEDIATELY);
- $node = $event->getNode();
+ // the entity has been published.
+ $scheduler_manager->dispatchSchedulerEvent($entity, 'PUBLISH_IMMEDIATELY');
}
else {
- // Ensure the node is unpublished as it will be published by cron later.
- $node->setUnpublished();
+ // Ensure the entity is unpublished as it will be published by cron later.
+ $entity->setUnpublished();
- // Only inform the user that the node is scheduled if publication has not
- // been prevented by other modules. Those modules have to display a
+ // Only inform the user that the entity is scheduled if publication has
+ // not been prevented by other modules. Those modules have to display a
// message themselves explaining why publication is denied.
- $publish_message = ($publication_allowed && $entity->getThirdPartySetting('scheduler', 'show_message_after_update', $config->get('default_show_message_after_update')));
+ $publish_message = ($publication_allowed && $scheduler_manager->getThirdPartySetting($entity, 'show_message_after_update', $config->get('default_show_message_after_update')));
}
- }
+ } // Entity has a publish_on date.
- if ($entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable')) && !empty($node->unpublish_on->value)) {
+ if ($unpublishing_enabled && !empty($entity->unpublish_on->value)) {
// Scheduler does not do the same 'immediate' processing for unpublishing.
// However, the api hook should still be called during presave as there may
// be messages to be displayed if the unpublishing will be disallowed later.
- $unpublication_allowed = $scheduler_manager->isAllowed($node, 'unpublish');
- $unpublish_message = ($unpublication_allowed && $entity->getThirdPartySetting('scheduler', 'show_message_after_update', $config->get('default_show_message_after_update')));
+ $unpublication_allowed = $scheduler_manager->isAllowed($entity, 'unpublish');
+ $unpublish_message = ($unpublication_allowed && $scheduler_manager->getThirdPartySetting($entity, 'show_message_after_update', $config->get('default_show_message_after_update')));
}
// Give one message, which will include the publish_on date, the unpublish_on
- // date or both dates. Cannot make the title into a link here when the node
- // is being created. But the node module gives the link in the next message.
+ // date or both dates. Cannot make the title into a link here when the entity
+ // is being created. But core provides the link in the subsequent message.
$date_formatter = \Drupal::service('date.formatter');
if ($publish_message && $unpublish_message) {
\Drupal::messenger()->addMessage(t('%title is scheduled to be published @publish_time and unpublished @unpublish_time.', [
- '%title' => $node->getTitle(),
- '@publish_time' => $date_formatter->format($node->publish_on->value, 'long'),
- '@unpublish_time' => $date_formatter->format($node->unpublish_on->value, 'long'),
+ '%title' => $entity->label(),
+ '@publish_time' => $date_formatter->format($entity->publish_on->value, 'long'),
+ '@unpublish_time' => $date_formatter->format($entity->unpublish_on->value, 'long'),
]), 'status', FALSE);
}
elseif ($publish_message) {
\Drupal::messenger()->addMessage(t('%title is scheduled to be published @publish_time.', [
- '%title' => $node->getTitle(),
- '@publish_time' => $date_formatter->format($node->publish_on->value, 'long'),
+ '%title' => $entity->label(),
+ '@publish_time' => $date_formatter->format($entity->publish_on->value, 'long'),
]), 'status', FALSE);
}
elseif ($unpublish_message) {
\Drupal::messenger()->addMessage(t('%title is scheduled to be unpublished @unpublish_time.', [
- '%title' => $node->getTitle(),
- '@unpublish_time' => $date_formatter->format($node->unpublish_on->value, 'long'),
+ '%title' => $entity->label(),
+ '@unpublish_time' => $date_formatter->format($entity->unpublish_on->value, 'long'),
]), 'status', FALSE);
}
}
-/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
- */
-function scheduler_node_insert(EntityInterface $node) {
- // Removed RULES code but keep the function. There may be code to add here.
- // @todo remove this comment when done. JSS Sep 2016.
-}
-
-/**
- * Implements hook_ENTITY_TYPE_update() for node entities.
- */
-function scheduler_node_update(EntityInterface $node) {
- // Removed RULES code but keep the function. There may be code to add here.
- // This function is currently called by actions SetPublishingDate and
- // SetUnpublishingDate.
- // @todo remove this comment when done. JSS Sep 2016.
-}
-
/**
* Implements hook_cron().
*/
@@ -607,42 +880,56 @@ function scheduler_cron_is_running() {
function scheduler_entity_extra_field_info() {
$config = \Drupal::config('scheduler.settings');
+ $plugins = \Drupal::service('scheduler.manager')->getPlugins();
+
// Expose the Scheduler group on the 'Manage Form Display' tab when editing a
// content type. This allows admins to adjust the weight of the group, and it
// works for vertical tabs and separate fieldsets.
$fields = [];
- foreach (NodeType::loadMultiple() as $type) {
- $publishing_enabled = $type->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'));
- $unpublishing_enabled = $type->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'));
-
- if ($publishing_enabled || $unpublishing_enabled) {
- // Weight 20 puts this below the core fields by default.
- $fields['node'][$type->get('type')]['form']['scheduler_settings'] = [
- 'label' => t('Scheduler Dates'),
- 'description' => t('Fieldset containing Scheduler Publish-on and Unpublish-on date input fields'),
- 'weight' => 20,
- ];
+
+ foreach ($plugins as $entityTypeId => $plugin) {
+ $types = $plugin->getTypes();
+ foreach ($types as $type) {
+ $publishing_enabled = $type->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'));
+ $unpublishing_enabled = $type->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'));
+
+ if ($publishing_enabled || $unpublishing_enabled) {
+ // Weight 20 puts this below the core fields by default.
+ $fields[$entityTypeId][$type->id()]['form']['scheduler_settings'] = [
+ 'label' => t('Scheduler Dates'),
+ 'description' => t('Fieldset containing Scheduler Publish-on and Unpublish-on date input fields'),
+ 'weight' => 20,
+ ];
+ }
}
}
+
return $fields;
}
/**
- * Prepares variables for node templates.
- *
- * Makes the publish_on and unpublish_on data available as theme variables.
- *
- * @see template_preprocess_node()
+ * Implements hook_preprocess().
*/
-function scheduler_preprocess_node(&$variables) {
- $date_formatter = \Drupal::service('date.formatter');
- /** @var \Drupal\node\NodeInterface $node */
- $node = $variables['node'];
- if (!empty($node->publish_on->value) && $node->publish_on->value && is_numeric($node->publish_on->value)) {
- $variables['publish_on'] = $date_formatter->format($node->publish_on->value, 'long');
+function scheduler_preprocess(&$variables, $hook) {
+ // For entity types that can be processed by Scheduler add the formatted
+ // publish_on and unpublish_on dates as variables for use in theme templates.
+ $plugins = &drupal_static(__FUNCTION__);
+ if (empty($plugins)) {
+ $plugins = \Drupal::service('scheduler.manager')->getPluginEntityTypes();
}
- if (!empty($node->unpublish_on->value) && $node->unpublish_on->value && is_numeric($node->unpublish_on->value)) {
- $variables['unpublish_on'] = $date_formatter->format($node->unpublish_on->value, 'long');
+ // For $hook = 'node' and 'media' the entity is stored in $variables[$hook].
+ // This is not guaranteed, for example in commerce_product the entity is in
+ // $variables['product_entity']. This could be extracted if there is a need,
+ // but for now just skip if the entity is not immediately available.
+ if (in_array($hook, $plugins) && isset($variables[$hook])) {
+ $date_formatter = \Drupal::service('date.formatter');
+ $entity = $variables[$hook];
+ if (!empty($entity->publish_on->value) && $entity->publish_on->value && is_numeric($entity->publish_on->value)) {
+ $variables['publish_on'] = $date_formatter->format($entity->publish_on->value, 'long');
+ }
+ if (!empty($entity->unpublish_on->value) && $entity->unpublish_on->value && is_numeric($entity->unpublish_on->value)) {
+ $variables['unpublish_on'] = $date_formatter->format($entity->unpublish_on->value, 'long');
+ }
}
}
@@ -652,29 +939,39 @@ function scheduler_preprocess_node(&$variables) {
* This function exposes publish_on and unpublish_on as mappable targets to the
* Feeds module.
*
+ * @see https://www.drupal.org/project/feeds
+ *
* @todo Port to Drupal 8.
*
- * @see https://www.drupal.org/node/2651354
+ * @see https://www.drupal.org/project/scheduler/issues/2651354
*/
function scheduler_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
- $config = \Drupal::config('scheduler.settings');
-
- // Scheduler module only works on nodes.
- if ($entity_type == 'node') {
+ // The plugins processing has not been tested, because the function has not
+ // been converted to Drupal 8. The processing below will need to be adjusted
+ // depending on whether $entity_type is a string or an object.
+ // See scheduler_entity_extra_field_info() for an example.
+ // May need to use $scheduler_manager->getThirdPartySetting.
+ $plugins = &drupal_static(__FUNCTION__);
+ if (empty($plugins)) {
+ $plugins = \Drupal::service('scheduler.manager')->getPluginEntityTypes();
+ }
+ if (in_array($entity_type, $plugins)) {
+ $config = \Drupal::config('scheduler.settings');
+ // @todo Get the entity object if $entity_type is a string.
$publishing_enabled = $entity_type->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'));
$unpublishing_enabled = $entity_type->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'));
if ($publishing_enabled) {
$targets['publish_on'] = [
'name' => t('Scheduler: publish on'),
- 'description' => t('The date when the Scheduler module will publish the node.'),
+ 'description' => t('The date when the Scheduler module will publish the content.'),
'callback' => 'scheduler_feeds_set_target',
];
}
if ($unpublishing_enabled) {
$targets['unpublish_on'] = [
'name' => t('Scheduler: unpublish on'),
- 'description' => t('The date when the Scheduler module will unpublish the node.'),
+ 'description' => t('The date when the Scheduler module will unpublish the content.'),
'callback' => 'scheduler_feeds_set_target',
];
}
@@ -686,7 +983,7 @@ function scheduler_feeds_processor_targets_alter(&$targets, $entity_type, $bundl
*
* @todo Port to Drupal 8.
*
- * @see https://www.drupal.org/node/2651354
+ * @see https://www.drupal.org/project/scheduler/issues/2651354
*/
function scheduler_feeds_set_target($source, $entity, $target, $value, $mapping) {
// We expect a string or integer, but can accomodate an array, by taking the
@@ -706,26 +1003,48 @@ function scheduler_feeds_set_target($source, $entity, $target, $value, $mapping)
$timestamp = $value;
}
- // If the timestamp is valid then use it to set the target field in the node.
- if (is_numeric($timestamp) && $timestamp > 0) {
+ // If the timestamp is valid, use it to set the target field in the entity.
+ if (is_numeric($timestamp)) {
$entity->$target = $timestamp;
}
}
/**
- * Returns all content types for which scheduler has been enabled.
- *
- * @param string $action
- * The action that needs to be checked. Can be 'publish' or 'unpublish'.
- *
- * @return \Drupal\node\NodeTypeInterface[]
- * Array of NodeTypeInterface objects
+ * Implements hook_modules_installed().
*/
-function _scheduler_get_scheduler_enabled_node_types($action) {
- $config = \Drupal::config('scheduler.settings');
- $node_types = NodeType::loadMultiple();
- return array_filter($node_types, function ($bundle) use ($action, $config) {
- /** @var \Drupal\node\NodeTypeInterface $bundle */
- return $bundle->getThirdPartySetting('scheduler', $action . '_enable', $config->get('default_' . $action . '_enable'));
- });
+function scheduler_modules_installed($modules) {
+ /** @var \Drupal\scheduler\SchedulerManager $scheduler_manager */
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+ $scheduler_manager->invalidatePluginCache();
+
+ // If there is a Scheduler plugin for a newly installed module then update
+ // the base tables by adding publish_on and unpublish_on for that entity type,
+ // and load/refresh the scheduled view. Third-party modules can provide
+ // Scheduler plugins for entity types that are not defined by that module, or
+ // that do not have the same id as the module name. Similarly, core modules
+ // define entity types for which Scheduler provides the plugin. Hence we need
+ // to check both the plugin entity type and the provider and if either of
+ // these match a module that is being installed we run the update functions.
+ $matches = [];
+ $plugin_definitions = $scheduler_manager->getPluginDefinitions();
+ foreach ($plugin_definitions as $definition) {
+ // If the plugin entity type or the plugin provider match any of the modules
+ // being installed then add the entity type to the list to be updated.
+ if (array_intersect([$definition['entityType'], $definition['provider']], $modules)) {
+ $matches[] = $definition['entityType'];
+ }
+ }
+ if (!empty($matches)) {
+ // Add the database fields.
+ $scheduler_manager->entityUpdate();
+ // Load/refresh the scheduler view.
+ $scheduler_manager->viewsUpdate($matches);
+ }
+}
+
+/**
+ * Implements hook_cache_flush().
+ */
+function scheduler_cache_flush() {
+ \Drupal::service('scheduler.manager')->invalidatePluginCache();
}
diff --git a/scheduler.permissions.yml b/scheduler.permissions.yml
index 5e36688..fe7220a 100644
--- a/scheduler.permissions.yml
+++ b/scheduler.permissions.yml
@@ -1,9 +1,7 @@
'administer scheduler':
title: 'Administer scheduler'
- description: 'Configure scheduler date formats, pop-up calendar, default times, lightweight cron'
-'schedule publishing of nodes':
- title: 'Schedule content publication'
- description: 'Allows users to set a start and end time for content publication'
-'view scheduled content':
- title: 'View scheduled content list'
- description: 'Allows users to see all content which is scheduled.'
+ description: 'Configure scheduler - set default times, lightweight cron.'
+# All other permissions are dynamically created for each supported entity type.
+# See src/SchedulerPermissions.php
+permission_callbacks:
+ - Drupal\scheduler\SchedulerPermissions::permissions
diff --git a/scheduler.routing.yml b/scheduler.routing.yml
index 33774ef..1d14fe7 100644
--- a/scheduler.routing.yml
+++ b/scheduler.routing.yml
@@ -25,3 +25,9 @@ scheduler.lightweight_cron:
no_cache: TRUE
requirements:
_custom_access: '\Drupal\scheduler\Controller\LightweightCronController::access'
+
+# This duplicate route is required for the local tasks. See links.task.yml.
+scheduler.commerce_products:
+ path: '/admin/commerce/products'
+ requirements:
+ _permission: 'access commerce_product overview'
diff --git a/scheduler.services.yml b/scheduler.services.yml
index b849e4f..282ca2d 100644
--- a/scheduler.services.yml
+++ b/scheduler.services.yml
@@ -9,16 +9,20 @@ services:
- '@config.factory'
- '@event_dispatcher'
- '@datetime.time'
+ - '@entity_field.manager'
+ - '@plugin.manager.scheduler'
logger.channel.scheduler:
class: Drupal\Core\Logger\LoggerChannel
factory: logger.factory:get
arguments: ['scheduler']
- access_checker.scheduler_content:
- class: Drupal\scheduler\Access\ScheduledListAccess
- arguments: ['@current_route_match']
- tags:
- - { name: access_check }
theme.negotiator.scheduler:
class: Drupal\scheduler\Theme\SchedulerThemeNegotiator
tags:
- { name: theme_negotiator, priority: 10 }
+ plugin.manager.scheduler:
+ class: Drupal\scheduler\SchedulerPluginManager
+ parent: default_plugin_manager
+ scheduler.route_subscriber:
+ class: Drupal\scheduler\Routing\SchedulerRouteSubscriber
+ tags:
+ - { name: event_subscriber }
diff --git a/scheduler.tokens.inc b/scheduler.tokens.inc
index 873e6a3..f60eeaa 100644
--- a/scheduler.tokens.inc
+++ b/scheduler.tokens.inc
@@ -11,16 +11,22 @@ use Drupal\Core\Render\BubbleableMetadata;
* Implements hook_token_info().
*/
function scheduler_token_info() {
- $info['tokens']['node']['scheduler-publish'] = [
- 'name' => t('Publish on date'),
- 'description' => t("The date the node will be published."),
- 'type' => 'date',
- ];
- $info['tokens']['node']['scheduler-unpublish'] = [
- 'name' => t('Unpublish on date'),
- 'description' => t("The date the node will be unpublished."),
- 'type' => 'date',
- ];
+
+ $plugin_types = \Drupal::service('scheduler.manager')->getPluginEntityTypes();
+ // Initialise the array to avoid 'variable is undefined' phpcs error.
+ $info = [];
+ foreach ($plugin_types as $type) {
+ $info['tokens'][$type]['scheduler-publish'] = [
+ 'name' => t('Publish on date'),
+ 'description' => t("The date the %type will be published.", ['%type' => $type]),
+ 'type' => 'date',
+ ];
+ $info['tokens'][$type]['scheduler-unpublish'] = [
+ 'name' => t('Unpublish on date'),
+ 'description' => t("The date the %type will be unpublished.", ['%type' => $type]),
+ 'type' => 'date',
+ ];
+ }
return $info;
}
@@ -34,31 +40,33 @@ function scheduler_tokens($type, $tokens, array $data, array $options, Bubbleabl
$language_code = isset($options['langcode']) ? $options['langcode'] : NULL;
$replacements = [];
- if ($type == 'node' && !empty($data['node'])) {
- $node = $data['node'];
+ $plugin_types = \Drupal::service('scheduler.manager')->getPluginEntityTypes();
+
+ if (in_array($type, $plugin_types) && !empty($data[$type])) {
+ $entity = $data[$type];
foreach ($tokens as $name => $original) {
switch ($name) {
case 'scheduler-publish':
- if (isset($node->publish_on->value)) {
- $replacements[$original] = $date_formatter->format($node->publish_on->value, 'medium', '', NULL, $language_code);
+ if (isset($entity->publish_on->value)) {
+ $replacements[$original] = $date_formatter->format($entity->publish_on->value, 'medium', '', NULL, $language_code);
}
break;
case 'scheduler-unpublish':
- if (isset($node->unpublish_on->value)) {
- $replacements[$original] = $date_formatter->format($node->unpublish_on->value, 'medium', '', NULL, $language_code);
+ if (isset($entity->unpublish_on->value)) {
+ $replacements[$original] = $date_formatter->format($entity->unpublish_on->value, 'medium', '', NULL, $language_code);
}
break;
}
}
// Chained token replacement.
- if (isset($node->publish_on->value) && $publish_tokens = $token_service->findWithPrefix($tokens, 'scheduler-publish')) {
- $replacements += $token_service->generate('date', $publish_tokens, ['date' => $node->publish_on->value], $options, $bubbleable_metadata);
+ if (isset($entity->publish_on->value) && $publish_tokens = $token_service->findWithPrefix($tokens, 'scheduler-publish')) {
+ $replacements += $token_service->generate('date', $publish_tokens, ['date' => $entity->publish_on->value], $options, $bubbleable_metadata);
}
- if (isset($node->unpublish_on->value) && $unpublish_tokens = $token_service->findWithPrefix($tokens, 'scheduler-unpublish')) {
- $replacements += $token_service->generate('date', $unpublish_tokens, ['date' => $node->unpublish_on->value], $options, $bubbleable_metadata);
+ if (isset($entity->unpublish_on->value) && $unpublish_tokens = $token_service->findWithPrefix($tokens, 'scheduler-unpublish')) {
+ $replacements += $token_service->generate('date', $unpublish_tokens, ['date' => $entity->unpublish_on->value], $options, $bubbleable_metadata);
}
}
diff --git a/scheduler_rules_integration/scheduler_rules_integration.module b/scheduler_rules_integration/scheduler_rules_integration.module
index fee05c9..1e8f38e 100644
--- a/scheduler_rules_integration/scheduler_rules_integration.module
+++ b/scheduler_rules_integration/scheduler_rules_integration.module
@@ -11,64 +11,78 @@
*/
use Drupal\Core\Entity\EntityInterface;
-use Drupal\scheduler_rules_integration\Event\ExistingNodeIsScheduledForPublishingEvent;
-use Drupal\scheduler_rules_integration\Event\ExistingNodeIsScheduledForUnpublishingEvent;
-use Drupal\scheduler_rules_integration\Event\NewNodeIsScheduledForPublishingEvent;
-use Drupal\scheduler_rules_integration\Event\NewNodeIsScheduledForUnpublishingEvent;
-use Drupal\scheduler_rules_integration\Event\SchedulerHasPublishedThisNodeEvent;
-use Drupal\scheduler_rules_integration\Event\SchedulerHasUnpublishedThisNodeEvent;
/**
- * Implements hook_ENTITY_TYPE_insert() for node entities.
+ * Dispatch a Rules Integration event for an entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object being processed.
+ * @param string $event_id
+ * The internal event id, for example NEW_FOR_PUBLISHING or CRON_PUBLISHED.
*/
-function scheduler_rules_integration_node_insert(EntityInterface $node) {
- // Invoke the Rules events to indicate that a new node has been scheduled.
- $scheduler_manager = \Drupal::service('scheduler.manager');
- if (!empty($node->publish_on->value)) {
- // @todo In 7.x we had the dates as parameters. These are available in Rules via node.publish_on.value so maybe we do not need the parms?
- $event = new NewNodeIsScheduledForPublishingEvent($node);
- $scheduler_manager->dispatch($event, NewNodeIsScheduledForPublishingEvent::EVENT_NAME);
- }
- if (!empty($node->unpublish_on->value)) {
- $event = new NewNodeIsScheduledForUnpublishingEvent($node);
- $scheduler_manager->dispatch($event, NewNodeIsScheduledForUnpublishingEvent::EVENT_NAME);
- }
+function _scheduler_rules_integration_event(EntityInterface $entity, $event_id) {
+ // Derive the fully namespaced event class for the given type of entity. The
+ // entity type id may contain underscores and these need to be converted to
+ // camelCase to match the event class. For example the class for 'node' is
+ // simply RulesNodeEvent, but the class for commerce_product is
+ // RulesCommerceProductEvent.
+ $camelCaseEntityType = str_replace(' ', '', ucwords(str_replace('_', ' ', $entity->getEntityTypeId())));
+ $event_class = "\Drupal\scheduler_rules_integration\Event\Rules{$camelCaseEntityType}Event";
+ $event = new $event_class($entity);
+ $event_name = constant(get_class($event) . "::$event_id");
+ \Drupal::service('scheduler.manager')->dispatch($event, $event_name);
}
/**
- * Implements hook_ENTITY_TYPE_update() for node entities.
+ * Trigger Rules events during cron.
+ *
+ * This function is called from the main Scheduler module publish() and
+ * unpublish() functions in the SchedulerManager class.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object being processed.
+ * @param string $action
+ * The action being performed - 'publish' or 'unpublish'.
*/
-function scheduler_rules_integration_node_update(EntityInterface $node) {
- // Invoke Rules events to indicate that an existing node has been scheduled.
+function _scheduler_rules_integration_dispatch_cron_event(EntityInterface $entity, $action) {
+ $event_id = strtoupper("CRON_{$action}ED");
+ _scheduler_rules_integration_event($entity, $event_id);
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function scheduler_rules_integration_entity_insert(EntityInterface $entity) {
+ // Invoke the Rules events to indicate that a new entity has been scheduled.
$scheduler_manager = \Drupal::service('scheduler.manager');
- if (!empty($node->publish_on->value)) {
- $event = new ExistingNodeIsScheduledForPublishingEvent($node, ['node' => $node]);
- $scheduler_manager->dispatch($event, ExistingNodeIsScheduledForPublishingEvent::EVENT_NAME);
+ // If this entity type is is not supported by Scheduler then go further.
+ if (!$scheduler_manager->getPlugin($entity->getEntityTypeId())) {
+ return;
+ }
+ if (!empty($entity->publish_on->value)) {
+ _scheduler_rules_integration_event($entity, 'NEW_FOR_PUBLISHING');
}
- if (!empty($node->unpublish_on->value)) {
- $event = new ExistingNodeIsScheduledForUnpublishingEvent($node);
- $scheduler_manager->dispatch($event, ExistingNodeIsScheduledForUnpublishingEvent::EVENT_NAME);
+ if (!empty($entity->unpublish_on->value)) {
+ _scheduler_rules_integration_event($entity, 'NEW_FOR_UNPUBLISHING');
}
}
/**
- * Trigger Rules events during cron.
- *
- * This function is called from the main Scheduler module publish() and
- * unpublish() functions in the SchedulerManager class.
+ * Implements hook_entity_update().
*/
-function _scheduler_rules_integration_dispatch_cron_event(EntityInterface $node, $event_type) {
+function scheduler_rules_integration_entity_update(EntityInterface $entity) {
$scheduler_manager = \Drupal::service('scheduler.manager');
- if ($event_type == 'publish') {
- // Invoke the event to tell Rules that Scheduler has published this node.
- // @todo 2nd param $publish_on may be needed as the date will no longer be on the node
- $event = new SchedulerHasPublishedThisNodeEvent($node);
- $scheduler_manager->dispatch($event, SchedulerHasPublishedThisNodeEvent::EVENT_NAME);
+ // If this entity type is is not supported by Scheduler then go further.
+ if (!$scheduler_manager->getPlugin($entity->getEntityTypeId())) {
+ return;
}
- elseif ($event_type == 'unpublish') {
- // Invoke the event to tell Rules that Scheduler has unpublished this node.
- // @todo 2nd param $publish_on may be needed as the date will no longer be on the node
- $event = new SchedulerHasUnpublishedThisNodeEvent($node);
- $scheduler_manager->dispatch($event, SchedulerHasUnpublishedThisNodeEvent::EVENT_NAME);
+
+ // Invoke Rules events to indicate that an existing entity has been scheduled.
+ if (!empty($entity->publish_on->value)) {
+ _scheduler_rules_integration_event($entity, 'EXISTING_FOR_PUBLISHING');
+ }
+
+ if (!empty($entity->unpublish_on->value)) {
+ _scheduler_rules_integration_event($entity, 'EXISTING_FOR_UNPUBLISHING');
}
}
diff --git a/scheduler_rules_integration/scheduler_rules_integration.rules.events.yml b/scheduler_rules_integration/scheduler_rules_integration.rules.events.yml
index faeee74..f60ded9 100644
--- a/scheduler_rules_integration/scheduler_rules_integration.rules.events.yml
+++ b/scheduler_rules_integration/scheduler_rules_integration.rules.events.yml
@@ -1,26 +1,16 @@
+# Six events dispatched for node entity types.
scheduler_new_node_is_scheduled_for_publishing_event:
- label: 'After saving new content that is scheduled for publishing'
- category: 'Scheduler'
+ label: 'After saving a new content item that is scheduled for publishing'
+ category: 'Content (Scheduler)'
context_definitions:
node:
type: 'entity:node'
label: 'Scheduled Content Node'
description: 'The node object representing the scheduled content'
-# These parameters were in the 7.x version. May not be needed, but left here
-# for reference until decision is made.
-# @TODO Add the parameters or remove these commented lines. Sep 2016
-# publish_on:
-# type: 'integer'
-# label: 'Scheduler Publish On date'
-# description: 'Date and time that the node will be published by Scheduler'
-# unpublish_on:
-# type: 'integer'
-# label: 'Scheduler Unpublish On date'
-# description: 'Date and time that the node will be unpublished by Scheduler'
scheduler_existing_node_is_scheduled_for_publishing_event:
- label: 'After updating existing content that is scheduled for publishing'
- category: 'Scheduler'
+ label: 'After updating a content item that is scheduled for publishing'
+ category: 'Content (Scheduler)'
context_definitions:
node:
type: 'entity:node'
@@ -28,8 +18,8 @@ scheduler_existing_node_is_scheduled_for_publishing_event:
description: 'The node object representing the scheduled content'
scheduler_new_node_is_scheduled_for_unpublishing_event:
- label: 'After saving new content that is scheduled for unpublishing'
- category: 'Scheduler'
+ label: 'After saving a new content item that is scheduled for unpublishing'
+ category: 'Content (Scheduler)'
context_definitions:
node:
type: 'entity:node'
@@ -37,8 +27,8 @@ scheduler_new_node_is_scheduled_for_unpublishing_event:
description: 'The node object representing the scheduled content'
scheduler_existing_node_is_scheduled_for_unpublishing_event:
- label: 'After updating existing content that is scheduled for unpublishing'
- category: 'Scheduler'
+ label: 'After updating a content item that is scheduled for unpublishing'
+ category: 'Content (Scheduler)'
context_definitions:
node:
type: 'entity:node'
@@ -46,8 +36,8 @@ scheduler_existing_node_is_scheduled_for_unpublishing_event:
description: 'The node object representing the scheduled content'
scheduler_has_published_this_node_event:
- label: 'After a node has been published by Scheduler'
- category: 'Scheduler'
+ label: 'After Scheduler has published a content item'
+ category: 'Content (Scheduler)'
context_definitions:
node:
type: 'entity:node'
@@ -55,10 +45,17 @@ scheduler_has_published_this_node_event:
description: 'The node object representing the scheduled content'
scheduler_has_unpublished_this_node_event:
- label: 'After a node has been unpublished by Scheduler'
- category: 'Scheduler'
+ label: 'After Scheduler has unpublished a content item'
+ category: 'Content (Scheduler)'
context_definitions:
node:
type: 'entity:node'
label: 'Scheduled Content Node'
description: 'The node object representing the scheduled content'
+
+# Use a deriver to build the corresponding six events for all other entity
+# types that are supported by Scheduler. This will not create any node events,
+# as they need to remain unchanged as above for backwards compatibilty.
+scheduler:
+ deriver: 'Drupal\scheduler_rules_integration\Event\EventDeriver'
+ class: '\Drupal\rules\EventHandler\ConfigurableEventHandlerEntityBundle'
diff --git a/scheduler_rules_integration/scheduler_rules_integration.rules_defaults.inc b/scheduler_rules_integration/scheduler_rules_integration.rules_defaults.inc
index 1d86d63..970d975 100644
--- a/scheduler_rules_integration/scheduler_rules_integration.rules_defaults.inc
+++ b/scheduler_rules_integration/scheduler_rules_integration.rules_defaults.inc
@@ -14,6 +14,9 @@
* @todo Convert to 8.x, this is stil 7.x code
*/
function scheduler_rules_integration_default_rules_configuration() {
+ // Initialise the array to avoid 'variable is undefined' phpcs error.
+ $configs = [];
+
// Define two reaction rules which will be displayed on the 'Rules' tab. These
// are initially inactive, but the user can enable them, and then modify the
// values and/or add more conditions and actions.
diff --git a/scheduler_rules_integration/src/Event/EventBase.php b/scheduler_rules_integration/src/Event/EventBase.php
new file mode 100644
index 0000000..6819830
--- /dev/null
+++ b/scheduler_rules_integration/src/Event/EventBase.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Event;
+
+// Drupal\Component\EventDispatcher\Event was introduced in Drupal core 9.1 to
+// assist with deprecations and the transition to Symfony 5.
+// @todo Remove this when core 9.1 is the lowest supported version.
+// @see https://www.drupal.org/project/scheduler/issues/3166688
+if (!class_exists('Drupal\Component\EventDispatcher\Event')) {
+ class_alias('Symfony\Component\EventDispatcher\Event', 'Drupal\Component\EventDispatcher\Event');
+}
+
+use Drupal\Component\EventDispatcher\Event;
+
+/**
+ * Base class on which all Scheduler Rules Integration events are extended.
+ */
+class EventBase extends Event {}
diff --git a/scheduler_rules_integration/src/Event/EventDeriver.php b/scheduler_rules_integration/src/Event/EventDeriver.php
new file mode 100644
index 0000000..402c0cd
--- /dev/null
+++ b/scheduler_rules_integration/src/Event/EventDeriver.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Event;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\scheduler\SchedulerManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Derives Rules events for all non-node entities supported by Scheduler.
+ *
+ * This creates events with names starting with a prefix of "scheduler:" as
+ * defined by the property name in scheduler_rules_integration.rules.events.yml,
+ * followed by the text in keys of the array $this->derivatives.
+ *
+ * The processing below is based on code in the Rules module. For an example see
+ * src/Plugin/RulesEvent/EntityUpdateDeriver.php. For backwards compatibility
+ * the node event names must remain unchnaged, and this is not possible when
+ * using this deriver. Hence the node event names stay written out long-hand in
+ * scheduler_rules_integration.rules.events.yml.
+ */
+class EventDeriver extends DeriverBase implements ContainerDeriverInterface {
+ use StringTranslationTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The scheduler manager.
+ *
+ * @var \Drupal\scheduler\SchedulerManager
+ */
+ protected $schedulerManager;
+
+ /**
+ * Creates a new EventDeriver object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+ * The string translation service.
+ * @param \Drupal\scheduler\SchedulerManager $scheduler_manager
+ * The scheduler manager.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, SchedulerManager $scheduler_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->stringTranslation = $string_translation;
+ $this->schedulerManager = $scheduler_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('string_translation'),
+ $container->get('scheduler.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ // Get all entity types supported by Scheduler plugins.
+ foreach ($this->schedulerManager->getPluginEntityTypes() as $entity_type_id) {
+ // Node events are the originals, and for backwards-compatibility those
+ // event ids must remain unchanged, which cannot be done with the deriver.
+ // So they remain defined in scheduler_rules_integration.rules.events.yml.
+ if ($entity_type_id == 'node') {
+ continue;
+ }
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+
+ // Define the values that are the same for all events of this entity type.
+ $defaults = [
+ 'entity_type_id' => $entity_type_id,
+ 'category' => $entity_type->getLabel() . ' (' . $this->t('Scheduler') . ')',
+ 'context_definitions' => [
+ $entity_type_id => [
+ 'type' => "entity:$entity_type_id",
+ 'label' => $this->t('The object representing the scheduled @entity_type', ['@entity_type' => $entity_type->getLabel()]),
+ ],
+ ],
+ ];
+
+ // Create six events for this entity type.
+ $this->derivatives["new_{$entity_type_id}_is_scheduled_for_publishing"] = [
+ 'label' => $this->t('After saving a new @entity_type that is scheduled for publishing', ['@entity_type' => $entity_type->getSingularLabel()]),
+ ] + $defaults + $base_plugin_definition;
+
+ $this->derivatives["new_{$entity_type_id}_is_scheduled_for_unpublishing"] = [
+ 'label' => $this->t('After saving a new @entity_type that is scheduled for unpublishing', ['@entity_type' => $entity_type->getSingularLabel()]),
+ ] + $defaults + $base_plugin_definition;
+
+ $this->derivatives["existing_{$entity_type_id}_is_scheduled_for_publishing"] = [
+ 'label' => $this->t('After updating a @entity_type that is scheduled for publishing', ['@entity_type' => $entity_type->getSingularLabel()]),
+ ] + $defaults + $base_plugin_definition;
+
+ $this->derivatives["existing_{$entity_type_id}_is_scheduled_for_unpublishing"] = [
+ 'label' => $this->t('After updating a @entity_type that is scheduled for unpublishing', ['@entity_type' => $entity_type->getSingularLabel()]),
+ ] + $defaults + $base_plugin_definition;
+
+ $this->derivatives["{$entity_type_id}_has_been_published_via_cron"] = [
+ 'label' => $this->t('After Scheduler has published a @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]),
+ ] + $defaults + $base_plugin_definition;
+
+ $this->derivatives["{$entity_type_id}_has_been_unpublished_via_cron"] = [
+ 'label' => $this->t('After Scheduler has unpublished a @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]),
+ ] + $defaults + $base_plugin_definition;
+
+ }
+ return $this->derivatives;
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Event/ExistingNodeIsScheduledForPublishingEvent.php b/scheduler_rules_integration/src/Event/ExistingNodeIsScheduledForPublishingEvent.php
deleted file mode 100644
index dd0f3d3..0000000
--- a/scheduler_rules_integration/src/Event/ExistingNodeIsScheduledForPublishingEvent.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Event;
-
-use Drupal\scheduler\EventBase;
-
-/**
- * An existing node is scheduled for publishing.
- *
- * This event is fired when an existing node is updated/saved and it has a
- * scheduled publishing date.
- */
-class ExistingNodeIsScheduledForPublishingEvent extends EventBase {
-
- const EVENT_NAME = 'scheduler_existing_node_is_scheduled_for_publishing_event';
-
-}
diff --git a/scheduler_rules_integration/src/Event/ExistingNodeIsScheduledForUnpublishingEvent.php b/scheduler_rules_integration/src/Event/ExistingNodeIsScheduledForUnpublishingEvent.php
deleted file mode 100644
index 1493bc9..0000000
--- a/scheduler_rules_integration/src/Event/ExistingNodeIsScheduledForUnpublishingEvent.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Event;
-
-use Drupal\scheduler\EventBase;
-
-/**
- * An existing node is scheduled for unpublishing.
- *
- * This event is fired when an existing node is updated/saved and it has a
- * scheduled unpublishing date.
- */
-class ExistingNodeIsScheduledForUnpublishingEvent extends EventBase {
-
- const EVENT_NAME = 'scheduler_existing_node_is_scheduled_for_unpublishing_event';
-
-}
diff --git a/scheduler_rules_integration/src/Event/NewNodeIsScheduledForPublishingEvent.php b/scheduler_rules_integration/src/Event/NewNodeIsScheduledForPublishingEvent.php
deleted file mode 100644
index 39acbe3..0000000
--- a/scheduler_rules_integration/src/Event/NewNodeIsScheduledForPublishingEvent.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Event;
-
-use Drupal\scheduler\EventBase;
-
-/**
- * A new node is scheduled for publishing.
- *
- * This event is fired when a newly created node is saved for the first time
- * and it has a scheduled publishing date.
- */
-class NewNodeIsScheduledForPublishingEvent extends EventBase {
-
- const EVENT_NAME = 'scheduler_new_node_is_scheduled_for_publishing_event';
-
-}
diff --git a/scheduler_rules_integration/src/Event/NewNodeIsScheduledForUnpublishingEvent.php b/scheduler_rules_integration/src/Event/NewNodeIsScheduledForUnpublishingEvent.php
deleted file mode 100644
index dd7dc27..0000000
--- a/scheduler_rules_integration/src/Event/NewNodeIsScheduledForUnpublishingEvent.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Event;
-
-use Drupal\scheduler\EventBase;
-
-/**
- * A new node is scheduled for unpublishing.
- *
- * This event is fired when a newly created node is saved for the first time
- * and it has a scheduled unpublishing date.
- */
-class NewNodeIsScheduledForUnpublishingEvent extends EventBase {
-
- const EVENT_NAME = 'scheduler_new_node_is_scheduled_for_unpublishing_event';
-
-}
diff --git a/scheduler_rules_integration/src/Event/RulesCommerceProductEvent.php b/scheduler_rules_integration/src/Event/RulesCommerceProductEvent.php
new file mode 100644
index 0000000..f477932
--- /dev/null
+++ b/scheduler_rules_integration/src/Event/RulesCommerceProductEvent.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Event;
+
+use Drupal\commerce_product\Entity\ProductInterface;
+
+/**
+ * Class for all Commerce Product events for use in Rules module.
+ */
+class RulesCommerceProductEvent extends EventBase {
+
+ /**
+ * Define constants to convert the event identifier into the full event name.
+ *
+ * The final event names here are defined in the event deriver and are
+ * different in format from the event names for node events, as originally
+ * coded long-hand in scheduler_rules_integration.rules.events.yml.
+ * However, the identifiers (CRON_PUBLISHED, NEW_FOR_PUBLISHING, etc) are the
+ * same for all types and this is how the actual event names are retrieved.
+ */
+ const CRON_PUBLISHED = 'scheduler:commerce_product_has_been_published_via_cron';
+ const CRON_UNPUBLISHED = 'scheduler:commerce_product_has_been_unpublished_via_cron';
+ const NEW_FOR_PUBLISHING = 'scheduler:new_commerce_product_is_scheduled_for_publishing';
+ const NEW_FOR_UNPUBLISHING = 'scheduler:new_commerce_product_is_scheduled_for_unpublishing';
+ const EXISTING_FOR_PUBLISHING = 'scheduler:existing_commerce_product_is_scheduled_for_publishing';
+ const EXISTING_FOR_UNPUBLISHING = 'scheduler:existing_commerce_product_is_scheduled_for_unpublishing';
+
+ /**
+ * The commerce product which is being processed.
+ *
+ * This property name could be changed to lowerCamelCase but that would also
+ * require the context_definitions key to be changed to match. This could also
+ * be done, but when editing a rule we get commerceproduct in the drop-downs,
+ * whereas all other usages in the Rules forms have commerce_product. This is
+ * confusing for the admin/developer who has to select from this list when
+ * editing a rule. There keep the property name matching the entity type id
+ * prevent Coder from reporting the invalid name by disabling this specific
+ * sniff for this file only.
+ *
+ * phpcs:disable Drupal.NamingConventions.ValidVariableName.LowerCamelName
+ *
+ * @var Drupal\commerce_product\Entity\ProductInterface
+ */
+ public $commerce_product;
+
+ /**
+ * Constructs the object.
+ *
+ * @param Drupal\commerce_product\Entity\ProductInterface $commerce_product
+ * The commerce_product item which is being processed.
+ */
+ public function __construct(ProductInterface $commerce_product) {
+ $this->commerce_product = $commerce_product;
+ }
+
+ /**
+ * Returns the entity which is being processed.
+ */
+ public function getEntity() {
+ // The Rules module requires the entity to be stored in a specifically named
+ // property which will obviously vary according to the entity type being
+ // processed. This generic getEntity() method is not strictly required by
+ // Rules but is added for convenience when manipulating the event entity.
+ return $this->commerce_product;
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Event/RulesMediaEvent.php b/scheduler_rules_integration/src/Event/RulesMediaEvent.php
new file mode 100644
index 0000000..a2e4f97
--- /dev/null
+++ b/scheduler_rules_integration/src/Event/RulesMediaEvent.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Event;
+
+use Drupal\media\MediaInterface;
+
+/**
+ * Class for all Rules media events.
+ */
+class RulesMediaEvent extends EventBase {
+
+ /**
+ * Define constants to convert the event identifier into the full event name.
+ *
+ * The final event names here are defined in the event deriver and are
+ * different in format from the event names for node events, as originally
+ * coded long-hand in scheduler_rules_integration.rules.events.yml.
+ * However, the identifiers (CRON_PUBLISHED, NEW_FOR_PUBLISHING, etc) are the
+ * same for all types and this is how the actual event names are retrieved.
+ */
+ const CRON_PUBLISHED = 'scheduler:media_has_been_published_via_cron';
+ const CRON_UNPUBLISHED = 'scheduler:media_has_been_unpublished_via_cron';
+ const NEW_FOR_PUBLISHING = 'scheduler:new_media_is_scheduled_for_publishing';
+ const NEW_FOR_UNPUBLISHING = 'scheduler:new_media_is_scheduled_for_unpublishing';
+ const EXISTING_FOR_PUBLISHING = 'scheduler:existing_media_is_scheduled_for_publishing';
+ const EXISTING_FOR_UNPUBLISHING = 'scheduler:existing_media_is_scheduled_for_unpublishing';
+
+ /**
+ * The media item which is being processed.
+ *
+ * @var \Drupal\media\MediaInterface
+ */
+ public $media;
+
+ /**
+ * Constructs the object.
+ *
+ * @param \Drupal\media\MediaInterface $media
+ * The media item which is being processed.
+ */
+ public function __construct(MediaInterface $media) {
+ $this->media = $media;
+ }
+
+ /**
+ * Returns the entity which is being processed.
+ */
+ public function getEntity() {
+ // The Rules module requires the entity to be stored in a specifically named
+ // property which will obviously vary according to the entity type being
+ // processed. This generic getEntity() method is not strictly required by
+ // Rules but is added for convenience when manipulating the event entity.
+ return $this->media;
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Event/RulesNodeEvent.php b/scheduler_rules_integration/src/Event/RulesNodeEvent.php
new file mode 100644
index 0000000..dfabe82
--- /dev/null
+++ b/scheduler_rules_integration/src/Event/RulesNodeEvent.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Event;
+
+use Drupal\node\NodeInterface;
+
+/**
+ * Class for all Rules node events.
+ */
+class RulesNodeEvent extends EventBase {
+
+ /**
+ * Define constants to convert the event identifier into the full event name.
+ *
+ * To retain backwards compatibility the event names for node events remain as
+ * originally specified in scheduler_rules_integration.rules.events.yml. The
+ * format is different from the new events derived for other entity types.
+ * However, the identifiers (CRON_PUBLISHED, NEW_FOR_PUBLISHING, etc) are the
+ * same for all types and this is how the actual event names are retrieved.
+ */
+ const CRON_PUBLISHED = 'scheduler_has_published_this_node_event';
+ const CRON_UNPUBLISHED = 'scheduler_has_unpublished_this_node_event';
+ const NEW_FOR_PUBLISHING = 'scheduler_new_node_is_scheduled_for_publishing_event';
+ const NEW_FOR_UNPUBLISHING = 'scheduler_new_node_is_scheduled_for_unpublishing_event';
+ const EXISTING_FOR_PUBLISHING = 'scheduler_existing_node_is_scheduled_for_publishing_event';
+ const EXISTING_FOR_UNPUBLISHING = 'scheduler_existing_node_is_scheduled_for_unpublishing_event';
+
+ /**
+ * The node which is being processed.
+ *
+ * @var \Drupal\node\NodeInterface
+ */
+ public $node;
+
+ /**
+ * Constructs the object.
+ *
+ * @param \Drupal\node\NodeInterface $node
+ * The node which is being processed.
+ */
+ public function __construct(NodeInterface $node) {
+ $this->node = $node;
+ }
+
+ /**
+ * Returns the entity which is being processed.
+ */
+ public function getEntity() {
+ // The Rules module requires the entity to be stored in a specifically named
+ // property which will obviously vary according to the entity type being
+ // processed. This generic getEntity() method is not strictly required by
+ // Rules but is added for convenience when manipulating the event entity.
+ return $this->node;
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Event/SchedulerHasPublishedThisNodeEvent.php b/scheduler_rules_integration/src/Event/SchedulerHasPublishedThisNodeEvent.php
deleted file mode 100644
index cf4e467..0000000
--- a/scheduler_rules_integration/src/Event/SchedulerHasPublishedThisNodeEvent.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Event;
-
-use Drupal\scheduler\EventBase;
-
-/**
- * A node is published by Scheduler.
- *
- * This event is fired when Scheduler publishes a node via cron.
- */
-class SchedulerHasPublishedThisNodeEvent extends EventBase {
-
- const EVENT_NAME = 'scheduler_has_published_this_node_event';
-
-}
diff --git a/scheduler_rules_integration/src/Event/SchedulerHasUnpublishedThisNodeEvent.php b/scheduler_rules_integration/src/Event/SchedulerHasUnpublishedThisNodeEvent.php
deleted file mode 100644
index c1956aa..0000000
--- a/scheduler_rules_integration/src/Event/SchedulerHasUnpublishedThisNodeEvent.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Event;
-
-use Drupal\scheduler\EventBase;
-
-/**
- * A node is unpublished by Scheduler.
- *
- * This event is fired when Scheduler unpublishes a node via cron.
- */
-class SchedulerHasUnpublishedThisNodeEvent extends EventBase {
-
- const EVENT_NAME = 'scheduler_has_unpublished_this_node_event';
-
-}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/ConditionDeriver.php b/scheduler_rules_integration/src/Plugin/Condition/ConditionDeriver.php
new file mode 100644
index 0000000..5762fce
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/ConditionDeriver.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\rules\Context\ContextDefinition;
+use Drupal\scheduler\SchedulerManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Derives conditions for each supported entity type (except nodes).
+ */
+class ConditionDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The scheduler manager.
+ *
+ * @var \Drupal\scheduler\SchedulerManager
+ */
+ protected $schedulerManager;
+
+ /**
+ * Creates a new deriver object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+ * The string translation service.
+ * @param \Drupal\scheduler\SchedulerManager $scheduler_manager
+ * The scheduler manager.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, SchedulerManager $scheduler_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->stringTranslation = $string_translation;
+ $this->schedulerManager = $scheduler_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('string_translation'),
+ $container->get('scheduler.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ // Get all entity types supported by Scheduler plugins.
+ $base_plugin_id = $base_plugin_definition['id'];
+ foreach ($this->schedulerManager->getPluginEntityTypes() as $entity_type_id) {
+ // Node actions are the originals, and for backwards-compatibility those
+ // action ids must remain the same, which can not be done using this
+ // deriver. Hence the node actions are defined in the 'Legacy' classes.
+ if ($entity_type_id == 'node') {
+ continue;
+ }
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+
+ // Create a context definition object for the 'entity'. This is common
+ // to all the derivatives.
+ $entity_context_definition = ContextDefinition::create("entity:$entity_type_id")
+ ->setAssignmentRestriction(ContextDefinition::ASSIGNMENT_RESTRICTION_SELECTOR)
+ ->setRequired(TRUE);
+
+ $t_args = [
+ '@entity_type_label' => $entity_type->getLabel(),
+ '@entity_type_singular' => $entity_type->getSingularLabel(),
+ ];
+ // Define the action label, context label and description, depending on
+ // which derivative we are building.
+ switch ($base_plugin_id) {
+ case 'scheduler_publishing_is_enabled':
+ $label = $this->t('@entity_type_label type is enabled for scheduled publishing', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label', $t_args))
+ ->setDescription($this->t('The @entity_type_singular to check for the type being enabled for scheduled publishing.', $t_args));
+ break;
+
+ case 'scheduler_unpublishing_is_enabled':
+ $label = $this->t('@entity_type_label type is enabled for scheduled unpublishing', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label', $t_args))
+ ->setDescription($this->t('The @entity_type_singular to check for the type being enabled for scheduled unpublishing.', $t_args));
+ break;
+
+ case 'scheduler_entity_is_scheduled_for_publishing':
+ $label = $this->t('@entity_type_label is scheduled for publishing', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label', $t_args))
+ ->setDescription($this->t('The @entity_type_singular to check for having a scheduled publishing date.', $t_args));
+ break;
+
+ case 'scheduler_entity_is_scheduled_for_unpublishing':
+ $label = $this->t('@entity_type_label is scheduled for unpublishing', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label', $t_args))
+ ->setDescription($this->t('The @entity_type_singular to check for having a scheduled unpublishing date.', $t_args));
+ break;
+
+ default:
+ $label = 'NOT SET for ' . $base_plugin_id;
+ $entity_context_definition->setLabel($label);
+ break;
+ }
+
+ // Build the basic condition definition with the entity context.
+ $condition_definition = [
+ 'label' => $label,
+ 'entity_type_id' => $entity_type_id,
+ 'category' => $entity_type->getLabel() . ' (' . $this->t('Scheduler') . ')',
+ 'context_definitions' => [$entity_type_id => $entity_context_definition],
+ ];
+
+ // Add the full definition to the derivatives array.
+ $this->derivatives[$entity_type_id] = $condition_definition + $base_plugin_definition;
+ }
+
+ return $this->derivatives;
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyPublishingIsEnabled.php b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyPublishingIsEnabled.php
new file mode 100644
index 0000000..cc83c5c
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyPublishingIsEnabled.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\Condition\PublishingIsEnabled;
+
+/**
+ * Provides a 'Publishing is enabled' condition for nodes only.
+ *
+ * @Condition(
+ * id = "scheduler_condition_publishing_is_enabled",
+ * label = @Translation("Node type is enabled for scheduled publishing"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node to check for the type being enabled for scheduled publishing."),
+ * assignment_restriction = "selector",
+ * )
+ * }
+ * )
+ */
+class LegacyPublishingIsEnabled extends PublishingIsEnabled {}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyScheduledForPublishing.php b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyScheduledForPublishing.php
new file mode 100644
index 0000000..23a11a5
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyScheduledForPublishing.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\Condition\ScheduledForPublishing;
+
+/**
+ * Provides 'Node is scheduled for publishing' condition.
+ *
+ * @Condition(
+ * id = "scheduler_condition_node_scheduled_for_publishing",
+ * label = @Translation("Node is scheduled for publishing"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node to check for having a scheduled publishing date."),
+ * assignment_restriction = "selector",
+ * )
+ * }
+ * )
+ */
+class LegacyScheduledForPublishing extends ScheduledForPublishing {}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyScheduledForUnpublishing.php b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyScheduledForUnpublishing.php
new file mode 100644
index 0000000..96540dd
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyScheduledForUnpublishing.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\Condition\ScheduledForUnpublishing;
+
+/**
+ * Provides 'Node is scheduled for unpublishing' condition.
+ *
+ * @Condition(
+ * id = "scheduler_condition_node_scheduled_for_unpublishing",
+ * label = @Translation("Node is scheduled for unpublishing"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node to check for having a scheduled unpublishing date."),
+ * assignment_restriction = "selector",
+ * )
+ * }
+ * )
+ */
+class LegacyScheduledForUnpublishing extends ScheduledForUnpublishing {}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyUnpublishingIsEnabled.php b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyUnpublishingIsEnabled.php
new file mode 100644
index 0000000..7720125
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/Legacy/LegacyUnpublishingIsEnabled.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\Condition\UnpublishingIsEnabled;
+
+/**
+ * Provides 'Unpublishing is enabled' condition for nodes only.
+ *
+ * @Condition(
+ * id = "scheduler_condition_unpublishing_is_enabled",
+ * label = @Translation("Node type is enabled for scheduled unpublishing"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node to check for the type being enabled for scheduled unpublishing."),
+ * assignment_restriction = "selector",
+ * )
+ * }
+ * )
+ */
+class LegacyUnpublishingIsEnabled extends UnpublishingIsEnabled {}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/NodeIsScheduledForPublishing.php b/scheduler_rules_integration/src/Plugin/Condition/NodeIsScheduledForPublishing.php
deleted file mode 100644
index 8f1dfaa..0000000
--- a/scheduler_rules_integration/src/Plugin/Condition/NodeIsScheduledForPublishing.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Plugin\Condition;
-
-use Drupal\rules\Core\RulesConditionBase;
-
-/**
- * Provides 'Node is scheduled for publishing' condition.
- *
- * @Condition(
- * id = "scheduler_condition_node_scheduled_for_publishing",
- * label = @Translation("Node is scheduled for publishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Scheduled Node"),
- * description = @Translation("The node to test for having a scheduled publishing date. Enter 'node' or use data selection.")
- * )
- * }
- * )
- */
-class NodeIsScheduledForPublishing extends RulesConditionBase {
-
- /**
- * Determines whether a node is scheduled for publishing.
- *
- * @return bool
- * TRUE if the node is scheduled for publishing, FALSE if not.
- */
- protected function doEvaluate() {
- $node = $this->getContextValue('node');
- return !empty($node->publish_on->value);
- }
-
-}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/NodeIsScheduledForUnpublishing.php b/scheduler_rules_integration/src/Plugin/Condition/NodeIsScheduledForUnpublishing.php
deleted file mode 100644
index 31460af..0000000
--- a/scheduler_rules_integration/src/Plugin/Condition/NodeIsScheduledForUnpublishing.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-namespace Drupal\scheduler_rules_integration\Plugin\Condition;
-
-use Drupal\rules\Core\RulesConditionBase;
-
-/**
- * Provides 'Node is scheduled for unpublishing' condition.
- *
- * @Condition(
- * id = "scheduler_condition_node_scheduled_for_unpublishing",
- * label = @Translation("Node is scheduled for unpublishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Scheduled Node"),
- * description = @Translation("The node to test for having a scheduled unpublishing date. Enter 'node' or use data selection.")
- * )
- * }
- * )
- */
-class NodeIsScheduledForUnpublishing extends RulesConditionBase {
-
- /**
- * Determines whether a node is scheduled for unpublishing.
- *
- * @return bool
- * TRUE if the node is scheduled for unpublishing, FALSE if not.
- */
- protected function doEvaluate() {
- $node = $this->getContextValue('node');
- return !empty($node->unpublish_on->value);
- }
-
-}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/PublishingIsEnabled.php b/scheduler_rules_integration/src/Plugin/Condition/PublishingIsEnabled.php
index dffa941..8f8a9de 100644
--- a/scheduler_rules_integration/src/Plugin/Condition/PublishingIsEnabled.php
+++ b/scheduler_rules_integration/src/Plugin/Condition/PublishingIsEnabled.php
@@ -2,36 +2,33 @@
namespace Drupal\scheduler_rules_integration\Plugin\Condition;
+use Drupal\Core\Entity\EntityInterface;
use Drupal\rules\Core\RulesConditionBase;
/**
- * Provides a 'Publishing is enabled' condition.
+ * Provides 'Publishing is enabled for the type of this entity' condition.
*
* @Condition(
- * id = "scheduler_condition_publishing_is_enabled",
- * label = @Translation("Node type is enabled for scheduled publishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Scheduled Node"),
- * description = @Translation("The node to check for scheduled publishing enabled. Enter 'node' or use data selection.")
- * )
- * }
+ * id = "scheduler_publishing_is_enabled",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\Condition\ConditionDeriver"
* )
*/
class PublishingIsEnabled extends RulesConditionBase {
/**
- * Determines whether scheduled publishing is enabled for this node type.
+ * Determines whether scheduled publishing is enabled for this entity type.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be checked.
*
* @return bool
- * TRUE if scheduled publishing is enabled for the content type of this
- * node.
+ * TRUE if scheduled publishing is enabled for the bundle of this entity
+ * type.
*/
- public function evaluate() {
- $node = $this->getContextValue('node');
+ public function doEvaluate(EntityInterface $entity) {
$config = \Drupal::config('scheduler.settings');
- return ($node->type->entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable')));
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ return ($entity->$bundle_field->entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable')));
}
}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/ScheduledForPublishing.php b/scheduler_rules_integration/src/Plugin/Condition/ScheduledForPublishing.php
new file mode 100644
index 0000000..5ae791c
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/ScheduledForPublishing.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\rules\Core\RulesConditionBase;
+
+/**
+ * Provides 'Entity is scheduled for publishing' condition.
+ *
+ * @Condition(
+ * id = "scheduler_entity_is_scheduled_for_publishing",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\Condition\ConditionDeriver"
+ * )
+ */
+class ScheduledForPublishing extends RulesConditionBase {
+
+ /**
+ * Determines whether an entity is scheduled for publishing.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be checked.
+ *
+ * @return bool
+ * TRUE if the entity is scheduled for publishing, FALSE if not.
+ */
+ public function doEvaluate(EntityInterface $entity) {
+ return isset($entity->publish_on->value);
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/ScheduledForUnpublishing.php b/scheduler_rules_integration/src/Plugin/Condition/ScheduledForUnpublishing.php
new file mode 100644
index 0000000..e047440
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/Condition/ScheduledForUnpublishing.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\Condition;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\rules\Core\RulesConditionBase;
+
+/**
+ * Provides 'Entity is scheduled for publishing' condition.
+ *
+ * @Condition(
+ * id = "scheduler_entity_is_scheduled_for_unpublishing",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\Condition\ConditionDeriver"
+ * )
+ */
+class ScheduledForUnpublishing extends RulesConditionBase {
+
+ /**
+ * Determines whether an entity is scheduled for unpublishing.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be checked.
+ *
+ * @return bool
+ * TRUE if the entity is scheduled for unpublishing, FALSE if not.
+ */
+ public function doEvaluate(EntityInterface $entity) {
+ return isset($entity->unpublish_on->value);
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Plugin/Condition/UnpublishingIsEnabled.php b/scheduler_rules_integration/src/Plugin/Condition/UnpublishingIsEnabled.php
index 9e1243e..01f62ff 100644
--- a/scheduler_rules_integration/src/Plugin/Condition/UnpublishingIsEnabled.php
+++ b/scheduler_rules_integration/src/Plugin/Condition/UnpublishingIsEnabled.php
@@ -2,36 +2,33 @@
namespace Drupal\scheduler_rules_integration\Plugin\Condition;
+use Drupal\Core\Entity\EntityInterface;
use Drupal\rules\Core\RulesConditionBase;
/**
- * Provides 'Unpublishing is enabled' condition.
+ * Provides 'Unpublishing is enabled for the type of this entity' condition.
*
* @Condition(
- * id = "scheduler_condition_unpublishing_is_enabled",
- * label = @Translation("Node type is enabled for scheduled unpublishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Scheduled Node"),
- * description = @Translation("The node to check for scheduled unpublishing enabled. Enter 'node' or use data selection.")
- * )
- * }
+ * id = "scheduler_unpublishing_is_enabled",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\Condition\ConditionDeriver"
* )
*/
class UnpublishingIsEnabled extends RulesConditionBase {
/**
- * Determines whether scheduled unpublishing is enabled for this node type.
+ * Determines whether scheduled unpublishing is enabled for this entity type.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be checked.
*
* @return bool
- * TRUE if scheduled unpublishing is enabled for the content type of this
- * node.
+ * TRUE if scheduled unpublishing is enabled for the bundle of this entity
+ * type.
*/
- public function evaluate() {
- $node = $this->getContextValue('node');
+ public function doEvaluate(EntityInterface $entity) {
$config = \Drupal::config('scheduler.settings');
- return ($node->type->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable')));
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ return ($entity->$bundle_field->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable')));
}
}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyPublishNow.php b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyPublishNow.php
new file mode 100644
index 0000000..91c86fa
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyPublishNow.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\RulesAction\PublishNow;
+
+/**
+ * Provides a 'Publish the node immediately' action.
+ *
+ * @RulesAction(
+ * id = "scheduler_publish_now_action",
+ * entity_type_id = "node",
+ * label = @Translation("Publish a content item immediately"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node to be published now"),
+ * assignment_restriction = "selector",
+ * ),
+ * }
+ * )
+ */
+class LegacyPublishNow extends PublishNow {}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyRemovePublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyRemovePublishingDate.php
new file mode 100644
index 0000000..6c4393f
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyRemovePublishingDate.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\RulesAction\RemovePublishingDate;
+
+/**
+ * Provides a 'Remove date for scheduled publishing' action, for nodes only.
+ *
+ * @RulesAction(
+ * id = "scheduler_remove_publishing_date_action",
+ * entity_type_id = "node",
+ * label = @Translation("Remove date for publishing a content item"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node from which to remove the scheduled publishing date"),
+ * assignment_restriction = "selector",
+ * ),
+ * }
+ * )
+ */
+class LegacyRemovePublishingDate extends RemovePublishingDate {}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyRemoveUnpublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyRemoveUnpublishingDate.php
new file mode 100644
index 0000000..5fd4015
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyRemoveUnpublishingDate.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\RulesAction\RemoveUnpublishingDate;
+
+/**
+ * Provides a 'Remove date for scheduled unpublishing' action for nodes only.
+ *
+ * @RulesAction(
+ * id = "scheduler_remove_unpublishing_date_action",
+ * entity_type_id = "node",
+ * label = @Translation("Remove date for unpublishing a content item"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node from which to remove the scheduled unpublishing date"),
+ * assignment_restriction = "selector",
+ * ),
+ * }
+ * )
+ */
+class LegacyRemoveUnpublishingDate extends RemoveUnpublishingDate {}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacySetPublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacySetPublishingDate.php
new file mode 100644
index 0000000..87f7486
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacySetPublishingDate.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\RulesAction\SetPublishingDate;
+
+/**
+ * Provides the 'Set date for scheduled unpublishing' action just for nodes.
+ *
+ * @RulesAction(
+ * id = "scheduler_set_publishing_date_action",
+ * entity_type_id = "node",
+ * label = @Translation("Set date for publishing a content item"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node for scheduling"),
+ * description = @Translation("The node which is to have a scheduled publishing date set"),
+ * assignment_restriction = "selector",
+ * ),
+ * "date" = @ContextDefinition("timestamp",
+ * label = @Translation("The date for publishing"),
+ * description = @Translation("The date when Scheduler will publish the node"),
+ * )
+ * }
+ * )
+ */
+class LegacySetPublishingDate extends SetPublishingDate {}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacySetUnpublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacySetUnpublishingDate.php
new file mode 100644
index 0000000..d5829ed
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacySetUnpublishingDate.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\RulesAction\SetUnpublishingDate;
+
+/**
+ * Provides a 'Set date for scheduled unpublishing' action just for nodes.
+ *
+ * @RulesAction(
+ * id = "scheduler_set_unpublishing_date_action",
+ * entity_type_id = "node",
+ * label = @Translation("Set date for unpublishing a content item"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node for scheduling"),
+ * description = @Translation("The node which is to have a scheduled unpublishing date set"),
+ * assignment_restriction = "selector",
+ * ),
+ * "date" = @ContextDefinition("timestamp",
+ * label = @Translation("The date for unpublishing"),
+ * description = @Translation("The date when Scheduler will unpublish the node"),
+ * )
+ * }
+ * )
+ */
+class LegacySetUnpublishingDate extends SetUnpublishingDate {}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyUnpublishNow.php b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyUnpublishNow.php
new file mode 100644
index 0000000..bc580be
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/Legacy/LegacyUnpublishNow.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction\Legacy;
+
+use Drupal\scheduler_rules_integration\Plugin\RulesAction\UnpublishNow;
+
+/**
+ * Provides an 'Unpublish the node immediately' action.
+ *
+ * @RulesAction(
+ * id = "scheduler_unpublish_now_action",
+ * entity_type_id = "node",
+ * label = @Translation("Unpublish a content item immediately"),
+ * category = @Translation("Content (Scheduler)"),
+ * context_definitions = {
+ * "node" = @ContextDefinition("entity:node",
+ * label = @Translation("Node"),
+ * description = @Translation("The node to be unpublished now"),
+ * assignment_restriction = "selector",
+ * ),
+ * }
+ * )
+ */
+class LegacyUnpublishNow extends UnpublishNow {}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/PublishNow.php b/scheduler_rules_integration/src/Plugin/RulesAction/PublishNow.php
index b7e2465..a929988 100644
--- a/scheduler_rules_integration/src/Plugin/RulesAction/PublishNow.php
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/PublishNow.php
@@ -2,35 +2,30 @@
namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
-use Drupal\rules\Core\RulesActionBase;
+use Drupal\Core\Entity\EntityInterface;
/**
- * Provides a 'Publish the node immediately' action.
+ * Provides a 'Publish immediately' action.
*
* @RulesAction(
- * id = "scheduler_publish_now_action",
- * label = @Translation("Publish the content immediately"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Node"),
- * description = @Translation("The node to be published now"),
- * ),
- * }
+ * id = "scheduler_publish_now",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\RulesAction\SchedulerRulesActionDeriver"
* )
*/
-class PublishNow extends RulesActionBase {
+class PublishNow extends SchedulerRulesActionBase {
/**
- * Set the node status to Published.
+ * Set the entity status to Published.
*
- * This action should really be provided by Rules or by Core, but it is not
- * yet done (as of Aug 2016). Scheduler users need this action so we provide
- * it here. It could be removed later when Rules or Core includes it.
+ * This action is provided by the Rules Module but only for node content, not
+ * Media. There is also a problem with recursion in the Rules action due to
+ * autoSaveContext(). Hence better for Scheduler to provide this action.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be published.
*/
- public function doExecute() {
- $node = $this->getContextValue('node');
- $node->setPublished();
+ public function doExecute(EntityInterface $entity) {
+ $entity->setPublished();
}
}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/RemovePublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/RemovePublishingDate.php
index 1ddebd0..f6c7fed 100644
--- a/scheduler_rules_integration/src/Plugin/RulesAction/RemovePublishingDate.php
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/RemovePublishingDate.php
@@ -2,60 +2,35 @@
namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\rules\Core\RulesActionBase;
+use Drupal\Core\Entity\EntityInterface;
/**
* Provides a 'Remove date for scheduled publishing' action.
*
* @RulesAction(
- * id = "scheduler_remove_publishing_date_action",
- * label = @Translation("Remove date for scheduled publishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Node"),
- * description = @Translation("The node from which to remove the scheduled publishing date"),
- * ),
- * }
+ * id = "scheduler_remove_publishing_date",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\RulesAction\SchedulerRulesActionDeriver"
* )
*/
-class RemovePublishingDate extends RulesActionBase {
+class RemovePublishingDate extends SchedulerRulesActionBase {
/**
- * Remove the publish_on date from the node.
+ * Remove the publish_on date from the entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity from which to remove the scheduled date.
*/
- public function doExecute() {
- $node = $this->getContextValue('node');
+ public function doExecute(EntityInterface $entity) {
$config = \Drupal::config('scheduler.settings');
- if ($node->type->entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'))) {
- $node->set('publish_on', NULL);
- scheduler_node_presave($node);
- scheduler_node_update($node);
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ if ($entity->$bundle_field->entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'))) {
+ $entity->set('publish_on', NULL);
+ scheduler_entity_presave($entity);
}
else {
// The action cannot be executed because the content type is not enabled
// for scheduled publishing.
- $action_label = $this->summary();
- // @todo Can we get the condition description from the actual condition
- // object instead of hard-coding it here?
- $condition = $this->t('Node type is enabled for scheduled publishing');
- $type_name = node_get_type_label($node);
- $url = new Url('entity.node_type.edit_form', ['node_type' => $node->getType()]);
- $arguments = [
- '%type' => $type_name,
- '%action_label' => $action_label,
- '%condition' => $condition,
- '@url' => $url->toString(),
- ];
-
- $link = Link::fromTextAndUrl($this->t('@type settings', ['@type' => $type_name]), $url)->toString();
- \Drupal::logger('scheduler')->warning('Action "%action_label" is not valid because scheduled publishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled publishing via the %type settings.',
- $arguments + ['link' => $link]);
-
- \Drupal::messenger()->addMessage($this->t('Action "%action_label" is not valid because scheduled publishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled publishing via the <a href="@url">%type</a> settings.',
- $arguments), 'warning', FALSE);
+ $this->notEnabledWarning($entity, 'publish');
}
}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/RemoveUnpublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/RemoveUnpublishingDate.php
index a3130d5..dce7fad 100644
--- a/scheduler_rules_integration/src/Plugin/RulesAction/RemoveUnpublishingDate.php
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/RemoveUnpublishingDate.php
@@ -2,60 +2,35 @@
namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\rules\Core\RulesActionBase;
+use Drupal\Core\Entity\EntityInterface;
/**
* Provides a 'Remove date for scheduled unpublishing' action.
*
* @RulesAction(
- * id = "scheduler_remove_unpublishing_date_action",
- * label = @Translation("Remove date for scheduled unpublishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Node"),
- * description = @Translation("The node from which to remove the scheduled unpublishing date"),
- * ),
- * }
+ * id = "scheduler_remove_unpublishing_date",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\RulesAction\SchedulerRulesActionDeriver"
* )
*/
-class RemoveUnpublishingDate extends RulesActionBase {
+class RemoveUnpublishingDate extends SchedulerRulesActionBase {
/**
- * Remove the unpublish_on date from the node.
+ * Remove the unpublish_on date from the entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity from which to remove the scheduled date.
*/
- public function doExecute() {
- $node = $this->getContextValue('node');
+ public function doExecute(EntityInterface $entity) {
$config = \Drupal::config('scheduler.settings');
- if ($node->type->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'))) {
- $node->set('unpublish_on', NULL);
- scheduler_node_presave($node);
- scheduler_node_update($node);
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ if ($entity->$bundle_field->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'))) {
+ $entity->set('unpublish_on', NULL);
+ scheduler_entity_presave($entity);
}
else {
// The action cannot be executed because the content type is not enabled
// for scheduled unpublishing.
- $action_label = $this->summary();
- // @todo Can we get the condition description from the actual condition
- // object instead of hard-coding it here?
- $condition = $this->t('Node type is enabled for scheduled unpublishing');
- $type_name = node_get_type_label($node);
- $url = new Url('entity.node_type.edit_form', ['node_type' => $node->getType()]);
- $arguments = [
- '%type' => $type_name,
- '%action_label' => $action_label,
- '%condition' => $condition,
- '@url' => $url->toString(),
- ];
-
- $link = Link::fromTextAndUrl($this->t('@type settings', ['@type' => $type_name]), $url)->toString();
- \Drupal::logger('scheduler')->warning('Action "%action_label" is not valid because scheduled unpublishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled unpublishing via the %type settings.',
- $arguments + ['link' => $link]);
-
- \Drupal::messenger()->addMessage($this->t('Action "%action_label" is not valid because scheduled unpublishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled unpublishing via the <a href="@url">%type</a> settings.',
- $arguments), 'warning', FALSE);
+ $this->notEnabledWarning($entity, 'unpublish');
}
}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/SchedulerRulesActionBase.php b/scheduler_rules_integration/src/Plugin/RulesAction/SchedulerRulesActionBase.php
new file mode 100644
index 0000000..ef7eebc
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/SchedulerRulesActionBase.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\rules\Core\RulesActionBase;
+
+/**
+ * Provides base class on which all Scheduler Rules actions are built.
+ */
+class SchedulerRulesActionBase extends RulesActionBase {
+
+ /**
+ * The entity type id.
+ *
+ * @var string
+ */
+ protected $entityTypeId;
+
+ /**
+ * Constructs a SchedulerRulesActionBase object.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin ID for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->entityTypeId = $plugin_definition['entity_type_id'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition
+ );
+ }
+
+ /**
+ * Gives a warning when an entity is not enabled for Scheduler.
+ *
+ * This is called from actions that attempt to set or remove a Scheduler date
+ * value when the entity type is not enabled for that process.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object being processed by the action.
+ * @param string $process
+ * The process that is not enabled, either 'publish' or 'unpublish'.
+ */
+ public function notEnabledWarning(EntityInterface $entity, string $process) {
+ $action = $this->summary();
+ $activity = ($process == 'publish') ? $this->t('scheduled publishing') : $this->t('scheduled unpublishing');
+ $condition = $this->t('@bundle_label is enabled for @activity', [
+ '@bundle_label' => $entity->getEntityType()->getBundleLabel(),
+ '@activity' => $activity,
+ ]);
+
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ $type_name = $entity->$bundle_field->entity->label();
+ $type_id = $entity->$bundle_field->entity->bundle();
+ $url = new Url("entity.$type_id.edit_form", [$type_id => $entity->bundle()]);
+ $arguments = [
+ '%action' => "'$action'",
+ '@activity' => $activity,
+ '%type' => $type_name,
+ '@group' => $entity->getEntityType()->getPluralLabel(),
+ '%condition' => "'$condition'",
+ '@url' => $url->toString(),
+ ];
+ $link = Link::fromTextAndUrl($this->t('@type settings', ['@type' => $type_name]), $url)->toString();
+ \Drupal::logger('scheduler')->warning('Action %action is not valid because @activity is not enabled for %type @group. Add the condition %condition to your Reaction Rule, or enable @activity via the %type settings.',
+ $arguments + ['link' => $link]);
+
+ \Drupal::messenger()->addMessage($this->t('Action %action is not valid because @activity is not enabled for %type @group. Add the condition %condition to your Reaction Rule, or enable @activity via the <a href="@url">%type</a> settings.',
+ $arguments), 'warning', FALSE);
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/SchedulerRulesActionDeriver.php b/scheduler_rules_integration/src/Plugin/RulesAction/SchedulerRulesActionDeriver.php
new file mode 100644
index 0000000..a5d5842
--- /dev/null
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/SchedulerRulesActionDeriver.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\rules\Context\ContextDefinition;
+use Drupal\scheduler\SchedulerManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Derives actions for each supported entity type.
+ *
+ * Based on code from Rules module EntityCreateDeriver.
+ */
+class SchedulerRulesActionDeriver extends DeriverBase implements ContainerDeriverInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The scheduler manager.
+ *
+ * @var \Drupal\scheduler\SchedulerManager
+ */
+ protected $schedulerManager;
+
+ /**
+ * Creates a new deriver object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+ * The string translation service.
+ * @param \Drupal\scheduler\SchedulerManager $scheduler_manager
+ * The scheduler manager.
+ */
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, SchedulerManager $scheduler_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ $this->stringTranslation = $string_translation;
+ $this->schedulerManager = $scheduler_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('string_translation'),
+ $container->get('scheduler.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ // Get all entity types supported by Scheduler plugins.
+ $base_plugin_id = $base_plugin_definition['id'];
+ foreach ($this->schedulerManager->getPluginEntityTypes() as $entity_type_id) {
+ // Node actions are the originals, and for backwards-compatibility those
+ // action ids must remain the same, which can not be done using this
+ // deriver. Hence the node actions are defined in the 'Legacy' classes.
+ if ($entity_type_id == 'node') {
+ continue;
+ }
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+
+ // Create a context definition object for the 'entity'. This is common
+ // to all the derivatives.
+ $entity_context_definition = ContextDefinition::create("entity:$entity_type_id")
+ ->setAssignmentRestriction(ContextDefinition::ASSIGNMENT_RESTRICTION_SELECTOR)
+ ->setRequired(TRUE);
+
+ $t_args = [
+ '@entity_type_label' => $entity_type->getLabel(),
+ '@entity_type_singular' => $entity_type->getSingularLabel(),
+ ];
+ // Define the action label, context label and description, depending on
+ // which derivative we are building.
+ switch ($base_plugin_id) {
+ case 'scheduler_set_publishing_date':
+ $action_label = $this->t('Set date for publishing a @entity_type_singular', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label for scheduling', $t_args))
+ ->setDescription($this->t('The @entity_type_singular which is to have a scheduled publishing date set', $t_args));
+ // Define a label and description for the date context definition.
+ $date_label = $this->t('Date for publishing');
+ $date_description = $this->t('The date when Scheduler will publish the @entity_type_singular', $t_args);
+ break;
+
+ case 'scheduler_set_unpublishing_date':
+ $action_label = $this->t('Set date for unpublishing a @entity_type_singular', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label for scheduling', $t_args))
+ ->setDescription($this->t('The @entity_type_singular which is to have a scheduled unpublishing date set', $t_args));
+ $date_label = $this->t('Date for unpublishing');
+ $date_description = $this->t('The date when Scheduler will unpublish the @entity_type_singular', $t_args);
+ break;
+
+ case 'scheduler_remove_publishing_date':
+ $action_label = $this->t('Remove date for publishing a @entity_type_singular', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label', $t_args))
+ ->setDescription($this->t('The @entity_type_singular from which to remove the scheduled publishing date', $t_args));
+ break;
+
+ case 'scheduler_remove_unpublishing_date':
+ $action_label = $this->t('Remove date for unpublishing a @entity_type_singular', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label', $t_args))
+ ->setDescription($this->t('The @entity_type_singular from which to remove the scheduled unpublishing date', $t_args));
+ break;
+
+ case 'scheduler_publish_now':
+ $action_label = $this->t('Publish a @entity_type_singular immediately', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label for publishing', $t_args))
+ ->setDescription($this->t('The @entity_type_singular to be published now', $t_args));
+ break;
+
+ case 'scheduler_unpublish_now':
+ $action_label = $this->t('Unpublish a @entity_type_singular immediately', $t_args);
+ $entity_context_definition
+ ->setLabel($this->t('@entity_type_label for unpublishing', $t_args))
+ ->setDescription($this->t('The @entity_type_singular to be unpublished now', $t_args));
+ break;
+
+ default:
+ $action_label = 'NOT SET for ' . $base_plugin_id;
+ $entity_context_definition->setLabel($action_label);
+ break;
+ }
+
+ // Build the basic action definition, with the entity context, which is
+ // common to all six actions.
+ $action_definition = [
+ 'label' => $action_label,
+ 'entity_type_id' => $entity_type_id,
+ 'category' => $entity_type->getLabel() . ' (' . $this->t('Scheduler') . ')',
+ 'context_definitions' => [$entity_type_id => $entity_context_definition],
+ ];
+
+ // For the actions that set a scheduler date add the date as a second
+ // context variable.
+ if ($base_plugin_id == 'scheduler_set_publishing_date' || $base_plugin_id == 'scheduler_set_unpublishing_date') {
+ $date_context_definition = ContextDefinition::create('timestamp')
+ ->setLabel($date_label)
+ ->setDescription($date_description)
+ ->setRequired(TRUE);
+ $action_definition['context_definitions']['date'] = $date_context_definition;
+ }
+
+ // Finally add the full action definition to the derivatives array.
+ $this->derivatives[$entity_type_id] = $action_definition + $base_plugin_definition;
+ }
+
+ return $this->derivatives;
+ }
+
+}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/SetPublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/SetPublishingDate.php
index 099066e..8a4b9f6 100644
--- a/scheduler_rules_integration/src/Plugin/RulesAction/SetPublishingDate.php
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/SetPublishingDate.php
@@ -2,70 +2,41 @@
namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\rules\Core\RulesActionBase;
+use Drupal\Core\Entity\EntityInterface;
/**
* Provides a 'Set date for scheduled publishing' action.
*
* @RulesAction(
- * id = "scheduler_set_publishing_date_action",
- * label = @Translation("Set date for scheduled publishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Node for scheduling"),
- * description = @Translation("The node which is to have a scheduled publishing date set"),
- * ),
- * "date" = @ContextDefinition("timestamp",
- * label = @Translation("The date for publishing"),
- * description = @Translation("The date when Scheduler will publish the node"),
- * )
- * }
+ * id = "scheduler_set_publishing_date",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\RulesAction\SchedulerRulesActionDeriver"
* )
*/
-class SetPublishingDate extends RulesActionBase {
+class SetPublishingDate extends SchedulerRulesActionBase {
/**
- * Set the publish_on date for the node.
+ * Set the publish_on date on the entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be scheduled for publishing.
+ * @param int $date
+ * The date for publishing.
*/
- public function doExecute() {
- $node = $this->getContextValue('node');
- $date = $this->getContextValue('date');
+ public function doExecute(EntityInterface $entity, $date) {
$config = \Drupal::config('scheduler.settings');
- if ($node->type->entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'))) {
- $node->set('publish_on', $date);
- // When this action is invoked and it operates on the node being editted
- // then hook_node_presave() and hook_node_update() will be executed
- // automatically. But if this action is being used to schedule a different
- // node then we need to call the functions directly here.
- scheduler_node_presave($node);
- scheduler_node_update($node);
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ if ($entity->$bundle_field->entity->getThirdPartySetting('scheduler', 'publish_enable', $config->get('default_publish_enable'))) {
+ $entity->set('publish_on', $date);
+ // When this action is invoked and it operates on the entity being edited
+ // then hook_entity_presave() will be executed automatically. But if this
+ // action is being used to schedule a different entity then we need to
+ // call the functions directly here.
+ scheduler_entity_presave($entity);
}
else {
// The action cannot be executed because the content type is not enabled
// for scheduled publishing.
- $action_label = $this->summary();
- // @todo Can we get the condition description from the actual condition
- // object instead of hard-coding it here?
- $condition = $this->t('Node type is enabled for scheduled publishing');
- $type_name = node_get_type_label($node);
- $url = new Url('entity.node_type.edit_form', ['node_type' => $node->getType()]);
- $arguments = [
- '%type' => $type_name,
- '%action_label' => $action_label,
- '%condition' => $condition,
- '@url' => $url->toString(),
- ];
-
- $link = Link::fromTextAndUrl($this->t('@type settings', ['@type' => $type_name]), $url)->toString();
- \Drupal::logger('scheduler')->warning('Action "%action_label" is not valid because scheduled publishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled publishing via the %type settings.',
- $arguments + ['link' => $link]);
-
- \Drupal::messenger()->addMessage($this->t('Action "%action_label" is not valid because scheduled publishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled publishing via the <a href="@url">%type</a> settings.',
- $arguments), 'warning', FALSE);
-
+ $this->notEnabledWarning($entity, 'publish');
}
}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/SetUnpublishingDate.php b/scheduler_rules_integration/src/Plugin/RulesAction/SetUnpublishingDate.php
index 0e00f60..e0006d2 100644
--- a/scheduler_rules_integration/src/Plugin/RulesAction/SetUnpublishingDate.php
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/SetUnpublishingDate.php
@@ -2,69 +2,41 @@
namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
-use Drupal\Core\Link;
-use Drupal\Core\Url;
-use Drupal\rules\Core\RulesActionBase;
+use Drupal\Core\Entity\EntityInterface;
/**
* Provides a 'Set date for scheduled unpublishing' action.
*
* @RulesAction(
- * id = "scheduler_set_unpublishing_date_action",
- * label = @Translation("Set date for scheduled unpublishing"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Node for scheduling"),
- * description = @Translation("The node which is to have a scheduled unpublishing date set"),
- * ),
- * "date" = @ContextDefinition("timestamp",
- * label = @Translation("The date for unpublishing"),
- * description = @Translation("The date when Scheduler will unpublish the node"),
- * )
- * }
+ * id = "scheduler_set_unpublishing_date",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\RulesAction\SchedulerRulesActionDeriver"
* )
*/
-class SetUnpublishingDate extends RulesActionBase {
+class SetUnpublishingDate extends SchedulerRulesActionBase {
/**
- * Set the unpublish_on date for the node.
+ * Set the unpublish_on date on the entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be scheduled for unpublishing.
+ * @param int $date
+ * The date for unpublishing.
*/
- public function doExecute() {
- $node = $this->getContextValue('node');
- $date = $this->getContextValue('date');
+ public function doExecute(EntityInterface $entity, $date) {
$config = \Drupal::config('scheduler.settings');
- if ($node->type->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'))) {
- $node->set('unpublish_on', $date);
- // When this action is invoked and it operates on the node being editted
- // then hook_node_presave() and hook_node_update() will be executed
- // automatically. But if this action is being used to schedule a different
- // node then we need to call the functions directly here.
- scheduler_node_presave($node);
- scheduler_node_update($node);
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ if ($entity->$bundle_field->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $config->get('default_unpublish_enable'))) {
+ $entity->set('unpublish_on', $date);
+ // When this action is invoked and it operates on the entity being edited
+ // then hook_entity_presave() will be executed automatically. But if this
+ // action is being used to schedule a different entity then we need to
+ // call the functions directly here.
+ scheduler_entity_presave($entity);
}
else {
// The action cannot be executed because the content type is not enabled
// for scheduled unpublishing.
- $action_label = $this->summary();
- // @todo Can we get the condition description from the actual condition
- // object instead of hard-coding it here?
- $condition = $this->t('Node type is enabled for scheduled unpublishing');
- $type_name = node_get_type_label($node);
- $url = new Url('entity.node_type.edit_form', ['node_type' => $node->getType()]);
- $arguments = [
- '%type' => $type_name,
- '%action_label' => $action_label,
- '%condition' => $condition,
- '@url' => $url->toString(),
- ];
-
- $link = Link::fromTextAndUrl($this->t('@type settings', ['@type' => $type_name]), $url)->toString();
- \Drupal::logger('scheduler')->warning('Action "%action_label" is not valid because scheduled unpublishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled unpublishing via the %type settings.',
- $arguments + ['link' => $link]);
-
- \Drupal::messenger()->addMessage($this->t('Action "%action_label" is not valid because scheduled unpublishing is not enabled for %type content. Add the condition "%condition" to your Reaction Rule, or enable scheduled unpublishing via the <a href="@url">%type</a> settings.',
- $arguments), 'warning', FALSE);
+ $this->notEnabledWarning($entity, 'unpublish');
}
}
diff --git a/scheduler_rules_integration/src/Plugin/RulesAction/UnpublishNow.php b/scheduler_rules_integration/src/Plugin/RulesAction/UnpublishNow.php
index f675af4..cba53e2 100644
--- a/scheduler_rules_integration/src/Plugin/RulesAction/UnpublishNow.php
+++ b/scheduler_rules_integration/src/Plugin/RulesAction/UnpublishNow.php
@@ -2,35 +2,30 @@
namespace Drupal\scheduler_rules_integration\Plugin\RulesAction;
-use Drupal\rules\Core\RulesActionBase;
+use Drupal\Core\Entity\EntityInterface;
/**
- * Provides an 'Unpublish the node immediately' action.
+ * Provides an 'Unpublish immediately' action.
*
* @RulesAction(
- * id = "scheduler_unpublish_now_action",
- * label = @Translation("Unpublish the content immediately"),
- * category = @Translation("Scheduler"),
- * context_definitions = {
- * "node" = @ContextDefinition("entity:node",
- * label = @Translation("Node"),
- * description = @Translation("The node to be unpublished now"),
- * ),
- * }
+ * id = "scheduler_unpublish_now",
+ * deriver = "Drupal\scheduler_rules_integration\Plugin\RulesAction\SchedulerRulesActionDeriver"
* )
*/
-class UnpublishNow extends RulesActionBase {
+class UnpublishNow extends SchedulerRulesActionBase {
/**
- * Set the node status to Unpublished.
+ * Set the entity status to Unpublished.
*
- * This action should really be provided by Rules or by Core, but it is not
- * yet done (as of Aug 2016). Scheduler users need this action so we provide
- * it here. It could be removed later when Rules or Core includes it.
+ * This action is provided by the Rules Module but only for node content, not
+ * Media. There is also a problem with recursion in the Rules action due to
+ * autoSaveContext(). Hence better for Scheduler to provide this action.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to be unpublished.
*/
- public function doExecute() {
- $node = $this->getContextValue('node');
- $node->setUnpublished();
+ public function doExecute(EntityInterface $entity) {
+ $entity->setUnpublished();
}
}
diff --git a/src/Access/ScheduledListAccess.php b/src/Access/ScheduledListAccess.php
deleted file mode 100644
index a63d0e7..0000000
--- a/src/Access/ScheduledListAccess.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-namespace Drupal\scheduler\Access;
-
-use Drupal\Core\Access\AccessCheckInterface;
-use Drupal\Core\Access\AccessResult;
-use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\Session\AccountInterface;
-use Symfony\Component\Routing\Route;
-
-/**
- * Checks access for displaying the scheduler list of scheduled nodes.
- */
-class ScheduledListAccess implements AccessCheckInterface {
-
- /**
- * The current route match.
- *
- * @var \Drupal\Core\Routing\RouteMatchInterface
- */
- protected $routeMatch;
-
- /**
- * Constructs a ScheduledListAccess object.
- *
- * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
- * The current route match.
- */
- public function __construct(RouteMatchInterface $route_match) {
- $this->routeMatch = $route_match;
- }
-
- /**
- * {@inheritdoc}
- */
- public function applies(Route $route) {
- return $route->hasRequirement('_access_scheduler_content');
- }
-
- /**
- * Determine if the $account has access to the scheduled content list.
- *
- * The result will vary depending on whether the page being viewed is the user
- * profile page or the scheduled content admin overview.
- */
- public function access(AccountInterface $account) {
- // When viewing a user profile page routeMatch->getRawParameter('user')
- // returns the user's id. If not on a user page it returns NULL silently.
- $viewing_own_tab = $this->routeMatch->getRawParameter('user') == $account->id();
-
- // Users with 'schedule publishing of nodes' can see their own scheduled
- // content via a tab on their user page. Users with 'view scheduled content'
- // will be able to access the 'scheduled' tab for any user, and also access
- // the scheduled content overview page.
- $allowed = $account->hasPermission('view scheduled content')
- || ($viewing_own_tab && $account->hasPermission('schedule publishing of nodes'));
- return $allowed ? AccessResult::allowed() : AccessResult::forbidden();
- }
-
-}
diff --git a/src/Access/SchedulerRouteAccess.php b/src/Access/SchedulerRouteAccess.php
new file mode 100644
index 0000000..771818e
--- /dev/null
+++ b/src/Access/SchedulerRouteAccess.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\scheduler\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\user\Entity\User;
+
+/**
+ * Sets access for specific scheduler views routes.
+ */
+class SchedulerRouteAccess {
+
+ /**
+ * Provides custom access checks for the scheduled views on the user page.
+ *
+ * A user is given access if either of the following conditions are met:
+ * - they are viewing their own page and they have the permission to schedule
+ * content of the required type.
+ * - they are viewing another user's page and they have permission to view
+ * user profiles and view scheduled content, and the user they are viewing has
+ * permission to schedule content (otherwise the list would always be empty).
+ *
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The currently logged in account.
+ * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+ * The current route match.
+ */
+ public function access(AccountInterface $account, RouteMatchInterface $route_match) {
+ $user_being_viewed = $route_match->getParameter('user');
+ $viewing_own_page = $user_being_viewed == $account->id();
+
+ // getUserPageViewRoutes() returns an array of user page view routes, keyed
+ // on the entity id. Use this to get the entity id.
+ $scheduler_manager = \Drupal::service('scheduler.manager');
+ $entityTypeId = array_search($route_match->getRouteName(), $scheduler_manager->getUserPageViewRoutes());
+ $viewing_permission_name = $scheduler_manager->permissionName($entityTypeId, 'view');
+ $scheduling_permission_name = $scheduler_manager->permissionName($entityTypeId, 'schedule');
+
+ if ($viewing_own_page && $account->hasPermission($scheduling_permission_name)) {
+ return AccessResult::allowed();
+ }
+ if (!$viewing_own_page && $account->hasPermission($viewing_permission_name) && $account->hasPermission('access user profiles')) {
+ $other_user = User::load($user_being_viewed);
+ if ($other_user && $other_user->hasPermission($scheduling_permission_name)) {
+ return AccessResult::allowed();
+ }
+ }
+ return AccessResult::forbidden();
+ }
+
+}
diff --git a/src/Annotation/SchedulerPlugin.php b/src/Annotation/SchedulerPlugin.php
new file mode 100644
index 0000000..9f86c4c
--- /dev/null
+++ b/src/Annotation/SchedulerPlugin.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\scheduler\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Annotation class for scheduler entity plugins.
+ *
+ * @package Drupal\scheduler\Annotation
+ *
+ * @Annotation
+ */
+class SchedulerPlugin extends Plugin {
+
+ /**
+ * The internal id / machine name of the plugin.
+ *
+ * @var string
+ */
+ public $id;
+
+ /**
+ * The readable name of the plugin.
+ *
+ * @var \Drupal\Core\Annotation\Translation
+ *
+ * @ingroup plugin_translatable
+ */
+ public $label;
+
+ /**
+ * Description of plugin.
+ *
+ * @var \Drupal\Core\Annotation\Translation
+ *
+ * @ingroup plugin_translatable
+ */
+ public $description;
+
+ /**
+ * The entity type.
+ *
+ * @var string
+ */
+ public $entityType;
+
+ /**
+ * The name of the type/bundle field.
+ *
+ * @var string
+ */
+ public $typeFieldName;
+
+ /**
+ * Module name that plugin requires.
+ *
+ * @var string
+ */
+ public $dependency;
+
+ /**
+ * The Form ID of the devel generate form (optional).
+ *
+ * @var string
+ */
+ public $develGenerateForm = '';
+
+ /**
+ * The route of the scheduled view on the user profile page (optional).
+ *
+ * @var string
+ */
+ public $userViewRoute = '';
+
+ /**
+ * The event class for Scheduler events relating to activity on the entity.
+ *
+ * If not specified, it is assumed that the plugin is part of the Scheduler
+ * module and the event class will default to Scheduler's namespace.
+ *
+ * @var string
+ */
+ public $schedulerEventClass;
+
+ /**
+ * The name of the publish action for the entity type (optional).
+ *
+ * This is used when the action name does not match the default pattern of
+ * {entity type id}_publish_action.
+ *
+ * @var string
+ */
+ public $publishAction;
+
+ /**
+ * The name of the unpublish action for the entity type (optional).
+ *
+ * This is used when the action name does not match the default pattern of
+ * {entity type id}_unpublish_action.
+ *
+ * @var string
+ */
+ public $unpublishAction;
+
+}
diff --git a/src/Commands/SchedulerCommands.php b/src/Commands/SchedulerCommands.php
index 0d73d2b..e2cff3e 100644
--- a/src/Commands/SchedulerCommands.php
+++ b/src/Commands/SchedulerCommands.php
@@ -61,4 +61,18 @@ class SchedulerCommands extends DrushCommands {
$options['nomsg'] ? NULL : $this->messenger->addMessage(dt('Scheduler lightweight cron completed.'));
}
+ /**
+ * Entity Update - add Scheduler fields for entities covered by plugins.
+ *
+ * Use the standard drush parameter -q for quiet mode (no terminal output).
+ *
+ * @command scheduler:entity-update
+ * @aliases sch-ent-upd, scheduler-entity-update
+ */
+ public function entityUpdate() {
+ $result = $this->schedulerManager->entityUpdate();
+ $updated = $result ? implode(', ', $result) : dt('nothing to update');
+ $this->messenger->addMessage(dt('Scheduler entity update - @updated', ['@updated' => $updated]));
+ }
+
}
diff --git a/src/Event/EventBase.php b/src/Event/EventBase.php
new file mode 100644
index 0000000..ac4dd76
--- /dev/null
+++ b/src/Event/EventBase.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\scheduler\Event;
+
+// Drupal\Component\EventDispatcher\Event was introduced in Drupal core 9.1 to
+// assist with deprecations and the transition to Symfony 5.
+// @todo Remove this when core 9.1 is the lowest supported version.
+// @see https://www.drupal.org/project/scheduler/issues/3166688
+if (!class_exists('Drupal\Component\EventDispatcher\Event')) {
+ class_alias('Symfony\Component\EventDispatcher\Event', 'Drupal\Component\EventDispatcher\Event');
+}
+
+use Drupal\Component\EventDispatcher\Event;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Base class on which all Scheduler events are extended.
+ */
+class EventBase extends Event {
+
+ /**
+ * The entity which is being processed.
+ *
+ * @var \Drupal\Core\Entity\EntityInterface
+ */
+ public $entity;
+
+ /**
+ * Constructs the object.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity which is being processed.
+ */
+ public function __construct(EntityInterface $entity) {
+ $this->entity = $entity;
+ }
+
+}
diff --git a/src/Event/SchedulerCommerceProductEvents.php b/src/Event/SchedulerCommerceProductEvents.php
new file mode 100644
index 0000000..df81f26
--- /dev/null
+++ b/src/Event/SchedulerCommerceProductEvents.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\scheduler\Event;
+
+/**
+ * Lists the six events dispatched by Scheduler for commerce_product entities.
+ */
+final class SchedulerCommerceProductEvents {
+
+ /**
+ * The event triggered after a commerce_product item is published immediately.
+ *
+ * This event allows modules to react after an entity is published
+ * immediately when being saved after editing. The event listener method
+ * receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PUBLISH_IMMEDIATELY = 'scheduler.commerce_product_publish_immediately';
+
+ /**
+ * The event triggered after a commerce_product item is published by cron.
+ *
+ * This event allows modules to react after an entity is published by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PUBLISH = 'scheduler.commerce_product_publish';
+
+ /**
+ * The event triggered before a commerce_product is published immediately.
+ *
+ * This event allows modules to react before an entity is published
+ * immediately when being saved after editing. The event listener method
+ * receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_PUBLISH_IMMEDIATELY = 'scheduler.commerce_product_pre_publish_immediately';
+
+ /**
+ * The event triggered before a commerce_product item is published by cron.
+ *
+ * This event allows modules to react before an entity is published by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_PUBLISH = 'scheduler.commerce_product_pre_publish';
+
+ /**
+ * The event triggered before a commerce_product item is unpublished by cron.
+ *
+ * This event allows modules to react before an entity is unpublished by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_UNPUBLISH = 'scheduler.commerce_product_pre_unpublish';
+
+ /**
+ * The event triggered after a commerce_product item is unpublished by cron.
+ *
+ * This event allows modules to react after an entity is unpublished by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const UNPUBLISH = 'scheduler.commerce_product_unpublish';
+
+}
diff --git a/src/Event/SchedulerEvent.php b/src/Event/SchedulerEvent.php
new file mode 100644
index 0000000..e9e7fa2
--- /dev/null
+++ b/src/Event/SchedulerEvent.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\scheduler\Event;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Wraps a scheduler event for event listeners.
+ */
+class SchedulerEvent extends EventBase {
+
+ /**
+ * Gets the entity object.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * The entity object that caused the event to fire.
+ */
+ public function getEntity() {
+ return $this->entity;
+ }
+
+ /**
+ * Sets the entity object.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object that caused the event to fire.
+ */
+ public function setEntity(EntityInterface $entity) {
+ $this->entity = $entity;
+ }
+
+ /**
+ * Gets the node object (same as the entity object).
+ *
+ * This method is retained for backwards compatibility because implementations
+ * of the event subscriber functions may be using $event->getNode().
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * The entity object that caused the event to fire.
+ */
+ public function getNode() {
+ return $this->entity;
+ }
+
+ /**
+ * Sets the node object (same as the entity object).
+ *
+ * This method is retained for backwards compatibility because implementations
+ * of the event subscriber functions may be using $event->setNode().
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object that caused the event to fire.
+ */
+ public function setNode(EntityInterface $entity) {
+ $this->entity = $entity;
+ }
+
+}
diff --git a/src/Event/SchedulerMediaEvents.php b/src/Event/SchedulerMediaEvents.php
new file mode 100644
index 0000000..3972911
--- /dev/null
+++ b/src/Event/SchedulerMediaEvents.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\scheduler\Event;
+
+/**
+ * Lists the six events dispatched by Scheduler relating to Media entities.
+ */
+final class SchedulerMediaEvents {
+
+ /**
+ * The event triggered after a media item is published immediately.
+ *
+ * This event allows modules to react after an entity is published
+ * immediately when being saved after editing. The event listener method
+ * receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PUBLISH_IMMEDIATELY = 'scheduler.media_publish_immediately';
+
+ /**
+ * The event triggered after a media item is published by cron.
+ *
+ * This event allows modules to react after an entity is published by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PUBLISH = 'scheduler.media_publish';
+
+ /**
+ * The event triggered before a media item is published immediately.
+ *
+ * This event allows modules to react before an entity is published
+ * immediately when being saved after editing. The event listener method
+ * receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_PUBLISH_IMMEDIATELY = 'scheduler.media_pre_publish_immediately';
+
+ /**
+ * The event triggered before a media item is published by cron.
+ *
+ * This event allows modules to react before an entity is published by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_PUBLISH = 'scheduler.media_pre_publish';
+
+ /**
+ * The event triggered before a media item is unpublished by cron.
+ *
+ * This event allows modules to react before an entity is unpublished by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_UNPUBLISH = 'scheduler.media_pre_unpublish';
+
+ /**
+ * The event triggered after a media item is unpublished by cron.
+ *
+ * This event allows modules to react after an entity is unpublished by Cron.
+ * The event listener receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const UNPUBLISH = 'scheduler.media_unpublish';
+
+}
diff --git a/src/Event/SchedulerNodeEvents.php b/src/Event/SchedulerNodeEvents.php
new file mode 100644
index 0000000..89eb320
--- /dev/null
+++ b/src/Event/SchedulerNodeEvents.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\scheduler\Event;
+
+/**
+ * Lists the six events dispatched by Scheduler relating to Node entities.
+ *
+ * The event names here are the original six, when only nodes were supported.
+ * See SchedulerMediaEvents for the generic naming convention to follow for any
+ * new entity plugin implementations.
+ */
+class SchedulerNodeEvents {
+
+ /**
+ * The event triggered after a node is published immediately.
+ *
+ * This event allows modules to react after a node is published immediately.
+ * The event listener method receives a \Drupal\Core\Entity\EntityInterface
+ * instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PUBLISH_IMMEDIATELY = 'scheduler.publish_immediately';
+
+ /**
+ * The event triggered after a node is published via cron.
+ *
+ * This event allows modules to react after a node is published. The event
+ * listener method receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PUBLISH = 'scheduler.publish';
+
+ /**
+ * The event triggered before a node is published immediately.
+ *
+ * This event allows modules to react before a node is published immediately.
+ * The event listener method receives a \Drupal\Core\Entity\EntityInterface
+ * instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_PUBLISH_IMMEDIATELY = 'scheduler.pre_publish_immediately';
+
+ /**
+ * The event triggered before a node is published via cron.
+ *
+ * This event allows modules to react before a node is published. The event
+ * listener method receives a \Drupal\Core\Entity\EntityInterface
+ * instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_PUBLISH = 'scheduler.pre_publish';
+
+ /**
+ * The event triggered before a node is unpublished via cron.
+ *
+ * This event allows modules to react before a node is unpublished. The
+ * event listener method receives a \Drupal\Core\Entity\EntityInterface
+ * instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const PRE_UNPUBLISH = 'scheduler.pre_unpublish';
+
+ /**
+ * The event triggered after a node is unpublished via cron.
+ *
+ * This event allows modules to react after a node is unpublished. The event
+ * listener method receives a \Drupal\Core\Entity\EntityInterface instance.
+ *
+ * @Event
+ *
+ * @see \Drupal\scheduler\Event\SchedulerEvent
+ *
+ * @var string
+ */
+ const UNPUBLISH = 'scheduler.unpublish';
+
+}
diff --git a/src/EventBase.php b/src/EventBase.php
deleted file mode 100644
index f5193ed..0000000
--- a/src/EventBase.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-namespace Drupal\scheduler;
-
-// Drupal\Component\EventDispatcher\Event was introduced in Drupal core 9.1 to
-// assist with deprecations and the transition to Symfony 5.
-// @todo Remove this when core 9.1 is the lowest supported version.
-// @see https://www.drupal.org/project/scheduler/issues/3166688
-if (!class_exists('Drupal\Component\EventDispatcher\Event')) {
- class_alias('Symfony\Component\EventDispatcher\Event', 'Drupal\Component\EventDispatcher\Event');
-}
-
-use Drupal\Component\EventDispatcher\Event;
-use Drupal\node\NodeInterface;
-
-/**
- * Base class on which all Scheduler events are extended.
- */
-class EventBase extends Event {
-
- /**
- * The node which is being processed.
- *
- * @var \Drupal\node\NodeInterface
- */
- public $node;
-
- /**
- * Constructs the object.
- *
- * @param \Drupal\node\NodeInterface $node
- * The node which is being processed.
- */
- public function __construct(NodeInterface $node) {
- $this->node = $node;
- }
-
-}
diff --git a/src/Exception/SchedulerEntityTypeNotEnabledException.php b/src/Exception/SchedulerEntityTypeNotEnabledException.php
new file mode 100644
index 0000000..7f79314
--- /dev/null
+++ b/src/Exception/SchedulerEntityTypeNotEnabledException.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Drupal\scheduler\Exception;
+
+/**
+ * Defines an exception when the entity type is not enabled for Scheduler.
+ *
+ * This exception is thrown when Scheduler attempts to publish or unpublish an
+ * entity during cron but the entity type/bundle is not enabled for Scheduler.
+ *
+ * @see \Drupal\scheduler\SchedulerManager::publish()
+ * @see \Drupal\scheduler\SchedulerManager::unpublish()
+ */
+class SchedulerEntityTypeNotEnabledException extends \Exception {}
diff --git a/src/Exception/SchedulerMissingDateException.php b/src/Exception/SchedulerMissingDateException.php
deleted file mode 100644
index eda12bc..0000000
--- a/src/Exception/SchedulerMissingDateException.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Drupal\scheduler\Exception;
-
-/**
- * Defines an exception when the scheduled date is missing.
- *
- * This exception is thrown when Scheduler attempts to publish or unpublish a
- * node during cron but the date is missing.
- *
- * @see \Drupal\scheduler\SchedulerManager::publish()
- * @see \Drupal\scheduler\SchedulerManager::unpublish()
- */
-class SchedulerMissingDateException extends \Exception {}
diff --git a/src/Exception/SchedulerNodeTypeNotEnabledException.php b/src/Exception/SchedulerNodeTypeNotEnabledException.php
deleted file mode 100644
index f26b4a2..0000000
--- a/src/Exception/SchedulerNodeTypeNotEnabledException.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Drupal\scheduler\Exception;
-
-/**
- * Defines an exception when the node type is not enabled for Scheduler.
- *
- * This exception is thrown when Scheduler attempts to publish or unpublish a
- * node during cron but the node type is not enabled for Scheduler.
- *
- * @see \Drupal\scheduler\SchedulerManager::publish()
- * @see \Drupal\scheduler\SchedulerManager::unpublish()
- */
-class SchedulerNodeTypeNotEnabledException extends \Exception {}
diff --git a/src/Form/SchedulerAdminForm.php b/src/Form/SchedulerAdminForm.php
index 1aacd57..df1b206 100644
--- a/src/Form/SchedulerAdminForm.php
+++ b/src/Form/SchedulerAdminForm.php
@@ -5,6 +5,7 @@ namespace Drupal\scheduler\Form;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -19,12 +20,28 @@ class SchedulerAdminForm extends ConfigFormBase {
*/
protected $dateFormatter;
+ /**
+ * The scheduler manager service.
+ *
+ * @var \Drupal\scheduler\SchedulerManager
+ */
+ protected $schedulerManager;
+
+ /**
+ * Entity Type Manager service object.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
$instance->setDateFormatter($container->get('date.formatter'));
+ $instance->schedulerManager = $container->get('scheduler.manager');
+ $instance->entityTypeManager = $container->get('entity_type.manager');
return $instance;
}
@@ -56,6 +73,70 @@ class SchedulerAdminForm extends ConfigFormBase {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['description'] = [
+ '#markup' => '<p>' . $this->t('Most of the Scheduler options are set independently for each entity type and bundle. These can be accessed from the <a href="@link">admin structure</a> page or directly by using the drop-button', ['@link' => Url::fromRoute('system.admin_structure')->toString()]) . '</p>',
+ ];
+
+ // Build a drop-button with links to configure all supported entity types.
+ $plugins = $this->schedulerManager->getPlugins();
+ $links = [];
+ $links[] = [
+ 'title' => $this->t('Entity Types'),
+ 'url' => Url::fromRoute('system.admin_structure'),
+ ];
+ foreach ($plugins as $entityTypeId => $plugin) {
+ $publishing_enabled_types = $this->schedulerManager->getEnabledTypes($entityTypeId, 'publish');
+ $unpublishing_enabled_types = $this->schedulerManager->getEnabledTypes($entityTypeId, 'unpublish');
+
+ // When all is running normally, $plugin->getTypes() will give a non-empty
+ // array of values, but we need to protect against this being empty.
+ if (!$types = $plugin->getTypes()) {
+ // When a module is enabled via drush there is no automatic clear cache.
+ // Thus moduleHandler()->moduleExists({module}) can return false when
+ // the module is actually enabled. This means we get nothing for
+ // plugin->getTypes() and processing should stop with a useful exception
+ // message, instead of letting Core give a confusing exception.
+ $type_definition = $this->entityTypeManager->getDefinition($entityTypeId . '_type', FALSE);
+ if (!$type_definition) {
+ throw new \Exception(sprintf('Invalid or empty entity type definition for %s module. Do a full cache clear via admin/config/development/performance or drush cr.', $plugin->dependency()));
+ }
+ // Some modules may not create a default entity type during installation
+ // or the entity type definitions may have been deleted. This is not an
+ // exception, but will cause an error if we do not stop this loop.
+ $message_parms = [
+ '%module' => $plugin->dependency(),
+ '%plugin_label' => $plugin->label(),
+ ];
+ $this->logger('scheduler')->warning('No entity types returned by %module module for use in %plugin_label', $message_parms);
+ $this->messenger()->addWarning($this->t('No entity types returned by %module module for use in %plugin_label', $message_parms));
+ continue;
+ }
+ $bundle_id = reset($types)->bundle();
+ $collection_label = $this->entityTypeManager->getStorage($bundle_id)->getEntityType()->get('label_collection');
+ // Cater for incomplete config entity with no collection label.
+ $collection_label = is_object($collection_label) ? $collection_label->__toString() : $collection_label;
+ $links[] = ['title' => "-- $collection_label --"];
+ foreach ($types as $id => $type) {
+ $text = [];
+ in_array($id, $publishing_enabled_types) ? $text[] = $this->t('publishing') : NULL;
+ in_array($id, $unpublishing_enabled_types) ? $text[] = $this->t('unpublishing') : NULL;
+ $links[] = [
+ 'title' => $type->label() . (!empty($text) ? ' (' . implode(', ', $text) . ')' : ''),
+ // Example: the route 'entity.media_type.edit_form' with parameter
+ // media_type={typeid} has url /admin/structure/media/manage/{typeid}.
+ 'url' => Url::fromRoute("entity.$bundle_id.edit_form", [$bundle_id => $type->id()]),
+ ];
+ }
+ }
+ $form['entity_type_links'] = [
+ '#type' => 'dropbutton',
+ '#links' => $links,
+ ];
+
+ $form['description2'] = [
+ '#markup' => '<p>' . $this->t('The settings below are common to all entity types.') . '</p>',
+ ];
+
// Options for setting date-only with default time.
$form['date_only_fieldset'] = [
'#type' => 'fieldset',
@@ -95,6 +176,9 @@ class SchedulerAdminForm extends ConfigFormBase {
'#description' => $this->t('When entering a time, only show hours and minutes in the input field.'),
];
+ // Attach library for admin css file.
+ $form['#attached']['library'][] = 'scheduler/admin';
+
return parent::buildForm($form, $form_state);
}
diff --git a/src/Plugin/Field/FieldWidget/TimestampDatetimeNoDefaultWidget.php b/src/Plugin/Field/FieldWidget/TimestampDatetimeNoDefaultWidget.php
index 2bf4e73..818f096 100644
--- a/src/Plugin/Field/FieldWidget/TimestampDatetimeNoDefaultWidget.php
+++ b/src/Plugin/Field/FieldWidget/TimestampDatetimeNoDefaultWidget.php
@@ -29,7 +29,7 @@ class TimestampDatetimeNoDefaultWidget extends TimestampDatetimeWidget {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
// The default description "Format: html-date html-time. Leave blank to use
// the time of form submission" is inherited from TimestampDatetimeWidget,
- // but this is entirely replaced in scheduler_form_node_form_alter().
+ // but this is entirely replaced in _scheduler_entity_form_alter().
// However this widget is generic and may be used elsewhere, so provide
// an accurate #description here.
$element['value']['#description'] = $this->t('Leave blank for no date.');
diff --git a/src/Plugin/Scheduler/CommerceProductScheduler.php b/src/Plugin/Scheduler/CommerceProductScheduler.php
new file mode 100644
index 0000000..049ff71
--- /dev/null
+++ b/src/Plugin/Scheduler/CommerceProductScheduler.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\scheduler\Plugin\Scheduler;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\scheduler\SchedulerPluginBase;
+
+/**
+ * Plugin for Commerce Product entity type.
+ *
+ * @package Drupal\Scheduler\Plugin\Scheduler
+ *
+ * @SchedulerPlugin(
+ * id = "commerce_product_scheduler",
+ * label = @Translation("Commerce Product Scheduler Plugin"),
+ * description = @Translation("Provides support for scheduling Commerce Product entities"),
+ * entityType = "commerce_product",
+ * typeFieldName = "type",
+ * dependency = "commerce_product",
+ * schedulerEventClass = "\Drupal\scheduler\Event\SchedulerCommerceProductEvents",
+ * publishAction = "commerce_publish_product",
+ * unpublishAction = "commerce_unpublish_product"
+ * )
+ */
+class CommerceProductScheduler extends SchedulerPluginBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * Get the available types/bundles for the entity type.
+ *
+ * Do not use static or drupal_static here, because changes to third-party
+ * settings invalidate the saved values during phpunit testing.
+ *
+ * @return array
+ * The commerce product bundle objects, keyed by bundle name, or an empty
+ * array if Commerce is not enabled.
+ */
+ public function getTypes() {
+ if (!\Drupal::moduleHandler()->moduleExists('commerce')) {
+ return [];
+ }
+ $productTypes = \Drupal::entityTypeManager()->getStorage('commerce_product_type')->loadMultiple();
+ return $productTypes;
+ }
+
+ /**
+ * Get the form IDs for commerce product add/edit forms.
+ *
+ * @return array
+ * The list of form IDs, or an empty array if Commerce is not enabled.
+ */
+ public function entityFormIds() {
+ if (!\Drupal::moduleHandler()->moduleExists('commerce')) {
+ return [];
+ }
+ static $ids;
+ if (!isset($ids)) {
+ $ids = [];
+ $types = array_keys($this->getTypes());
+ foreach ($types as $typeId) {
+ $ids[] = 'commerce_product_' . $typeId . '_add_form';
+ $ids[] = 'commerce_product_' . $typeId . '_edit_form';
+ }
+ }
+ return $ids;
+ }
+
+ /**
+ * Get the form IDs for commerce product type forms.
+ *
+ * @return array
+ * The list of form IDs, or an empty array if Commerce is not enabled.
+ */
+ public function entityTypeFormIds() {
+ if (!\Drupal::moduleHandler()->moduleExists('commerce')) {
+ return [];
+ }
+ return [
+ 'commerce_product_type_add_form',
+ 'commerce_product_type_edit_form',
+ ];
+ }
+
+}
diff --git a/src/Plugin/Scheduler/MediaScheduler.php b/src/Plugin/Scheduler/MediaScheduler.php
new file mode 100644
index 0000000..236ed31
--- /dev/null
+++ b/src/Plugin/Scheduler/MediaScheduler.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\scheduler\Plugin\Scheduler;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\scheduler\SchedulerPluginBase;
+
+/**
+ * Plugin for Media entity type.
+ *
+ * @package Drupal\Scheduler\Plugin\Scheduler
+ *
+ * @SchedulerPlugin(
+ * id = "media_scheduler",
+ * label = @Translation("Media Scheduler Plugin"),
+ * description = @Translation("Provides support for scheduling media entities"),
+ * entityType = "media",
+ * typeFieldName = "bundle",
+ * dependency = "media",
+ * develGenerateForm = "devel_generate_form_media",
+ * userViewRoute = "view.scheduler_scheduled_media.user_page",
+ * )
+ */
+class MediaScheduler extends SchedulerPluginBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * Get the available types/bundles for the entity type.
+ *
+ * Do not use static or drupal_static here, because changes to third-party
+ * settings invalidate the saved values during phpunit testing.
+ *
+ * @return array
+ * The media bundle objects, keyed by bundle name, or an empty array if
+ * Media is not enabled.
+ */
+ public function getTypes() {
+ if (!\Drupal::moduleHandler()->moduleExists('media')) {
+ return [];
+ }
+ $mediaTypes = \Drupal::entityTypeManager()->getStorage('media_type')->loadMultiple();
+ return $mediaTypes;
+ }
+
+ /**
+ * Get the form IDs for media add/edit forms.
+ *
+ * @return array
+ * The list of form IDs, or an empty array if Media is not enabled.
+ */
+ public function entityFormIds() {
+ if (!\Drupal::moduleHandler()->moduleExists('media')) {
+ return [];
+ }
+ static $ids;
+ if (!isset($ids)) {
+ $ids = [];
+ $types = array_keys($this->getTypes());
+ foreach ($types as $typeId) {
+ $ids[] = 'media_' . $typeId . '_add_form';
+ $ids[] = 'media_' . $typeId . '_edit_form';
+ }
+ }
+ return $ids;
+ }
+
+ /**
+ * Get the form IDs for media type forms.
+ *
+ * @return array
+ * The list of form IDs, or an empty array if Media is not enabled.
+ */
+ public function entityTypeFormIds() {
+ if (!\Drupal::moduleHandler()->moduleExists('media')) {
+ return [];
+ }
+ return [
+ 'media_type_add_form',
+ 'media_type_edit_form',
+ ];
+ }
+
+}
diff --git a/src/Plugin/Scheduler/NodeScheduler.php b/src/Plugin/Scheduler/NodeScheduler.php
new file mode 100644
index 0000000..e344051
--- /dev/null
+++ b/src/Plugin/Scheduler/NodeScheduler.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Drupal\scheduler\Plugin\Scheduler;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\scheduler\SchedulerPluginBase;
+
+/**
+ * Plugin for Node entity type.
+ *
+ * @package Drupal\Scheduler\Plugin\Scheduler
+ *
+ * @SchedulerPlugin(
+ * id = "node_scheduler",
+ * label = @Translation("Node Scheduler Plugin"),
+ * description = @Translation("Provides support for scheduling node entities"),
+ * entityType = "node",
+ * typeFieldName = "type",
+ * dependency = "node",
+ * develGenerateForm = "devel_generate_form_content",
+ * userViewRoute = "view.scheduler_scheduled_content.user_page",
+ * )
+ */
+class NodeScheduler extends SchedulerPluginBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * Get the available types/bundles for the entity type.
+ *
+ * Do not use static or drupal_static here, because changes to third-party
+ * settings invalidate the saved values during phpunit testing.
+ *
+ * @return array
+ * The node type objects, keyed by node type name.
+ */
+ public function getTypes() {
+ $nodeTypes = \Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple();
+ return $nodeTypes;
+ }
+
+ /**
+ * Get the form IDs for node add/edit forms.
+ *
+ * @return array
+ * The list of form IDs.
+ */
+ public function entityFormIds() {
+ static $ids;
+ if (!isset($ids)) {
+ $ids = [];
+ $types = array_keys($this->getTypes());
+ foreach ($types as $typeId) {
+ // The node add form is named node_{type}_form. This is different from
+ // other entities, which have {entity}_{type}_add_form.
+ $ids[] = 'node_' . $typeId . '_form';
+ $ids[] = 'node_' . $typeId . '_edit_form';
+ }
+ }
+ return $ids;
+ }
+
+ /**
+ * Get the form IDs for node type forms.
+ *
+ * @return array
+ * The list of form IDs.
+ */
+ public function entityTypeFormIds() {
+ return [
+ 'node_type_add_form',
+ 'node_type_edit_form',
+ ];
+ }
+
+}
diff --git a/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraint.php b/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraint.php
index a7eafe7..36aaab8 100644
--- a/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraint.php
+++ b/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraint.php
@@ -10,13 +10,13 @@ use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
* @Constraint(
* id = "SchedulerPublishOn",
* label = @Translation("Scheduler publish on", context = "Validation"),
- * type = "entity:node"
+ * type = "entity"
* )
*/
class SchedulerPublishOnConstraint extends CompositeConstraintBase {
/**
- * Message shown when publish_on is not the future.
+ * Message shown when publish_on is not in the future.
*
* @var string
*/
diff --git a/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraintValidator.php b/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraintValidator.php
index b4963e1..9df8df9 100644
--- a/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraintValidator.php
+++ b/src/Plugin/Validation/Constraint/SchedulerPublishOnConstraintValidator.php
@@ -16,7 +16,7 @@ class SchedulerPublishOnConstraintValidator extends ConstraintValidator {
public function validate($entity, Constraint $constraint) {
$publish_on = $entity->value;
$default_publish_past_date = \Drupal::config('scheduler.settings')->get('default_publish_past_date');
- $scheduler_publish_past_date = $entity->getEntity()->type->entity->getThirdPartySetting('scheduler', 'publish_past_date', $default_publish_past_date);
+ $scheduler_publish_past_date = \Drupal::service('scheduler.manager')->getThirdPartySetting($entity->getEntity(), 'publish_past_date', $default_publish_past_date);
if ($publish_on && $scheduler_publish_past_date == 'error' && $publish_on < \Drupal::time()->getRequestTime()) {
$this->context->buildViolation($constraint->messagePublishOnDateNotInFuture)
diff --git a/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraint.php b/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraint.php
index 8523034..ff50a4d 100644
--- a/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraint.php
+++ b/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraint.php
@@ -10,7 +10,7 @@ use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
* @Constraint(
* id = "SchedulerUnpublishOn",
* label = @Translation("Scheduler unpublish on", context = "Validation"),
- * type = "entity:node"
+ * type = "entity"
* )
*/
class SchedulerUnpublishOnConstraint extends CompositeConstraintBase {
diff --git a/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraintValidator.php b/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraintValidator.php
index e207848..c53545f 100644
--- a/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraintValidator.php
+++ b/src/Plugin/Validation/Constraint/SchedulerUnpublishOnConstraintValidator.php
@@ -16,12 +16,12 @@ class SchedulerUnpublishOnConstraintValidator extends ConstraintValidator {
public function validate($entity, Constraint $constraint) {
// If the content type is not enabled for unpublishing then exit early.
- if (!$entity->getEntity()->type->entity->getThirdPartySetting('scheduler', 'unpublish_enable', FALSE)) {
+ if (!\Drupal::service('scheduler.manager')->getThirdPartySetting($entity->getEntity(), 'unpublish_enable', FALSE)) {
return;
}
$default_unpublish_required = \Drupal::config('scheduler.settings')->get('default_unpublish_required');
- $scheduler_unpublish_required = $entity->getEntity()->type->entity->getThirdPartySetting('scheduler', 'unpublish_required', $default_unpublish_required);
+ $scheduler_unpublish_required = \Drupal::service('scheduler.manager')->getThirdPartySetting($entity->getEntity(), 'unpublish_required', $default_unpublish_required);
$publish_on = $entity->getEntity()->publish_on->value;
$unpublish_on = $entity->value;
$status = $entity->getEntity()->status->value;
diff --git a/src/Plugin/views/access/Scheduler.php b/src/Plugin/views/access/Scheduler.php
index 4255a7f..9f371c0 100644
--- a/src/Plugin/views/access/Scheduler.php
+++ b/src/Plugin/views/access/Scheduler.php
@@ -2,58 +2,37 @@
namespace Drupal\scheduler\Plugin\views\access;
-use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\views\Plugin\views\access\AccessPluginBase;
use Symfony\Component\Routing\Route;
/**
- * Access plugin that provides access control for Scheduler.
+ * Access plugin that provided access control for Scheduler views.
*
- * @ingroup views_access_plugins
+ * This access plugin has been replaced by SchedulerRouteAccess, and is no
+ * longer needed. However it has to remain (temporarily) as it is used in the
+ * existing view. Deleting this class causes errors before the view can be
+ * updated via update.php. The content below has been reduced to the minimum
+ * necessary to avoid errors before update.php is run.
*
* @ViewsAccess(
* id = "scheduler",
- * title = @Translation("Scheduled content access"),
- * help = @Translation("All Scheduler users can see their own scheduled content via their user page. In addition, if they have 'view scheduled content' permission they will be able to see all scheduled content by all authors."),
+ * title = @Translation("Scheduled content access. REDUNDANT, DO NOT USE THIS."),
+ * help = @Translation("NOT USED"),
* )
*/
-class Scheduler extends AccessPluginBase implements CacheableDependencyInterface {
+class Scheduler extends AccessPluginBase {
/**
* {@inheritdoc}
*/
public function access(AccountInterface $account) {
- return \Drupal::service('access_checker.scheduler_content')->access($account);
}
/**
* {@inheritdoc}
*/
public function alterRouteDefinition(Route $route) {
- $route->setRequirement('_access_scheduler_content', 'TRUE');
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCacheContexts() {
- return ['user'];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCacheTags() {
- return [];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCacheMaxAge() {
- return Cache::PERMANENT;
}
}
diff --git a/src/Routing/SchedulerRouteSubscriber.php b/src/Routing/SchedulerRouteSubscriber.php
new file mode 100644
index 0000000..94f084e
--- /dev/null
+++ b/src/Routing/SchedulerRouteSubscriber.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\scheduler\Routing;
+
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Scheduler route subscriber to add custom access for user views.
+ */
+class SchedulerRouteSubscriber extends RouteSubscriberBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function alterRoutes(RouteCollection $collection) {
+ $user_page_routes = \Drupal::service('scheduler.manager')->getUserPageViewRoutes();
+ foreach ($user_page_routes as $user_route) {
+ if ($route = $collection->get($user_route)) {
+ $requirements = $route->getRequirements();
+ $requirements['_custom_access'] = '\Drupal\scheduler\Access\SchedulerRouteAccess::access';
+ $route->setRequirements($requirements);
+ }
+ }
+ }
+
+}
diff --git a/src/SchedulerEvent.php b/src/SchedulerEvent.php
index cda8c6f..7faa2c4 100644
--- a/src/SchedulerEvent.php
+++ b/src/SchedulerEvent.php
@@ -1,32 +1,24 @@
<?php
-namespace Drupal\scheduler;
-
-use Drupal\node\NodeInterface;
-
/**
- * Wraps a scheduler event for event listeners.
+ * @file
+ * Class alias for Drupal\scheduler\SchedulerEvent.
*/
-class SchedulerEvent extends EventBase {
-
- /**
- * Gets node object.
- *
- * @return \Drupal\node\NodeInterface
- * The node object that caused the event to fire.
- */
- public function getNode() {
- return $this->node;
- }
- /**
- * Sets the node object.
- *
- * @param \Drupal\node\NodeInterface $node
- * The node object that caused the event to fire.
- */
- public function setNode(NodeInterface $node) {
- $this->node = $node;
- }
+/**
+ * Create event class alias to maintain backwards-compatibility.
+ *
+ * The original event classes, named Drupal\scheduler\SchedulerEvent and
+ * Drupal\scheduler\SchedulerEvents must remain for backwards-compatibility
+ * with existing implementations of event subscribers for Node events. The
+ * namespace should have been Drupal\scheduler\Event and all the event-related
+ * files stored in a src/Event folder, but instead they were just in /src.
+ *
+ * Now that Scheduler supports non-node entities and each type has to have its
+ * own specific event class named 'Scheduler{Type}Events', they can be moved
+ * into a Drupal\scheduler\Event namespace, with all event files being stored in
+ * a src/Event folder. These two aliases, for the original node events, ensure
+ * that any existing event subscribers will continue work unchnaged.
+ */
-}
+class_alias('Drupal\scheduler\Event\SchedulerEvent', 'Drupal\scheduler\SchedulerEvent');
diff --git a/src/SchedulerEvents.php b/src/SchedulerEvents.php
index dc1d895..4a3002a 100644
--- a/src/SchedulerEvents.php
+++ b/src/SchedulerEvents.php
@@ -1,103 +1,24 @@
<?php
-namespace Drupal\scheduler;
+/**
+ * @file
+ * Class alias for Drupal\scheduler\SchedulerEvents.
+ */
/**
- * Contains all events dispatched by Scheduler.
+ * Create event class alias to maintain backwards-compatibility.
+ *
+ * The original event classes, named Drupal\scheduler\SchedulerEvent and
+ * Drupal\scheduler\SchedulerEvents must remain for backwards-compatibility
+ * with existing implementations of event subscribers for Node events. The
+ * namespace should have been Drupal\scheduler\Event and all the event-related
+ * files stored in a src/Event folder, but instead they were just in /src.
*
- * Ideally the namespace should have been Drupal\scheduler\Event and all the
- * event-related files stored in a src/Event folder. This cannot be chnaged now
- * as it would break the API which is being used by 3rd-party modules
- * subscribing to scheduler's events.
+ * Now that Scheduler supports non-node entities and each type has to have its
+ * own specific event class named 'Scheduler{Type}Events', they can be moved
+ * into a Drupal\scheduler\Event namespace, with all event files being stored in
+ * a src/Event folder. These two aliases, for the original node events, ensure
+ * that any existing event subscribers will continue work unchnaged.
*/
-final class SchedulerEvents {
-
- /**
- * The event triggered after a node is published immediately.
- *
- * This event allows modules to react after a node is published immediately.
- * The event listener method receives a \Drupal\Core\Entity\EntityInterface
- * instance.
- *
- * @Event
- *
- * @see \Drupal\scheduler\SchedulerEvent
- *
- * @var string
- */
- const PUBLISH_IMMEDIATELY = 'scheduler.publish_immediately';
-
- /**
- * The event triggered after a node is published via cron.
- *
- * This event allows modules to react after a node is published. The event
- * listener method receives a \Drupal\Core\Entity\EntityInterface instance.
- *
- * @Event
- *
- * @see \Drupal\scheduler\SchedulerEvent
- *
- * @var string
- */
- const PUBLISH = 'scheduler.publish';
-
- /**
- * The event triggered before a node is published immediately.
- *
- * This event allows modules to react before a node is published immediately.
- * The event listener method receives a \Drupal\Core\Entity\EntityInterface
- * instance.
- *
- * @Event
- *
- * @see \Drupal\scheduler\SchedulerEvent
- *
- * @var string
- */
- const PRE_PUBLISH_IMMEDIATELY = 'scheduler.pre_publish_immediately';
-
- /**
- * The event triggered before a node is published via cron.
- *
- * This event allows modules to react before a node is published. The event
- * listener method receives a \Drupal\Core\Entity\EntityInterface
- * instance.
- *
- * @Event
- *
- * @see \Drupal\scheduler\SchedulerEvent
- *
- * @var string
- */
- const PRE_PUBLISH = 'scheduler.pre_publish';
-
- /**
- * The event triggered before a node is unpublished via cron.
- *
- * This event allows modules to react before a node is unpublished. The
- * event listener method receives a \Drupal\Core\Entity\EntityInterface
- * instance.
- *
- * @Event
- *
- * @see \Drupal\scheduler\SchedulerEvent
- *
- * @var string
- */
- const PRE_UNPUBLISH = 'scheduler.pre_unpublish';
-
- /**
- * The event triggered after a node is unpublished via cron.
- *
- * This event allows modules to react after a node is unpublished. The event
- * listener method receives a \Drupal\Core\Entity\EntityInterface instance.
- *
- * @Event
- *
- * @see \Drupal\scheduler\SchedulerEvent
- *
- * @var string
- */
- const UNPUBLISH = 'scheduler.unpublish';
-}
+class_alias('Drupal\scheduler\Event\SchedulerNodeEvents', 'Drupal\scheduler\SchedulerEvents');
diff --git a/src/SchedulerManager.php b/src/SchedulerManager.php
index fbb7735..5498505 100644
--- a/src/SchedulerManager.php
+++ b/src/SchedulerManager.php
@@ -5,16 +5,17 @@ namespace Drupal\scheduler;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\Component\EventDispatcher\Event;
+use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\FileStorage;
use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Link;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
-use Drupal\node\NodeInterface;
-use Drupal\scheduler\Exception\SchedulerMissingDateException;
-use Drupal\scheduler\Exception\SchedulerNodeTypeNotEnabledException;
use Psr\Log\LoggerInterface;
/**
@@ -73,10 +74,33 @@ class SchedulerManager {
*/
protected $time;
+ /**
+ * Entity Field Manager service object.
+ *
+ * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+ */
+ private $entityFieldManager;
+
+ /**
+ * Scheduler Plugin Manager service object.
+ *
+ * @var SchedulerPluginManager
+ */
+ private $pluginManager;
+
/**
* Constructs a SchedulerManager object.
*/
- public function __construct(DateFormatterInterface $dateFormatter, LoggerInterface $logger, ModuleHandlerInterface $moduleHandler, EntityTypeManagerInterface $entityTypeManager, ConfigFactoryInterface $configFactory, ContainerAwareEventDispatcher $eventDispatcher, TimeInterface $time) {
+ public function __construct(DateFormatterInterface $dateFormatter,
+ LoggerInterface $logger,
+ ModuleHandlerInterface $moduleHandler,
+ EntityTypeManagerInterface $entityTypeManager,
+ ConfigFactoryInterface $configFactory,
+ ContainerAwareEventDispatcher $eventDispatcher,
+ TimeInterface $time,
+ EntityFieldManagerInterface $entityFieldManager,
+ SchedulerPluginManager $pluginManager
+ ) {
$this->dateFormatter = $dateFormatter;
$this->logger = $logger;
$this->moduleHandler = $moduleHandler;
@@ -84,6 +108,8 @@ class SchedulerManager {
$this->configFactory = $configFactory;
$this->eventDispatcher = $eventDispatcher;
$this->time = $time;
+ $this->entityFieldManager = $entityFieldManager;
+ $this->pluginManager = $pluginManager;
}
/**
@@ -120,181 +146,268 @@ class SchedulerManager {
}
/**
- * Publish scheduled nodes.
+ * Dispatches a Scheduler event for an entity.
*
- * @return bool
- * TRUE if any node has been published, FALSE otherwise.
+ * This function dispatches a Scheduler event, identified by $event_id, for
+ * the entity type of the provided $entity. Each entity type has its own
+ * events class Scheduler{EntityType}Events, for example SchedulerNodeEvents,
+ * SchedulerMediaEvents, etc. This class contains constants (with names
+ * matching the $event_id parameter) which uniquely define the final event
+ * name string to be dispatched. The actual event object dispatched is always
+ * of class SchedulerEvent.
+ *
+ * The $entity is passed by reference so that any changes made in the event
+ * subscriber implementations are automatically stored and passed forward.
*
- * @throws \Drupal\scheduler\Exception\SchedulerMissingDateException
- * @throws \Drupal\scheduler\Exception\SchedulerNodeTypeNotEnabledException
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity object.
+ * @param string $event_id
+ * The short text id the event, for example 'PUBLISH' or 'PRE_UNPUBLISH'.
*/
- public function publish() {
- $result = FALSE;
- $action = 'publish';
-
- // Select all nodes of the types that are enabled for scheduled publishing
- // and where publish_on is less than or equal to the current time.
- $nids = [];
- $scheduler_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types($action));
- if (!empty($scheduler_enabled_types)) {
- $query = $this->entityTypeManager->getStorage('node')->getQuery()
- ->exists('publish_on')
- ->condition('publish_on', $this->time->getRequestTime(), '<=')
- ->condition('type', $scheduler_enabled_types, 'IN')
- ->latestRevision()
- ->sort('publish_on')
- ->sort('nid');
- // Disable access checks for this query.
- // @see https://www.drupal.org/node/2700209
- $query->accessCheck(FALSE);
- $nids = $query->execute();
- }
-
- // Allow other modules to add to the list of nodes to be published.
- $nids = array_unique(array_merge($nids, $this->nidList($action)));
-
- // Allow other modules to alter the list of nodes to be published.
- $this->moduleHandler->alter('scheduler_nid_list', $nids, $action);
-
- // In 8.x the entity translations are all associated with one node id
- // unlike 7.x where each translation was a separate node. This means that
- // the list of node ids returned above may have some translations that need
- // processing now and others that do not.
- /** @var \Drupal\node\NodeInterface[] $nodes */
- $nodes = $this->loadNodes($nids);
- foreach ($nodes as $node_multilingual) {
-
- // The API calls could return nodes of types which are not enabled for
- // scheduled publishing, so do not process these. This check can be done
- // once, here, as the setting will be the same for all translations.
- if (!$node_multilingual->type->entity->getThirdPartySetting('scheduler', 'publish_enable', $this->setting('default_publish_enable'))) {
- throw new SchedulerNodeTypeNotEnabledException(sprintf("Node %d '%s' will not be published because node type '%s' is not enabled for scheduled publishing", $node_multilingual->id(), $node_multilingual->getTitle(), node_get_type_label($node_multilingual)));
- }
-
- $languages = $node_multilingual->getTranslationLanguages();
- foreach ($languages as $language) {
- // The object returned by getTranslation() behaves the same as a $node.
- $node = $node_multilingual->getTranslation($language->getId());
-
- // If the current translation does not have a publish on value, or it is
- // later than the date we are processing then move on to the next.
- $publish_on = $node->publish_on->value;
- if (empty($publish_on) || $publish_on > $this->time->getRequestTime()) {
- continue;
- }
+ public function dispatchSchedulerEvent(EntityInterface &$entity, string $event_id) {
+ // Get the fully named-spaced event class name for the entity type, for use
+ // in the constant() function.
+ $event_class = $this->getPlugin($entity->getEntityTypeId())->schedulerEventClass();
+ $event_name = constant("$event_class::$event_id");
+
+ // Create the event object and dispatch the required event_name.
+ $event = new SchedulerEvent($entity);
+ $this->dispatch($event, $event_name);
+ // Get the entity, as it may have been modified by an event subscriber.
+ $entity = $event->getEntity();
+ }
- // Check that other modules allow the action on this node.
- if (!$this->isAllowed($node, $action)) {
- continue;
- }
+ /**
+ * Handles throwing exceptions.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ * The entity causing the exepction.
+ * @param string $exception_name
+ * Which exception to throw.
+ * @param string $process
+ * The process being performed (publish|unpublish).
+ *
+ * @throws \Drupal\scheduler\Exception\SchedulerEntityTypeNotEnabledException
+ */
+ private function throwSchedulerException(EntityInterface $entity, $exception_name, $process) {
+ $plugin = $this->getPlugin($entity->getEntityTypeId());
+
+ // Exception messages are developer-facing and do not need to be translated
+ // from English. So it is accpetable to create words such as "{$process}ed"
+ // and "{$process}ing".
+ switch ($exception_name) {
+ case 'SchedulerEntityTypeNotEnabledException':
+ $message = "'%s' (id %d) was not %s because %s %s '%s' is not enabled for scheduled %s. One of the following hook functions added the id incorrectly: %s. Processing halted";
+ $p1 = $entity->label();
+ $p2 = $entity->id();
+ $p3 = "{$process}ed";
+ $p4 = $entity->getEntityTypeId();
+ $p5 = $plugin->typeFieldName();
+ $p6 = $entity->{$plugin->typeFieldName()}->entity->label();
+ $p7 = "{$process}ing";
+ // Get a list of the hook function implementations, as one of these will
+ // have caused this exception.
+ $hooks = array_merge(
+ $this->getHookImplementations('list', $entity),
+ $this->getHookImplementations('list_alter', $entity)
+ );
+ asort($hooks);
+ $p8 = implode(', ', $hooks);
+ break;
+ }
- // $node->setChangedTime($publish_on) will fail badly if an API call has
- // removed the date. Trap this as an exception here and give a
- // meaningful message.
- // @todo This will now never be thrown due to the empty(publish_on)
- // check above to cater for translations. Remove this exception?
- if (empty($node->publish_on->value)) {
- $field_definitions = $this->entityTypeManager->getFieldDefinitions('node', $node->getType());
- $field = (string) $field_definitions['publish_on']->getLabel();
- throw new SchedulerMissingDateException(sprintf("Node %d '%s' will not be published because field '%s' has no value", $node->id(), $node->getTitle(), $field));
- }
+ $class = "\\Drupal\\scheduler\\Exception\\$exception_name";
+ throw new $class(sprintf($message, $p1, $p2, $p3, $p4, $p5, $p6, $p7, $p8));
+ }
- // Trigger the PRE_PUBLISH event so that modules can react before the
- // node is published.
- $event = new SchedulerEvent($node);
- $this->dispatch($event, SchedulerEvents::PRE_PUBLISH);
- $node = $event->getNode();
-
- // Update 'changed' timestamp.
- $node->setChangedTime($publish_on);
- $old_creation_date = $node->getCreatedTime();
- $msg_extra = '';
- // If required, set the created date to match published date.
- if ($node->type->entity->getThirdPartySetting('scheduler', 'publish_touch', $this->setting('default_publish_touch')) ||
- ($node->getCreatedTime() > $publish_on && $node->type->entity->getThirdPartySetting('scheduler', 'publish_past_date_created', $this->setting('default_publish_past_date_created')))
- ) {
- $node->setCreatedTime($publish_on);
- $msg_extra = $this->t('The previous creation date was @old_creation_date, now updated to match the publishing date.', [
- '@old_creation_date' => $this->dateFormatter->format($old_creation_date, 'short'),
- ]);
+ /**
+ * Publish scheduled entities.
+ *
+ * @return bool
+ * TRUE if any entity has been published, FALSE otherwise.
+ *
+ * @throws \Drupal\scheduler\Exception\SchedulerEntityTypeNotEnabledException
+ */
+ public function publish() {
+ $result = FALSE;
+ $process = 'publish';
+ $plugins = $this->getPlugins();
+
+ foreach ($plugins as $entityTypeId => $plugin) {
+ // Select all entities of the types for this plugin that are enabled for
+ // scheduled publishing and where publish_on is less than or equal to the
+ // current time.
+ $ids = [];
+ $scheduler_enabled_types = $this->getEnabledTypes($entityTypeId, $process);
+
+ if (!empty($scheduler_enabled_types)) {
+ $query = $this->entityTypeManager->getStorage($entityTypeId)->getQuery()
+ ->exists('publish_on')
+ ->condition('publish_on', $this->time->getRequestTime(), '<=')
+ ->condition($plugin->typeFieldName(), $scheduler_enabled_types, 'IN')
+ ->sort('publish_on');
+ // Disable access checks for this query.
+ // @see https://www.drupal.org/node/2700209
+ $query->accessCheck(FALSE);
+ // If the entity type is revisionable then make sure we look for the
+ // latest revision. This is important for moderated entities.
+ if ($this->entityTypeManager->getDefinition($entityTypeId)->isRevisionable()) {
+ $query->latestRevision();
}
+ $ids = $query->execute();
+ }
- $create_publishing_revision = $node->type->entity->getThirdPartySetting('scheduler', 'publish_revision', $this->setting('default_publish_revision'));
- if ($create_publishing_revision) {
- $node->setNewRevision();
- // Use a core date format to guarantee a time is included.
- $revision_log_message = rtrim($this->t('Published by Scheduler. The scheduled publishing date was @publish_on.', [
- '@publish_on' => $this->dateFormatter->format($publish_on, 'short'),
- ]) . ' ' . $msg_extra);
- $node->setRevisionLogMessage($revision_log_message)
- ->setRevisionCreationTime($this->time->getRequestTime());
- }
- // Unset publish_on so the node will not get rescheduled by subsequent
- // calls to $node->save().
- $node->publish_on->value = NULL;
-
- // Invoke all implementations of hook_scheduler_publish_action() to
- // allow other modules to do the "publishing" process instead of
- // Scheduler.
- $hook = 'scheduler_publish_action';
- $processed = FALSE;
- $failed = FALSE;
- foreach ($this->moduleHandler->getImplementations($hook) as $module) {
- $function = $module . '_' . $hook;
- $return = $function($node);
- $processed = $processed || ($return === 1);
- $failed = $failed || ($return === -1);
- }
+ // Allow other modules to add to the list of entities to be published.
+ $hook_implementations = $this->getHookImplementations('list', $entityTypeId);
+ foreach ($hook_implementations as $function) {
+ // Cast each hook result as array, to protect from bad implementations.
+ $ids = array_merge($ids, (array) $function($process, $entityTypeId));
+ }
- // Log the fact that a scheduled publication is about to take place.
- $view_link = $node->toLink($this->t('View node'));
- $node_type = $this->entityTypeManager->getStorage('node_type')->load($node->bundle());
- $node_type_link = $node_type->toLink($this->t('@label settings', ['@label' => $node_type->label()]), 'edit-form');
- $logger_variables = [
- '@type' => $node_type->label(),
- '%title' => $node->getTitle(),
- 'link' => $node_type_link->toString() . ' ' . $view_link->toString(),
- '@hook' => 'hook_' . $hook,
- ];
-
- if ($failed) {
- // At least one hook function returned a failure or exception, so stop
- // processing this node and move on to the next one.
- $this->logger->warning('Publishing failed for %title. Calls to @hook returned a failure code.', $logger_variables);
- continue;
- }
- elseif ($processed) {
- // The node had 'publishing' processed by a module implementing the
- // hook, so no need to do anything more, apart from log this result.
- $this->logger->notice('@type: scheduled processing of %title completed by calls to @hook.', $logger_variables);
- }
- else {
- // None of the above hook calls processed the node and there were no
- // errors detected so set the node to published.
- $this->logger->notice('@type: scheduled publishing of %title.', $logger_variables);
- $node->setPublished();
- }
+ // Allow other modules to alter the list of entities to be published.
+ $hook_implementations = $this->getHookImplementations('list_alter', $entityTypeId);
+ foreach ($hook_implementations as $function) {
+ $function($ids, $process, $entityTypeId);
+ }
- // Invoke the event to tell Rules that Scheduler has published the node.
- if ($this->moduleHandler->moduleExists('scheduler_rules_integration')) {
- _scheduler_rules_integration_dispatch_cron_event($node, 'publish');
+ // Finally ensure that there are no duplicates in the list of ids.
+ $ids = array_unique($ids);
+
+ // In 8.x the entity translations are all associated with one entity id
+ // unlike 7.x where each translation was a separate id. This means that
+ // the list of ids returned above may have some translations that need
+ // processing now and others that do not.
+ /** @var \Drupal\Core\Entity\EntityInterface[] $entities */
+ $entities = $this->loadEntities($ids, $entityTypeId);
+ foreach ($entities as $entity_multilingual) {
+
+ // The API calls could return entities of types which are not enabled
+ // for scheduled publishing, so do not process these. This check can be
+ // done once as the setting will be the same for all translations.
+ if (!$this->getThirdPartySetting($entity_multilingual, 'publish_enable', $this->setting('default_publish_enable'))) {
+ $this->throwSchedulerException($entity_multilingual, 'SchedulerEntityTypeNotEnabledException', $process);
}
- // Trigger the PUBLISH event so that modules can react after the node is
- // published.
- $event = new SchedulerEvent($node);
- $this->dispatch($event, SchedulerEvents::PUBLISH);
-
- // Use the standard actions system to publish and save the node.
- $node = $event->getNode();
- $action_id = 'node_publish_action';
- if ($this->moduleHandler->moduleExists('workbench_moderation_actions')) {
- // workbench_moderation_actions module uses a custom action instead.
- $action_id = 'state_change__node__published';
+ $languages = $entity_multilingual->getTranslationLanguages();
+ foreach ($languages as $language) {
+ // The object returned by getTranslation() is a normal $entity.
+ $entity = $entity_multilingual->getTranslation($language->getId());
+
+ // If the current translation does not have a publish on value, or it
+ // is later than the date we are processing then move on to the next.
+ $publish_on = $entity->publish_on->value;
+ if (empty($publish_on) || $publish_on > $this->time->getRequestTime()) {
+ continue;
+ }
+
+ // Check that other modules allow the process on this entity.
+ if (!$this->isAllowed($entity, $process)) {
+ continue;
+ }
+
+ // Trigger the PRE_PUBLISH Scheduler event so that modules can react
+ // before the entity is published.
+ $this->dispatchSchedulerEvent($entity, 'PRE_PUBLISH');
+
+ // Update 'changed' timestamp.
+ $entity->setChangedTime($publish_on);
+ $old_creation_date = $entity->getCreatedTime();
+ $msg_extra = '';
+
+ // If required, set the created date to match published date.
+ if ($this->getThirdPartySetting($entity, 'publish_touch', $this->setting('default_publish_touch')) ||
+ ($entity->getCreatedTime() > $publish_on && $this->getThirdPartySetting($entity, 'publish_past_date_created', $this->setting('default_publish_past_date_created')))
+ ) {
+ $entity->setCreatedTime($publish_on);
+ $msg_extra = $this->t('The previous creation date was @old_creation_date, now updated to match the publishing date.', [
+ '@old_creation_date' => $this->dateFormatter->format($old_creation_date, 'short'),
+ ]);
+ }
+
+ $create_publishing_revision = $this->getThirdPartySetting($entity, 'publish_revision', $this->setting('default_publish_revision'));
+ if ($create_publishing_revision && $entity->getEntityType()->isRevisionable()) {
+ $entity->setNewRevision();
+ // Use a core date format to guarantee a time is included.
+ $revision_log_message = rtrim($this->t('Published by Scheduler. The scheduled publishing date was @publish_on.', [
+ '@publish_on' => $this->dateFormatter->format($publish_on, 'short'),
+ ]) . ' ' . $msg_extra);
+ $entity->setRevisionLogMessage($revision_log_message)
+ ->setRevisionCreationTime($this->time->getRequestTime());
+ }
+ // Unset publish_on so the entity will not get rescheduled by any
+ // interim calls to $entity->save().
+ $entity->publish_on->value = NULL;
+
+ // Invoke all implementations of hook_scheduler_publish_process() and
+ // hook_scheduler_{type}_publish_process() to allow other modules to
+ // do the "publishing" process instead of Scheduler.
+ $hook_implementations = $this->getHookImplementations('publish_process', $entity);
+ $processed = FALSE;
+ $failed = FALSE;
+ foreach ($hook_implementations as $function) {
+ $return = $function($entity);
+ $processed = $processed || ($return === 1);
+ $failed = $failed || ($return === -1);
+ }
+
+ // Log the fact that a scheduled publication is about to take place.
+ $entity_type = $this->entityTypeManager->getStorage($entityTypeId . '_type')->load($entity->bundle());
+ $view_link = $entity->toLink($this->t('View @type', [
+ '@type' => strtolower($entity_type->label()),
+ ]));
+ $entity_type_link = $entity_type->toLink($this->t('@label settings', [
+ '@label' => $entity_type->label(),
+ ]), 'edit-form');
+ $logger_variables = [
+ '@type' => $entity_type->label(),
+ '%title' => $entity->label(),
+ '@hook' => implode(', ', $hook_implementations),
+ 'link' => $view_link->toString() . ' ' . $entity_type_link->toString(),
+ ];
+
+ if ($failed) {
+ // At least one hook function returned a failure or exception, so
+ // stop processing this entity and move on to the next one.
+ $this->logger->warning('Publishing failed for %title. Calls to @hook returned a failure code.', $logger_variables);
+ // Restore the publish_on date to allow another attempt next time.
+ $entity->publish_on->value = $publish_on;
+ $entity->save();
+ continue;
+ }
+ elseif ($processed) {
+ // The entity was 'published' by a module implementing the hook, so
+ // we only need to log this result.
+ $this->logger->notice('@type: scheduled processing of %title completed by calls to @hook.', $logger_variables);
+ }
+ else {
+ // None of the above hook calls processed the entity and there were
+ // no errors detected so set the entity to published.
+ $this->logger->notice('@type: scheduled publishing of %title.', $logger_variables);
+ $entity->setPublished();
+ }
+
+ // Invoke event to tell Rules that Scheduler has published the entity.
+ if ($this->moduleHandler->moduleExists('scheduler_rules_integration')) {
+ _scheduler_rules_integration_dispatch_cron_event($entity, $process);
+ }
+
+ // Trigger the PUBLISH Scheduler event so that modules can react after
+ // the entity is published.
+ $this->dispatchSchedulerEvent($entity, 'PUBLISH');
+
+ // Use the standard actions system to publish and save the entity.
+ $action_id = $plugin->publishAction();
+ if ($this->moduleHandler->moduleExists('workbench_moderation_actions')) {
+ // workbench_moderation_actions module uses a custom action instead.
+ $action_id = 'state_change__' . $entityTypeId . '__published';
+ }
+ if (!$loaded_action = $this->entityTypeManager->getStorage('action')->load($action_id)) {
+ $this->missingAction($action_id, $process);
+ }
+ $loaded_action->getPlugin()->execute($entity);
+
+ $result = TRUE;
}
- $this->entityTypeManager->getStorage('action')->load($action_id)->getPlugin()->execute($node);
-
- $result = TRUE;
}
}
@@ -302,174 +415,188 @@ class SchedulerManager {
}
/**
- * Unpublish scheduled nodes.
+ * Unpublish scheduled entities.
*
* @return bool
- * TRUE if any node has been unpublished, FALSE otherwise.
+ * TRUE if any entity has been unpublished, FALSE otherwise.
*
- * @throws \Drupal\scheduler\Exception\SchedulerMissingDateException
- * @throws \Drupal\scheduler\Exception\SchedulerNodeTypeNotEnabledException
+ * @throws \Drupal\scheduler\Exception\SchedulerEntityTypeNotEnabledException
*/
public function unpublish() {
$result = FALSE;
- $action = 'unpublish';
-
- // Select all nodes of the types that are enabled for scheduled unpublishing
- // and where unpublish_on is less than or equal to the current time.
- $nids = [];
- $scheduler_enabled_types = array_keys(_scheduler_get_scheduler_enabled_node_types($action));
- if (!empty($scheduler_enabled_types)) {
- $query = $this->entityTypeManager->getStorage('node')->getQuery()
- ->exists('unpublish_on')
- ->condition('unpublish_on', $this->time->getRequestTime(), '<=')
- ->condition('type', $scheduler_enabled_types, 'IN')
- ->latestRevision()
- ->sort('unpublish_on')
- ->sort('nid');
- // Disable access checks for this query.
- // @see https://www.drupal.org/node/2700209
- $query->accessCheck(FALSE);
- $nids = $query->execute();
- }
-
- // Allow other modules to add to the list of nodes to be unpublished.
- $nids = array_unique(array_merge($nids, $this->nidList($action)));
-
- // Allow other modules to alter the list of nodes to be unpublished.
- $this->moduleHandler->alter('scheduler_nid_list', $nids, $action);
-
- /** @var \Drupal\node\NodeInterface[] $nodes */
- $nodes = $this->loadNodes($nids);
- foreach ($nodes as $node_multilingual) {
- // The API calls could return nodes of types which are not enabled for
- // scheduled unpublishing. Do not process these.
- if (!$node_multilingual->type->entity->getThirdPartySetting('scheduler', 'unpublish_enable', $this->setting('default_unpublish_enable'))) {
- throw new SchedulerNodeTypeNotEnabledException(sprintf("Node %d '%s' will not be unpublished because node type '%s' is not enabled for scheduled unpublishing", $node_multilingual->id(), $node_multilingual->getTitle(), node_get_type_label($node_multilingual)));
- }
-
- $languages = $node_multilingual->getTranslationLanguages();
- foreach ($languages as $language) {
- // The object returned by getTranslation() behaves the same as a $node.
- $node = $node_multilingual->getTranslation($language->getId());
-
- // If the current translation does not have an unpublish on value, or it
- // is later than the date we are processing then move on to the next.
- $unpublish_on = $node->unpublish_on->value;
- if (empty($unpublish_on) || $unpublish_on > $this->time->getRequestTime()) {
- continue;
- }
-
- // Do not process the node if it still has a publish_on time which is in
- // the past, as this implies that scheduled publishing has been blocked
- // by one of the hook functions we provide, and is still being blocked
- // now that the unpublishing time has been reached.
- $publish_on = $node->publish_on->value;
- if (!empty($publish_on) && $publish_on <= $this->time->getRequestTime()) {
- continue;
+ $process = 'unpublish';
+ $plugins = $this->getPlugins();
+
+ foreach ($plugins as $entityTypeId => $plugin) {
+ // Select all entities of the types for this plugin that are enabled for
+ // scheduled unpublishing and where unpublish_on is less than or equal to
+ // the current time.
+ $ids = [];
+ $scheduler_enabled_types = $this->getEnabledTypes($entityTypeId, $process);
+
+ if (!empty($scheduler_enabled_types)) {
+ $query = $this->entityTypeManager->getStorage($entityTypeId)->getQuery()
+ ->exists('unpublish_on')
+ ->condition('unpublish_on', $this->time->getRequestTime(), '<=')
+ ->condition($plugin->typeFieldName(), $scheduler_enabled_types, 'IN')
+ ->sort('unpublish_on');
+ // Disable access checks for this query.
+ // @see https://www.drupal.org/node/2700209
+ $query->accessCheck(FALSE);
+ // If the entity type is revisionable then make sure we look for the
+ // latest revision. This is important for moderated entities.
+ if ($this->entityTypeManager->getDefinition($entityTypeId)->isRevisionable()) {
+ $query->latestRevision();
}
+ $ids = $query->execute();
+ }
- // Check that other modules allow the action on this node.
- if (!$this->isAllowed($node, $action)) {
- continue;
- }
+ // Allow other modules to add to the list of entities to be unpublished.
+ $hook_implementations = $this->getHookImplementations('list', $entityTypeId);
+ foreach ($hook_implementations as $function) {
+ // Cast each hook result as array, to protect from bad implementations.
+ $ids = array_merge($ids, (array) $function($process, $entityTypeId));
+ }
- // $node->setChangedTime($unpublish_on) will fail badly if an API call
- // has removed the date. Trap this as an exception here and give a
- // meaningful message.
- // @todo This will now never be thrown due to the empty(unpublish_on)
- // check above to cater for translations. Remove this exception?
- if (empty($unpublish_on)) {
- $field_definitions = $this->entityTypeManager->getFieldDefinitions('node', $node->getType());
- $field = (string) $field_definitions['unpublish_on']->getLabel();
- throw new SchedulerMissingDateException(sprintf("Node %d '%s' will not be unpublished because field '%s' has no value", $node->id(), $node->getTitle(), $field));
- }
+ // Allow other modules to alter the list of entities to be unpublished.
+ $hook_implementations = $this->getHookImplementations('list_alter', $entityTypeId);
+ foreach ($hook_implementations as $function) {
+ $function($ids, $process, $entityTypeId);
+ }
- // Trigger the PRE_UNPUBLISH event so that modules can react before the
- // node is unpublished.
- $event = new SchedulerEvent($node);
- $this->dispatch($event, SchedulerEvents::PRE_UNPUBLISH);
- $node = $event->getNode();
-
- // Update 'changed' timestamp.
- $node->setChangedTime($unpublish_on);
-
- $create_unpublishing_revision = $node->type->entity->getThirdPartySetting('scheduler', 'unpublish_revision', $this->setting('default_unpublish_revision'));
- if ($create_unpublishing_revision) {
- $node->setNewRevision();
- // Use a core date format to guarantee a time is included.
- $revision_log_message = $this->t('Unpublished by Scheduler. The scheduled unpublishing date was @unpublish_on.', [
- '@unpublish_on' => $this->dateFormatter->format($unpublish_on, 'short'),
- ]);
- // Create the new revision, setting message and revision timestamp.
- $node->setRevisionLogMessage($revision_log_message)
- ->setRevisionCreationTime($this->time->getRequestTime());
- }
- // Unset unpublish_on so the node will not get rescheduled by subsequent
- // calls to $node->save().
- $node->unpublish_on->value = NULL;
-
- // Invoke all implementations of hook_scheduler_unpublish_action() to
- // allow other modules to do the "unpublishing" process instead of
- // Scheduler.
- $hook = 'scheduler_unpublish_action';
- $processed = FALSE;
- $failed = FALSE;
- foreach ($this->moduleHandler->getImplementations($hook) as $module) {
- $function = $module . '_' . $hook;
- $return = $function($node);
- $processed = $processed || ($return === 1);
- $failed = $failed || ($return === -1);
- }
+ // Finally ensure that there are no duplicates in the list of ids.
+ $ids = array_unique($ids);
- // Set up the log variables.
- $view_link = $node->toLink($this->t('View node'));
- $node_type = $this->entityTypeManager->getStorage('node_type')->load($node->bundle());
- $node_type_link = $node_type->toLink($this->t('@label settings', ['@label' => $node_type->label()]), 'edit-form');
- $logger_variables = [
- '@type' => $node_type->label(),
- '%title' => $node->getTitle(),
- 'link' => $node_type_link->toString() . ' ' . $view_link->toString(),
- '@hook' => 'hook_' . $hook,
- ];
-
- if ($failed) {
- // At least one hook function returned a failure or exception, so stop
- // processing this node and move on to the next one.
- $this->logger->warning('Unpublishing failed for %title. Calls to @hook returned a failure code.', $logger_variables);
- continue;
- }
- elseif ($processed) {
- // The node has 'unpublishing' processed by a module implementing the
- // hook, so no need to do anything more, apart from log this result.
- $this->logger->notice('@type: scheduled processing of %title completed by calls to @hook.', $logger_variables);
- }
- else {
- // None of the above hook calls processed the node and there were no
- // errors detected so set the node to unpublished.
- $this->logger->notice('@type: scheduled unpublishing of %title.', $logger_variables);
- $node->setUnpublished();
- }
+ /** @var \Drupal\Core\Entity\EntityInterface[] $entities */
+ $entities = $this->loadEntities($ids, $entityTypeId);
+ foreach ($entities as $entity_multilingual) {
- // Invoke event to tell Rules that Scheduler has unpublished this node.
- if ($this->moduleHandler->moduleExists('scheduler_rules_integration')) {
- _scheduler_rules_integration_dispatch_cron_event($node, 'unpublish');
+ // The API calls could return entities of types which are not enabled
+ // for scheduled unpublishing, so do not process these. This check can
+ // be done once as the setting will be the same for all translations.
+ if (!$this->getThirdPartySetting($entity_multilingual, 'unpublish_enable', $this->setting('default_unpublish_enable'))) {
+ $this->throwSchedulerException($entity_multilingual, 'SchedulerEntityTypeNotEnabledException', $process);
}
- // Trigger the UNPUBLISH event so that modules can react after the node
- // is unpublished.
- $event = new SchedulerEvent($node);
- $this->dispatch($event, SchedulerEvents::UNPUBLISH);
-
- // Use the standard actions system to unpublish and save the node.
- $node = $event->getNode();
- $action_id = 'node_unpublish_action';
- if ($this->moduleHandler->moduleExists('workbench_moderation_actions')) {
- // workbench_moderation_actions module uses a custom action instead.
- $action_id = 'state_change__node__archived';
+ $languages = $entity_multilingual->getTranslationLanguages();
+ foreach ($languages as $language) {
+ // The object returned by getTranslation() is a normal $entity.
+ $entity = $entity_multilingual->getTranslation($language->getId());
+
+ // If the current translation does not have an unpublish-on value, or
+ // it is later than the date we are processing then move to the next.
+ $unpublish_on = $entity->unpublish_on->value;
+ if (empty($unpublish_on) || $unpublish_on > $this->time->getRequestTime()) {
+ continue;
+ }
+
+ // Do not process the entity if it still has a publish_on time which
+ // is in the past, as this implies that scheduled publishing has been
+ // blocked by one of the hook functions we provide, and is still being
+ // blocked now that the unpublishing time has been reached.
+ $publish_on = $entity->publish_on->value;
+ if (!empty($publish_on) && $publish_on <= $this->time->getRequestTime()) {
+ continue;
+ }
+
+ // Check that other modules allow the process on this entity.
+ if (!$this->isAllowed($entity, $process)) {
+ continue;
+ }
+
+ // Trigger the PRE_UNPUBLISH Scheduler event so that modules can react
+ // before the entity is unpublished.
+ $this->dispatchSchedulerEvent($entity, 'PRE_UNPUBLISH');
+
+ // Update 'changed' timestamp.
+ $entity->setChangedTime($unpublish_on);
+
+ $create_unpublishing_revision = $this->getThirdPartySetting($entity, 'unpublish_revision', $this->setting('default_unpublish_revision'));
+ if ($create_unpublishing_revision && $entity->getEntityType()->isRevisionable()) {
+ $entity->setNewRevision();
+ // Use a core date format to guarantee a time is included.
+ $revision_log_message = $this->t('Unpublished by Scheduler. The scheduled unpublishing date was @unpublish_on.', [
+ '@unpublish_on' => $this->dateFormatter->format($unpublish_on, 'short'),
+ ]);
+ // Create the new revision, setting message and revision timestamp.
+ $entity->setRevisionLogMessage($revision_log_message)
+ ->setRevisionCreationTime($this->time->getRequestTime());
+ }
+ // Unset publish_on so the entity will not get rescheduled by any
+ // interim calls to $entity->save().
+ $entity->unpublish_on->value = NULL;
+
+ // Invoke all implementations of hook_scheduler_unpublish_process()
+ // and hook_scheduler_{type}_unpublish_process() to allow other
+ // modules to do the "unpublishing" process instead of Scheduler.
+ $hook_implementations = $this->getHookImplementations('unpublish_process', $entity);
+ $processed = FALSE;
+ $failed = FALSE;
+ foreach ($hook_implementations as $function) {
+ $return = $function($entity);
+ $processed = $processed || ($return === 1);
+ $failed = $failed || ($return === -1);
+ }
+
+ // Log the fact that a scheduled unpublication is about to take place.
+ $entity_type = $this->entityTypeManager->getStorage($entityTypeId . '_type')->load($entity->bundle());
+ $view_link = $entity->toLink($this->t('View @type', [
+ '@type' => strtolower($entity_type->label()),
+ ]));
+ $entity_type_link = $entity_type->toLink($this->t('@label settings', [
+ '@label' => $entity_type->label(),
+ ]), 'edit-form');
+ $logger_variables = [
+ '@type' => $entity_type->label(),
+ '%title' => $entity->label(),
+ '@hook' => implode(', ', $hook_implementations),
+ 'link' => $view_link->toString() . ' ' . $entity_type_link->toString(),
+ ];
+
+ if ($failed) {
+ // At least one hook function returned a failure or exception, so
+ // stop processing this entity and move on to the next one.
+ $this->logger->warning('Unpublishing failed for %title. Calls to @hook returned a failure code.', $logger_variables);
+ // Restore the unpublish_on date to allow another attempt next time.
+ $entity->unpublish_on->value = $unpublish_on;
+ $entity->save();
+ continue;
+ }
+ elseif ($processed) {
+ // The entity was 'unpublished' by a module implementing the hook,
+ // so we only need to log this result.
+ $this->logger->notice('@type: scheduled processing of %title completed by calls to @hook.', $logger_variables);
+ }
+ else {
+ // None of the above hook calls processed the entity and there were
+ // no errors detected so set the entity to unpublished.
+ $this->logger->notice('@type: scheduled unpublishing of %title.', $logger_variables);
+ $entity->setUnpublished();
+ }
+
+ // Invoke event to tell Rules that Scheduler has unpublished the
+ // entity.
+ if ($this->moduleHandler->moduleExists('scheduler_rules_integration')) {
+ _scheduler_rules_integration_dispatch_cron_event($entity, $process);
+ }
+
+ // Trigger the UNPUBLISH Scheduler event so that modules can react
+ // after the entity is unpublished.
+ $this->dispatchSchedulerEvent($entity, 'UNPUBLISH');
+
+ // Use the standard actions system to unpublish and save the entity.
+ $action_id = $plugin->unpublishAction();
+ if ($this->moduleHandler->moduleExists('workbench_moderation_actions')) {
+ // workbench_moderation_actions module uses a custom action instead.
+ $action_id = 'state_change__' . $entityTypeId . '__archived';
+ }
+ if (!$loaded_action = $this->entityTypeManager->getStorage('action')->load($action_id)) {
+ $this->missingAction($action_id, $process);
+ }
+ $loaded_action->getPlugin()->execute($entity);
+
+ $result = TRUE;
}
- $this->entityTypeManager->getStorage('action')->load($action_id)->getPlugin()->execute($node);
-
- $result = TRUE;
}
}
@@ -477,57 +604,135 @@ class SchedulerManager {
}
/**
- * Checks whether a scheduled action on a node is allowed.
+ * Checks whether a scheduled process on an entity is allowed.
*
- * This provides a way for other modules to prevent scheduled publishing or
- * unpublishing, by implementing hook_scheduler_allow_publishing() or
- * hook_scheduler_allow_unpublishing().
+ * Other modules can prevent scheduled publishing or unpublishing by
+ * implementing any or all of the following:
+ * hook_scheduler_publishing_allowed()
+ * hook_scheduler_unpublishing_allowed()
+ * hook_scheduler_{type}_publishing_allowed()
+ * hook_scheduler_{type}_unpublishing_allowed()
*
- * @param \Drupal\node\NodeInterface $node
- * The node on which the action is to be performed.
- * @param string $action
- * The action that needs to be checked. Can be 'publish' or 'unpublish'.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity on which the process is to be performed.
+ * @param string $process
+ * The process to be checked. Values are 'publish' or 'unpublish'.
*
* @return bool
- * TRUE if the action is allowed, FALSE if not.
- *
- * @see hook_scheduler_allow_publishing()
- * @see hook_scheduler_allow_unpublishing()
+ * TRUE if the process is allowed, FALSE if not.
*/
- public function isAllowed(NodeInterface $node, $action) {
+ public function isAllowed(EntityInterface $entity, $process) {
// Default to TRUE.
$result = TRUE;
- // Check that other modules allow the action.
- $hook = 'scheduler_allow_' . $action . 'ing';
- foreach ($this->moduleHandler->getImplementations($hook) as $module) {
- $function = $module . '_' . $hook;
- $result &= $function($node);
- }
+ // Get all implementations of the required hook function.
+ $hook_implementations = $this->getHookImplementations($process . 'ing_allowed', $entity);
+
+ // Call the hook functions. If any specifically return FALSE the overall
+ // result is FALSE. If a hook returns nothing it will not affect the result.
+ foreach ($hook_implementations as $function) {
+ $returned = $function($entity);
+ $result &= !(isset($returned) && $returned == FALSE);
+ }
return $result;
}
/**
- * Gather node IDs for all nodes that need to be $action'ed.
+ * Returns an array of hook function names implemented for a hook type.
*
- * Modules can implement hook_scheduler_nid_list($action) and return an array
- * of node ids which will be added to the existing list.
+ * The return array will include all implementations of the general hook
+ * function called for all entity types, plus all implemented hooks for the
+ * specific type of entity being processed. In addition, for node entities,
+ * the original hook functions (prior to entity plugins) are added to maintain
+ * backwards-compatibility.
*
- * @param string $action
- * The action being performed, either "publish" or "unpublish".
+ * @param string $hookType
+ * The identifier of the hook function, for example 'publish_process' or
+ * 'unpublishing_allowed' or 'hide_publish_date'.
+ * @param \Drupal\Core\Entity\EntityInterface|string $entity
+ * The entity object which is being processed, or a string containing the
+ * entity type id (for example 'node' or 'media').
*
* @return array
- * An array of node ids.
+ * An array of callable function names for the implementations of this hook
+ * function for the type of entity being processed.
*/
- public function nidList($action) {
- $nids = [];
+ public function getHookImplementations(string $hookType, $entity) {
+ $entityTypeId = (is_object($entity)) ? $entity->getEntityTypeid() : $entity;
+ $hooks = [$hookType, "{$entityTypeId}_{$hookType}"];
+
+ // For backwards compatibility the original node hook is also added.
+ if ($entityTypeId == 'node') {
+ $legacy_node_hooks = [
+ 'hide_publish_date' => 'hide_publish_on_field',
+ 'hide_unpublish_date' => 'hide_unpublish_on_field',
+ 'list' => 'nid_list',
+ 'list_alter' => 'nid_list_alter',
+ 'publish_process' => 'publish_action',
+ 'unpublish_process' => 'unpublish_action',
+ 'publishing_allowed' => 'allow_publishing',
+ 'unpublishing_allowed' => 'allow_unpublishing',
+ ];
+ $hooks[] = $legacy_node_hooks[$hookType];
+ }
+
+ // Get all modules that implement these hooks, then use array_walk to append
+ // the $hook to the end of the module, thus giving the full function name.
+ $all_hook_implementations = [];
+ foreach ($hooks as $hook) {
+ $hook = "scheduler_$hook";
+ $implementations = $this->moduleHandler->getImplementations($hook);
+ array_walk($implementations, function (&$item) use ($hook) {
+ $item = $item . '_' . $hook;
+ });
+ $all_hook_implementations = array_merge($all_hook_implementations, $implementations);
+ }
+ return $all_hook_implementations;
+ }
- foreach ($this->moduleHandler->getImplementations('scheduler_nid_list') as $module) {
- $function = $module . '_scheduler_nid_list';
- $nids = array_merge($nids, $function($action));
+ /**
+ * Gives details and throws exception when a required action is missing.
+ *
+ * This displays a screen error message which is useful if the cron run was
+ * initiated via the site UI. This will also be shown on the terminal if cron
+ * was run via drush. If the Config Update module is installed then a link is
+ * given to the actions report in Config UI, which lists the missing items and
+ * provides a button to import from source. If Config Update is not installed
+ * then a link is provided to its Drupal project page.
+ *
+ * @param string $action_id
+ * The id of the missing action.
+ * @param string $process
+ * The Scheduler process being run, 'publish' or 'unpublish'.
+ */
+ protected function missingAction(string $action_id, string $process) {
+ $logger_variables = ['%action_id' => $action_id];
+ // If the Config Update module is available then link to the UI report. If
+ // not then link to the project page on drupal.org.
+ if (\Drupal::moduleHandler()->moduleExists('config_update')) {
+ // If the report UI sub-module is enabled then link directly to the
+ // actions report. Otherwise link to 'Extend' so it can be enabled.
+ if (\Drupal::moduleHandler()->moduleExists('config_update_ui')) {
+ $link = Link::fromTextAndUrl($this->t('Config Update for actions'), Url::fromRoute('config_update_ui.report', [
+ 'report_type' => 'type',
+ 'name' => 'action',
+ ]));
+ }
+ else {
+ $link = Link::fromTextAndUrl($this->t('Enable Config Update Reports'), Url::fromRoute('system.modules_list', ['filter' => 'config_update']));
+ }
+ $logger_variables['link'] = $link->toString();
+ $logger_variables[':url'] = $link->getUrl()->toString();
+ }
+ else {
+ $project_page = 'https://www.drupal.org/project/config_update';
+ $logger_variables[':url'] = $project_page;
+ $logger_variables['link'] = Link::fromTextAndUrl('Config Update project page', Url::fromUri($project_page))->toString();
}
- return $nids;
+ \Drupal::messenger()->addError($this->t("Action '%action_id' is missing. Use <a href=':url'>Config Update</a> to import the missing action.", $logger_variables));
+ $this->logger->warning("Action '%action_id' is missing. Use Config Update to import the missing action.", $logger_variables);
+ throw new \Exception("Action '{$action_id}' is missing. Scheduled $process halted.");
}
/**
@@ -558,6 +763,7 @@ class SchedulerManager {
else {
$trigger = 'url';
}
+ // This has to be 'notice' not 'info' so that drush can show the message.
$this->logger->notice('Lightweight cron run activated by @trigger.', ['@trigger' => $trigger]);
}
scheduler_cron();
@@ -587,30 +793,380 @@ class SchedulerManager {
}
/**
- * Helper method to load latest revision of each node.
+ * Get third-party setting for and entity type, via the entity object.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity.
+ * @param string $setting
+ * The setting to retrieve.
+ * @param mixed $default
+ * The default value for setting if none is found.
+ *
+ * @return mixed
+ * The value of the setting.
+ */
+ public function getThirdPartySetting(EntityInterface $entity, $setting, $default) {
+ $typeFieldName = $this->getPlugin($entity->getEntityTypeId())->typeFieldName();
+ if (empty($entity->$typeFieldName)) {
+ // Avoid exception and give details if the typeFieldName does not exist.
+ $params = [
+ '%field' => $typeFieldName,
+ '%id' => $this->getPlugin($entity->getEntityTypeId())->getPluginId(),
+ '%entity' => $entity->getEntityTypeId(),
+ ];
+ \Drupal::messenger()->addError($this->t("Field '%field' specified by typeFieldName in the Scheduler plugin %id is not found in entity type %entity", $params));
+ $this->logger->error("Field '%field' specified by typeFieldName in the Scheduler plugin %id is not found in entity type %entity", $params);
+ return $default;
+ }
+ else {
+ return $entity->$typeFieldName->entity->getThirdPartySetting('scheduler', $setting, $default);
+ }
+ }
+
+ /**
+ * Helper method to load latest revision of each entity.
*
- * @param array $nids
- * Array of node ids.
+ * @param array $ids
+ * Array of entity ids.
+ * @param string $type
+ * The type of entity.
*
* @return array
- * Array of loaded nodes.
+ * Array of loaded entity objects, keyed by id.
+ */
+ protected function loadEntities(array $ids, string $type) {
+ $storage = $this->entityTypeManager->getStorage($type);
+ $entities = [];
+ foreach ($ids as $id) {
+ // Avoid errors when an implementation of hook_scheduler_{type}_list has
+ // added an id of the wrong type.
+ if (!$entity = $storage->load($id)) {
+ $this->logger->notice('Entity id @id is not a @type entity. Processing skipped.', [
+ '@id' => $id,
+ '@type' => $type,
+ ]);
+ continue;
+ }
+ // If the entity type is revisionable then load the latest revision. For
+ // moderated entities this may be an unpublished draft update of a
+ // currently published entity.
+ if ($entity->getEntityType()->isRevisionable()) {
+ $vid = $storage->getLatestRevisionId($id);
+ $entities[$id] = $storage->loadRevision($vid);
+ }
+ else {
+ $entities[$id] = $entity;
+ }
+ }
+ return $entities;
+ }
+
+ /**
+ * Get a list of all scheduler plugin definitions.
*
- * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
- * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @return array|mixed[]|null
+ * A list of definitions for the registered scheduler plugins.
*/
- protected function loadNodes(array $nids) {
- $node_storage = $this->entityTypeManager->getStorage('node');
- $nodes = [];
+ public function getPluginDefinitions() {
+ $plugin_definitions = $this->pluginManager->getDefinitions();
+ // Sort in reverse order so that we have 'node_scheduler' followed by
+ // 'media_scheduler'. When a third entity type plugin gets implemented it
+ // would be possible to add a 'weight' property and sort by that.
+ arsort($plugin_definitions);
+ return $plugin_definitions;
+ }
- // Load the latest revision for each node.
- foreach ($nids as $nid) {
- $node = $node_storage->load($nid);
- $revision_ids = $node_storage->revisionIds($node);
- $vid = end($revision_ids);
- $nodes[] = $node_storage->loadRevision($vid);
+ /**
+ * Gets instances of applicable Scheduler plugins for the enabled modules.
+ *
+ * @param string $provider
+ * Optional. Filter the plugins to return only those that are provided by
+ * the named $provider module.
+ *
+ * @return array
+ * Array of plugin objects, keyed by the entity type the plugin supports.
+ */
+ public function getPlugins(string $provider = NULL) {
+ $cache = \Drupal::cache()->get('scheduler.plugins');
+ if (!empty($cache) && !empty($cache->data) && empty($provider)) {
+ return $cache->data;
+ }
+
+ $definitions = $this->getPluginDefinitions();
+ $plugins = [];
+ foreach ($definitions as $definition) {
+ $plugin = $this->pluginManager->createInstance($definition['id']);
+ $dependency = $plugin->dependency();
+ // Ignore plugins if there is a dependency module and it is not enabled.
+ if ($dependency && !\Drupal::moduleHandler()->moduleExists($dependency)) {
+ continue;
+ }
+ // Ignore plugins that do not match the specified provider module name.
+ if ($provider && $definition['provider'] != $provider) {
+ continue;
+ }
+ $plugins[$plugin->entityType()] = $plugin;
+ }
+
+ // Save to the cache only when not filtered for a particular a provider.
+ if (empty($provider)) {
+ \Drupal::cache()->set('scheduler.plugins', $plugins);
+ }
+ return $plugins;
+ }
+
+ /**
+ * Reset the scheduler plugins cache.
+ */
+ public function invalidatePluginCache() {
+ \Drupal::cache()->invalidate('scheduler.plugins');
+ }
+
+ /**
+ * Get the supported entity types applicable to the currently enabled modules.
+ *
+ * @param string $provider
+ * Optional. Filter the returned entity types for only those from the
+ * plugins that are provided by the named $provider module.
+ *
+ * @return array
+ * A list of the entity type ids.
+ */
+ public function getPluginEntityTypes(string $provider = NULL) {
+ return array_keys($this->getPlugins($provider));
+ }
+
+ /**
+ * Get a plugin for a specific entity type.
+ *
+ * @param string $entity_type
+ * The entity type.
+ *
+ * @return mixed
+ * The plugin object associated with a specific entity, or NULL if none.
+ */
+ public function getPlugin($entity_type) {
+ $plugins = $this->getPlugins();
+ return $plugins[$entity_type] ?? NULL;
+ }
+
+ /**
+ * Gets the names of the types/bundles enabled for a specific process.
+ *
+ * If the entity type is not supported by Scheduler, or there are no enabled
+ * bundles for this process within the entity type, then an empty array is
+ * returned.
+ *
+ * @param string $entityTypeId
+ * The entity type id, for example 'node' or 'media'.
+ * @param string $process
+ * The process to check - 'publish' or 'unpublish'.
+ *
+ * @return array
+ * The entity's type/bundle names that are enabled for the required process.
+ */
+ public function getEnabledTypes($entityTypeId, $process) {
+ if (!$plugin = $this->getPlugin($entityTypeId)) {
+ return [];
+ };
+ $types = $plugin->getTypes();
+ $types = array_filter($types, function ($bundle) use ($process) {
+ return $bundle->getThirdPartySetting('scheduler', $process . '_enable', $this->setting('default_' . $process . '_enable'));
+ });
+ return array_keys($types);
+ }
+
+ /**
+ * Gets list of entity add/edit form IDs.
+ *
+ * @return array
+ * List of entity add/edit form IDs for all registered scheduler plugins.
+ */
+ public function getEntityFormIds() {
+ $plugins = $this->getPlugins();
+ $form_ids = [];
+ foreach ($plugins as $plugin) {
+ $form_ids = array_merge($form_ids, $plugin->entityFormIDs());
+ }
+ return $form_ids;
+ }
+
+ /**
+ * Gets list of entity type add/edit form IDs.
+ *
+ * @return array
+ * List of entity type add/edit form IDs for registered scheduler plugins.
+ */
+ public function getEntityTypeFormIds() {
+ $plugins = $this->getPlugins();
+ $form_ids = [];
+ foreach ($plugins as $plugin) {
+ $form_ids = array_merge($form_ids, $plugin->entityTypeFormIDs());
+ }
+ return $form_ids;
+ }
+
+ /**
+ * Gets the supported Devel Generate form IDs.
+ *
+ * @return array
+ * List of form IDs used by Devel Generate, keyed by entity type.
+ */
+ public function getDevelGenerateFormIds() {
+ $plugins = $this->getPlugins();
+ $form_ids = [];
+ foreach ($plugins as $entityTypeId => $plugin) {
+ // The devel_generate form id is optional so only save if a value exists.
+ // Use entity type as key so we can get back from form_id to entity.
+ if ($form_id = $plugin->develGenerateForm()) {
+ $form_ids[$entityTypeId] = $form_id;
+ }
+ }
+ return $form_ids;
+ }
+
+ /**
+ * Gets the routes for user profile page scheduled views.
+ *
+ * @return array
+ * List of routes for the user page views, keyed by entity type.
+ */
+ public function getUserPageViewRoutes() {
+ $plugins = $this->getPlugins();
+ $routes = [];
+ foreach ($plugins as $entityTypeId => $plugin) {
+ // The user view is optional so only save if there is a value.
+ if ($route = $plugin->userViewRoute()) {
+ $routes[$entityTypeId] = $route;
+ }
+ }
+ return $routes;
+ }
+
+ /**
+ * Derives the permission name for an entity type and permission type.
+ *
+ * This function is added because for backwards-compatibility the node
+ * permission names have to end with 'nodes' and 'content'. For all other
+ * newly-supported entity types it is $entityTypeId.
+ *
+ * @param string $entityTypeId
+ * The entity type id, for example 'node', 'media' etc.
+ * @param string $permissionType
+ * The type of permission - 'schedule' or 'view'.
+ *
+ * @return string
+ * The internal name of the scheduler permission.
+ */
+ public function permissionName($entityTypeId, $permissionType) {
+ switch ($permissionType) {
+ case 'schedule':
+ return 'schedule publishing of ' . ($entityTypeId == 'node' ? 'nodes' : $entityTypeId);
+
+ case 'view':
+ return 'view scheduled ' . ($entityTypeId == 'node' ? 'content' : $entityTypeId);
+ }
+ }
+
+ /**
+ * Updates db tables for entities that should have the Scheduler fields.
+ *
+ * This is called from scheduler_modules_installed and scheduler_update_8103.
+ * It can also be called manually via drush command scheduler-entity-update.
+ *
+ * @return array
+ * Labels of the entity types updated.
+ */
+ public function entityUpdate() {
+ $entityUpdateManager = \Drupal::entityDefinitionUpdateManager();
+ $updated = [];
+ $list = $entityUpdateManager->getChangeList();
+ foreach ($list as $entity_type_id => $definitions) {
+ if ($definitions['field_storage_definitions']['publish_on'] ?? 0) {
+ $entity_type = $entityUpdateManager->getEntityType($entity_type_id);
+ $fields = scheduler_entity_base_field_info($entity_type);
+ foreach ($fields as $field_name => $field_definition) {
+ $entityUpdateManager->installFieldStorageDefinition($field_name, $entity_type_id, $entity_type_id, $field_definition);
+ }
+ $this->logger->notice('%entity updated with Scheduler publish_on and unpublish_on fields.', [
+ '%entity' => $entity_type->getLabel(),
+ ]);
+ $updated[] = (string) $entity_type->getLabel();
+ }
}
+ return $updated;
+ }
- return $nodes;
+ /**
+ * Refreshes scheduler views from source.
+ *
+ * If the view exists in the site's active storage it will be updated from the
+ * source yml file. If the view is now required but does not exist in active
+ * storage it will be loaded.
+ *
+ * Called from scheduler_modules_installed() and scheduler_update_8104().
+ *
+ * @param array $only_these_types
+ * List of entity types to restrict the update of views to these types only.
+ * Optional. If none then revert/load all applicable scheduler views.
+ *
+ * @return array
+ * Labels of the views that were updated.
+ */
+ public function viewsUpdate(array $only_these_types = []) {
+ $updated = [];
+ $definition = $this->entityTypeManager->getDefinition('view');
+ $view_storage = $this->entityTypeManager->getStorage('view');
+ // Get the supported entity type ids for enabled modules where the provider
+ // is Scheduler. Third-party plugins do not need to be processed here.
+ $entity_types = $this->getPluginEntityTypes('scheduler');
+ if ($only_these_types) {
+ $entity_types = array_intersect($entity_types, $only_these_types);
+ }
+
+ foreach ($entity_types as $entity_type) {
+ $name = 'scheduler_scheduled_' . ($entity_type == 'node' ? 'content' : $entity_type);
+ $full_name = $definition->getConfigPrefix() . '.' . $name;
+
+ // Read the view definition from the .yml file. First try the /optional
+ // folder, then the main /config folder.
+ $optional_folder = drupal_get_path('module', 'scheduler') . '/config/optional';
+ $source_storage = new FileStorage($optional_folder);
+ if (!$source = $source_storage->read($full_name)) {
+ $install_folder = drupal_get_path('module', 'scheduler') . '/config/install';
+ $source_storage = new FileStorage($install_folder);
+ if (!$source = $source_storage->read($full_name)) {
+ throw new \Exception(sprintf('Failed to read source file for %s from either %s or %s folders', $full_name, $install_folder, $optional_folder));
+ }
+ }
+
+ // Try to read the view definition from active config storage.
+ /** @var \Drupal\Core\Config\StorageInterface $config_storage */
+ $config_storage = \Drupal::service('config.storage');
+ if ($config_storage->read($full_name)) {
+ // The view does exist in active storage, so load it, then replace the
+ // value with the source, but retain the _core and uuid values.
+ $view = $view_storage->load($name);
+ $core = $view->get('_core');
+ $uuid = $view->get('uuid');
+ $view = $view_storage->updateFromStorageRecord($view, $source);
+ $view->set('_core', $core);
+ $view->set('uuid', $uuid);
+ $view->save();
+ $this->logger->notice('%view view updated.', ['%view' => $source['label']]);
+ }
+ else {
+ // The view does not exist in active storage so import it from source.
+ $view = $view_storage->createFromStorageRecord($source);
+ $view->save();
+ $this->logger->notice('%view view loaded from source.', ['%view' => $source['label']]);
+ }
+ $updated[] = $source['label'];
+ }
+ // The views are loaded OK but the publish-on and unpublish-on views field
+ // handlers are not found. Clearing the views data cache solves the problem.
+ Cache::invalidateTags(['views_data']);
+ return $updated;
}
}
diff --git a/src/SchedulerPermissions.php b/src/SchedulerPermissions.php
new file mode 100644
index 0000000..0af7384
--- /dev/null
+++ b/src/SchedulerPermissions.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Drupal\scheduler;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides dynamic permissions for scheduler plugins.
+ */
+class SchedulerPermissions implements ContainerInjectionInterface {
+
+ use StringTranslationTrait;
+
+ /**
+ * The scheduler manager service.
+ *
+ * @var SchedulerManager
+ */
+ private $schedulerManager;
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs a \Drupal\scheduler\SchedulerPermissions instance.
+ *
+ * @param \Drupal\scheduler\SchedulerManager $scheduler_manager
+ * The entity type manager.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ */
+ public function __construct(SchedulerManager $scheduler_manager, EntityTypeManagerInterface $entity_type_manager) {
+ $this->schedulerManager = $scheduler_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static($container->get('scheduler.manager'), $container->get('entity_type.manager'));
+ }
+
+ /**
+ * Build permissions for each entity type.
+ *
+ * SchedulerManager function permissionName() can be used to return the
+ * permission name for a given entity type and permission type.
+ *
+ * @return array|array[]
+ * The full list of permissions to schedule and to view each entity type.
+ */
+ public function permissions() {
+ $permissions = [];
+ $types = $this->schedulerManager->getPluginEntityTypes();
+ foreach ($types as $entity_type_id) {
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+ // For backwards-compatibility with existing permissions, the node
+ // permission names have to end with 'nodes' and 'content'. For all other
+ // entity types we use $entity_type_id for both permissions.
+ if ($entity_type_id == 'node') {
+ $edit_key = 'nodes';
+ $view_key = 'content';
+ }
+ else {
+ $edit_key = $view_key = $entity_type_id;
+ }
+ $t_args = [
+ '%label' => $entity_type->getLabel(),
+ '%singular_label' => $entity_type->getSingularLabel(),
+ '%plural_label' => $entity_type->getPluralLabel(),
+ ];
+
+ $permissions += [
+ "schedule publishing of $edit_key" => [
+ 'title' => $this->t('Schedule publishing and unpublishing of %label', $t_args),
+ 'description' => $this->t('Allows users to set a start and end time for %singular_label publication.', $t_args),
+ ],
+ "view scheduled $view_key" => [
+ 'title' => $this->t('View scheduled %label', $t_args),
+ 'description' => $this->t('Allows users to see a list of all %plural_label that are scheduled.', $t_args),
+ ],
+ ];
+ }
+ return $permissions;
+ }
+
+}
diff --git a/src/SchedulerPluginBase.php b/src/SchedulerPluginBase.php
new file mode 100644
index 0000000..00185a3
--- /dev/null
+++ b/src/SchedulerPluginBase.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Drupal\scheduler;
+
+use Drupal\Core\Plugin\PluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Base class for scheduler plugins.
+ */
+abstract class SchedulerPluginBase extends PluginBase implements SchedulerPluginInterface {
+
+ /**
+ * Create method.
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('string_translation')
+ );
+ }
+
+ /**
+ * Get plugin label.
+ *
+ * @return string
+ * The label.
+ */
+ public function label() {
+ return $this->pluginDefinition['label'];
+ }
+
+ /**
+ * Get the plugin description.
+ *
+ * @inheritDoc
+ */
+ public function description() {
+ return $this->pluginDefinition['description'];
+ }
+
+ /**
+ * Get the type of entity supported by this plugin.
+ *
+ * @return string
+ * The name of the entity type.
+ */
+ public function entityType() {
+ return $this->pluginDefinition['entityType'];
+ }
+
+ /**
+ * Get the name of the "type" field for the entity.
+ *
+ * @return string
+ * The name of the type/bundle field for this entity type.
+ */
+ public function typeFieldName() {
+ return $this->pluginDefinition['typeFieldName'];
+ }
+
+ /**
+ * Get module dependency.
+ *
+ * @return string
+ * The name of the required module.
+ */
+ public function dependency() {
+ return $this->pluginDefinition['dependency'];
+ }
+
+ /**
+ * Get the id of the Devel Generate form for this entity type.
+ *
+ * @return string
+ * The form id, or an empty string if none.
+ */
+ public function develGenerateForm() {
+ return $this->pluginDefinition['develGenerateForm'];
+ }
+
+ /**
+ * Get the route of the scheduled view on the user profile page.
+ *
+ * @return string
+ * The form id, or an empty string if none.
+ */
+ public function userViewRoute() {
+ return $this->pluginDefinition['userViewRoute'];
+ }
+
+ /**
+ * Get the Scheduler event class.
+ *
+ * @return string
+ * The event class.
+ */
+ public function schedulerEventClass() {
+ // If no class is defined in the plugin then default to the standard
+ // scheduler class '\Drupal\scheduler\Event\Scheduler{Type}Events'.
+ $class = $this->pluginDefinition['schedulerEventClass'] ??
+ '\Drupal\scheduler\Event\Scheduler' . ucfirst($this->entityType()) . 'Events';
+ return $class;
+ }
+
+ /**
+ * Get the publish action name of the entity type.
+ *
+ * If no value is given in the plugin annotation then default to the commonly
+ * used {entity type id}_publish_action.
+ *
+ * @return string
+ * The action name.
+ */
+ public function publishAction() {
+ return $this->pluginDefinition['publishAction'] ?? $this->entityType() . '_publish_action';
+ }
+
+ /**
+ * Get the unpublish action name of the entity type.
+ *
+ * If no value is given in the plugin annotation then default to the commonly
+ * used {entity type id}_unpublish_action.
+ *
+ * @return string
+ * The action name.
+ */
+ public function unpublishAction() {
+ return $this->pluginDefinition['unpublishAction'] ?? $this->entityType() . '_unpublish_action';
+ }
+
+ /**
+ * Get all the type/bundle objects for this entity.
+ *
+ * @return array
+ * The type/bundle objects.
+ */
+ abstract public function getTypes();
+
+ /**
+ * Get the form IDs for entity add/edit forms.
+ */
+ abstract public function entityFormIds();
+
+ /**
+ * Get the form IDs for entity type add/edit forms.
+ */
+ abstract public function entityTypeFormIds();
+
+}
diff --git a/src/SchedulerPluginInterface.php b/src/SchedulerPluginInterface.php
new file mode 100644
index 0000000..7fb8753
--- /dev/null
+++ b/src/SchedulerPluginInterface.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\scheduler;
+
+/**
+ * Interface for Scheduler entity plugin definition.
+ */
+interface SchedulerPluginInterface {
+
+ /**
+ * Get the label.
+ *
+ * @return mixed
+ * The label.
+ */
+ public function label();
+
+ /**
+ * Get the description.
+ *
+ * @return mixed
+ * The description.
+ */
+ public function description();
+
+ /**
+ * Get the type of entity supported by this plugin.
+ *
+ * @return string
+ * The name of the entity type.
+ */
+ public function entityType();
+
+ /**
+ * Get the name of the "type" field for the entity.
+ *
+ * @return string
+ * The name of the type/bundle field for this entity type.
+ */
+ public function typeFieldName();
+
+ /**
+ * Get module dependency.
+ *
+ * @return string
+ * The name of the required module.
+ */
+ public function dependency();
+
+ /**
+ * Get the id of the Devel Generate form for this entity type. Optional.
+ *
+ * @return string
+ * The form id.
+ */
+ public function develGenerateForm();
+
+ /**
+ * Get the route of the user page scheduled view. Optional.
+ *
+ * @return string
+ * The route id.
+ */
+ public function userViewRoute();
+
+ /**
+ * Get the scheduler event class.
+ *
+ * Optional. Defaults to '\Drupal\scheduler\Event\Scheduler{Type}Events' the
+ * event class within the Scheduler module namespace.
+ *
+ * @return string
+ * The event class.
+ */
+ public function schedulerEventClass();
+
+ /**
+ * Get the publish action name of the entity type.
+ *
+ * Optional. Defaults to the commonly used {entity type id}_publish_action.
+ *
+ * @return string
+ * The action name.
+ */
+ public function publishAction();
+
+ /**
+ * Get the unpublish action name of the entity type.
+ *
+ * Optional. Defaults to the commonly used {entity type id}_unpublish_action.
+ *
+ * @return string
+ * The action name.
+ */
+ public function unpublishAction();
+
+ /**
+ * Get all the type/bundle objects for this entity.
+ *
+ * @return array
+ * The type/bundle objects.
+ */
+ public function getTypes();
+
+ /**
+ * Get the form IDs for entity add/edit forms.
+ *
+ * @return array
+ * A list of add/edit form ids for all bundles in this entity type.
+ */
+ public function entityFormIds();
+
+ /**
+ * Get the form IDs for entity type add/edit forms.
+ *
+ * @return array
+ * A list of add/edit form ids for this entity type.
+ */
+ public function entityTypeFormIds();
+
+}
diff --git a/src/SchedulerPluginManager.php b/src/SchedulerPluginManager.php
new file mode 100644
index 0000000..e049b15
--- /dev/null
+++ b/src/SchedulerPluginManager.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\scheduler;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\scheduler\Annotation\SchedulerPlugin;
+
+/**
+ * Provides a Scheduler Plugin Manager.
+ *
+ * @package Drupal\scheduler
+ */
+class SchedulerPluginManager extends DefaultPluginManager {
+
+ /**
+ * Constructor.
+ */
+ public function __construct(\Traversable $namespaces, CacheBackendInterface $cacheBackend, ModuleHandlerInterface $module_handler) {
+
+ $subdir = 'Plugin/Scheduler';
+ $plugin_interface = SchedulerPluginInterface::class;
+ $plugin_definition_annotation_name = SchedulerPlugin::class;
+
+ parent::__construct(
+ $subdir,
+ $namespaces,
+ $module_handler,
+ $plugin_interface,
+ $plugin_definition_annotation_name
+ );
+
+ $this->alterInfo('scheduler_info');
+ $this->setCacheBackend($cacheBackend, 'scheduler_info');
+ }
+
+}
diff --git a/src/Theme/SchedulerThemeNegotiator.php b/src/Theme/SchedulerThemeNegotiator.php
index 11f5a45..3e0cd9d 100644
--- a/src/Theme/SchedulerThemeNegotiator.php
+++ b/src/Theme/SchedulerThemeNegotiator.php
@@ -14,8 +14,9 @@ class SchedulerThemeNegotiator implements ThemeNegotiatorInterface {
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
- // Use the Scheduler theme negotiator for the user 'scheduled' tab.
- $applies = ($route_match->getRouteName() == 'view.scheduler_scheduled_content.user_page');
+ // Use the Scheduler theme negotiator for scheduler views on the user page.
+ $user_page_routes = \Drupal::service('scheduler.manager')->getUserPageViewRoutes();
+ $applies = (in_array($route_match->getRouteName(), $user_page_routes));
return $applies;
}
diff --git a/tests/modules/scheduler_access_test/scheduler_access_test.info.yml b/tests/modules/scheduler_access_test/scheduler_access_test.info.yml
index 75db76f..b7c0d52 100644
--- a/tests/modules/scheduler_access_test/scheduler_access_test.info.yml
+++ b/tests/modules/scheduler_access_test/scheduler_access_test.info.yml
@@ -1,6 +1,6 @@
-name: 'Scheduler Node Access Test'
+name: 'Scheduler Entity Access Test'
type: module
-description: 'Support module for Scheduler restricted node access testing.'
+description: 'Support module for Scheduler testing with restricted entity access.'
package: Testing
core: 8.x
core_version_requirement: ^8 || ^9
diff --git a/tests/modules/scheduler_access_test/scheduler_access_test.module b/tests/modules/scheduler_access_test/scheduler_access_test.module
index 2caafbb..df95ee9 100644
--- a/tests/modules/scheduler_access_test/scheduler_access_test.module
+++ b/tests/modules/scheduler_access_test/scheduler_access_test.module
@@ -2,7 +2,12 @@
/**
* @file
- * Installation file for Scheduler Access Test module.
+ * Scheduler Access Test module.
+ *
+ * This module is used in SchedulerEntityAccessTest and removes access to all
+ * published nodes. The Media module does not provide any corresponding hooks to
+ * restrict Media access. This module, and the tests, can be expanded when a
+ * suitable access restriction method becomes available for Media entities.
*/
use Drupal\Core\Session\AccountInterface;
@@ -13,7 +18,8 @@ use Drupal\node\NodeInterface;
*/
function scheduler_access_test_node_access_records(NodeInterface $node) {
// For the purpose of this test we deny access to every node regardless of
- // its published status.
+ // its published status. However, users with 'View own unpublished {type}'
+ // permission will still be able to view their unpublished nodes.
$grants = [[
'realm' => 'scheduler',
'gid' => 1,
diff --git a/tests/modules/scheduler_api_test/config/install/commerce_product.commerce_product_type.scheduler_api_product_test.yml b/tests/modules/scheduler_api_test/config/install/commerce_product.commerce_product_type.scheduler_api_product_test.yml
new file mode 100644
index 0000000..ac25817
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/commerce_product.commerce_product_type.scheduler_api_product_test.yml
@@ -0,0 +1,30 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - scheduler
+ enforced:
+ module:
+ - scheduler_api_test
+third_party_settings:
+ scheduler:
+ expand_fieldset: always
+ fields_display_mode: fieldset
+ publish_enable: true
+ publish_past_date: schedule
+ publish_past_date_created: false
+ publish_required: false
+ publish_revision: false
+ publish_touch: false
+ show_message_after_update: true
+ unpublish_enable: true
+ unpublish_required: false
+ unpublish_revision: false
+id: scheduler_api_product_test
+label: 'Scheduler API Product Test'
+description: 'Commerce Product type used in Scheduler API testing'
+variationType: default
+multipleVariations: true
+injectVariationFields: true
+traits: { }
+locked: false
diff --git a/tests/modules/scheduler_api_test/config/install/core.entity_form_display.commerce_product.scheduler_api_product_test.default.yml b/tests/modules/scheduler_api_test/config/install/core.entity_form_display.commerce_product.scheduler_api_product_test.default.yml
new file mode 100644
index 0000000..ee919a8
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/core.entity_form_display.commerce_product.scheduler_api_product_test.default.yml
@@ -0,0 +1,97 @@
+uuid: fdf40551-08a6-4b2c-83eb-44bc868be4ac
+langcode: en
+status: true
+dependencies:
+ config:
+ - commerce_product.commerce_product_type.scheduler_api_product_test
+ - field.field.commerce_product.scheduler_api_product_test.field_approved_publishing
+ - field.field.commerce_product.scheduler_api_product_test.field_approved_unpublishing
+ module:
+ - commerce
+ - path
+ - scheduler
+id: commerce_product.scheduler_api_product_test.default
+targetEntityType: commerce_product
+bundle: scheduler_api_product_test
+mode: default
+content:
+ created:
+ type: datetime_timestamp
+ weight: 8
+ region: content
+ settings: { }
+ third_party_settings: { }
+ field_approved_publishing:
+ type: boolean_checkbox
+ weight: 1
+ region: content
+ settings:
+ display_label: true
+ third_party_settings: { }
+ field_approved_unpublishing:
+ type: boolean_checkbox
+ weight: 2
+ region: content
+ settings:
+ display_label: true
+ third_party_settings: { }
+ path:
+ type: path
+ weight: 9
+ region: content
+ settings: { }
+ third_party_settings: { }
+ publish_on:
+ type: datetime_timestamp_no_default
+ weight: 4
+ region: content
+ settings: { }
+ third_party_settings: { }
+ scheduler_settings:
+ weight: 3
+ region: content
+ settings: { }
+ third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 10
+ region: content
+ third_party_settings: { }
+ stores:
+ type: commerce_entity_select
+ weight: 6
+ region: content
+ settings:
+ hide_single_entity: true
+ autocomplete_threshold: 7
+ autocomplete_size: 60
+ autocomplete_placeholder: ''
+ third_party_settings: { }
+ title:
+ type: string_textfield
+ weight: 0
+ region: content
+ settings:
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ uid:
+ type: entity_reference_autocomplete
+ weight: 7
+ region: content
+ settings:
+ match_operator: CONTAINS
+ match_limit: 10
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ unpublish_on:
+ type: datetime_timestamp_no_default
+ weight: 5
+ region: content
+ settings: { }
+ third_party_settings: { }
+hidden:
+ variations: true
diff --git a/tests/modules/scheduler_api_test/config/install/core.entity_form_display.media.scheduler_api_media_test.default.yml b/tests/modules/scheduler_api_test/config/install/core.entity_form_display.media.scheduler_api_media_test.default.yml
new file mode 100644
index 0000000..a226a64
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/core.entity_form_display.media.scheduler_api_media_test.default.yml
@@ -0,0 +1,95 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.media.scheduler_api_media_test.field_approved_publishing
+ - field.field.media.scheduler_api_media_test.field_approved_unpublishing
+ - field.field.media.scheduler_api_media_test.field_media_image_api
+ - image.style.thumbnail
+ - media.type.scheduler_api_media_test
+ module:
+ - image
+ - path
+ - scheduler
+id: media.scheduler_api_media_test.default
+targetEntityType: media
+bundle: scheduler_api_media_test
+mode: default
+content:
+ created:
+ type: datetime_timestamp
+ weight: 8
+ region: content
+ settings: { }
+ third_party_settings: { }
+ field_approved_publishing:
+ weight: 2
+ settings:
+ display_label: true
+ third_party_settings: { }
+ type: boolean_checkbox
+ region: content
+ field_approved_unpublishing:
+ weight: 3
+ settings:
+ display_label: true
+ third_party_settings: { }
+ type: boolean_checkbox
+ region: content
+ field_media_image_api:
+ weight: 1
+ settings:
+ progress_indicator: throbber
+ preview_image_style: thumbnail
+ third_party_settings: { }
+ type: image_image
+ region: content
+ name:
+ type: string_textfield
+ weight: 0
+ region: content
+ settings:
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ path:
+ type: path
+ weight: 9
+ region: content
+ settings: { }
+ third_party_settings: { }
+ publish_on:
+ type: datetime_timestamp_no_default
+ weight: 5
+ region: content
+ settings: { }
+ third_party_settings: { }
+ scheduler_settings:
+ weight: 4
+ region: content
+ settings: { }
+ third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 10
+ region: content
+ third_party_settings: { }
+ uid:
+ type: entity_reference_autocomplete
+ weight: 7
+ settings:
+ match_operator: CONTAINS
+ size: 60
+ placeholder: ''
+ match_limit: 10
+ region: content
+ third_party_settings: { }
+ unpublish_on:
+ type: datetime_timestamp_no_default
+ weight: 6
+ region: content
+ settings: { }
+ third_party_settings: { }
+hidden: { }
diff --git a/tests/modules/scheduler_api_test/config/install/core.entity_form_display.node.scheduler_api_test.default.yml b/tests/modules/scheduler_api_test/config/install/core.entity_form_display.node.scheduler_api_test.default.yml
index e4bb485..9f2db14 100644
--- a/tests/modules/scheduler_api_test/config/install/core.entity_form_display.node.scheduler_api_test.default.yml
+++ b/tests/modules/scheduler_api_test/config/install/core.entity_form_display.node.scheduler_api_test.default.yml
@@ -2,76 +2,93 @@ langcode: en
status: true
dependencies:
config:
- - field.field.node.scheduler_api_test.field_approved_publishing
- - field.field.node.scheduler_api_test.field_approved_unpublishing
- - node.type.scheduler_api_test
+ - field.field.node.scheduler_api_node_test.field_approved_publishing
+ - field.field.node.scheduler_api_node_test.field_approved_unpublishing
+ - node.type.scheduler_api_node_test
module:
- path
- scheduler
-id: node.scheduler_api_test.default
+id: node.scheduler_api_node_test.default
targetEntityType: node
-bundle: scheduler_api_test
+bundle: scheduler_api_node_test
mode: default
content:
- title:
- type: string_textfield
- weight: 0
- settings:
- size: 60
- placeholder: ''
- third_party_settings: { }
- langcode:
- type: language_select
- weight: 1
- settings: { }
- third_party_settings: { }
- uid:
- type: number
- weight: 2
- settings: { }
- third_party_settings: { }
created:
type: datetime_timestamp
- weight: 3
+ weight: 7
settings: { }
third_party_settings: { }
- promote:
+ region: content
+ field_approved_publishing:
type: boolean_checkbox
+ weight: 1
settings:
display_label: true
- weight: 4
third_party_settings: { }
- sticky:
+ region: content
+ field_approved_unpublishing:
type: boolean_checkbox
+ weight: 2
settings:
display_label: true
- weight: 5
third_party_settings: { }
+ region: content
path:
type: path
- weight: 6
+ weight: 10
settings: { }
third_party_settings: { }
- field_approved_publishing:
+ region: content
+ promote:
type: boolean_checkbox
- weight: 7
settings:
display_label: true
+ weight: 8
third_party_settings: { }
- field_approved_unpublishing:
+ region: content
+ publish_on:
+ type: datetime_timestamp_no_default
+ weight: 4
+ settings: { }
+ third_party_settings: { }
+ region: content
+ scheduler_settings:
+ weight: 3
+ region: content
+ settings: { }
+ third_party_settings: { }
+ status:
type: boolean_checkbox
- weight: 8
settings:
display_label: true
+ weight: 11
+ region: content
third_party_settings: { }
- publish_on:
- type: datetime_timestamp_no_default
+ sticky:
+ type: boolean_checkbox
+ settings:
+ display_label: true
weight: 9
+ third_party_settings: { }
+ region: content
+ title:
+ type: string_textfield
+ weight: 0
+ settings:
+ size: 60
+ placeholder: ''
+ third_party_settings: { }
+ region: content
+ uid:
+ type: options_select
+ weight: 6
settings: { }
third_party_settings: { }
+ region: content
unpublish_on:
type: datetime_timestamp_no_default
- weight: 10
+ weight: 5
settings: { }
third_party_settings: { }
+ region: content
hidden: { }
diff --git a/tests/modules/scheduler_api_test/config/install/core.entity_view_display.commerce_product.scheduler_api_product_test.default.yml b/tests/modules/scheduler_api_test/config/install/core.entity_view_display.commerce_product.scheduler_api_product_test.default.yml
new file mode 100644
index 0000000..2fe808d
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/core.entity_view_display.commerce_product.scheduler_api_product_test.default.yml
@@ -0,0 +1,54 @@
+uuid: a46cedee-917e-4576-9b67-ecbd2fc01501
+langcode: en
+status: true
+dependencies:
+ config:
+ - commerce_product.commerce_product_type.scheduler_api_product_test
+ - field.field.commerce_product.scheduler_api_product_test.field_approved_publishing
+ - field.field.commerce_product.scheduler_api_product_test.field_approved_unpublishing
+id: commerce_product.scheduler_api_product_test.default
+targetEntityType: commerce_product
+bundle: scheduler_api_product_test
+mode: default
+content:
+ field_approved_publishing:
+ type: boolean
+ weight: 2
+ region: content
+ label: inline
+ settings:
+ format: default
+ format_custom_false: ''
+ format_custom_true: ''
+ third_party_settings: { }
+ field_approved_unpublishing:
+ type: boolean
+ weight: 3
+ region: content
+ label: inline
+ settings:
+ format: default
+ format_custom_false: ''
+ format_custom_true: ''
+ third_party_settings: { }
+ title:
+ label: hidden
+ type: string
+ weight: 0
+ region: content
+ settings:
+ link_to_entity: false
+ third_party_settings: { }
+ variations:
+ type: entity_reference_entity_view
+ weight: 1
+ region: content
+ label: above
+ settings:
+ view_mode: default
+ link: false
+ third_party_settings: { }
+hidden:
+ created: true
+ stores: true
+ uid: true
diff --git a/tests/modules/scheduler_api_test/config/install/core.entity_view_display.media.scheduler_api_media_test.default.yml b/tests/modules/scheduler_api_test/config/install/core.entity_view_display.media.scheduler_api_media_test.default.yml
new file mode 100644
index 0000000..00e691f
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/core.entity_view_display.media.scheduler_api_media_test.default.yml
@@ -0,0 +1,57 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.media.scheduler_api_media_test.field_approved_publishing
+ - field.field.media.scheduler_api_media_test.field_approved_unpublishing
+ - field.field.media.scheduler_api_media_test.field_media_image_api
+ - image.style.large
+ - media.type.scheduler_api_media_test
+ module:
+ - image
+id: media.scheduler_api_media_test.default
+targetEntityType: media
+bundle: scheduler_api_media_test
+mode: default
+content:
+ field_approved_publishing:
+ weight: 2
+ label: inline
+ settings:
+ format: default
+ format_custom_false: ''
+ format_custom_true: ''
+ third_party_settings: { }
+ type: boolean
+ region: content
+ field_approved_unpublishing:
+ weight: 3
+ label: inline
+ settings:
+ format: default
+ format_custom_false: ''
+ format_custom_true: ''
+ third_party_settings: { }
+ type: boolean
+ region: content
+ field_media_image_api:
+ label: visually_hidden
+ weight: 1
+ settings:
+ image_style: large
+ image_link: ''
+ third_party_settings: { }
+ type: image
+ region: content
+ name:
+ type: string
+ weight: 0
+ region: content
+ label: above
+ settings:
+ link_to_entity: false
+ third_party_settings: { }
+hidden:
+ created: true
+ thumbnail: true
+ uid: true
diff --git a/tests/modules/scheduler_api_test/config/install/core.entity_view_display.node.scheduler_api_test.default.yml b/tests/modules/scheduler_api_test/config/install/core.entity_view_display.node.scheduler_api_test.default.yml
index 34011b1..f502c32 100644
--- a/tests/modules/scheduler_api_test/config/install/core.entity_view_display.node.scheduler_api_test.default.yml
+++ b/tests/modules/scheduler_api_test/config/install/core.entity_view_display.node.scheduler_api_test.default.yml
@@ -2,14 +2,14 @@ langcode: en
status: true
dependencies:
config:
- - field.field.node.scheduler_api_test.field_approved_publishing
- - field.field.node.scheduler_api_test.field_approved_unpublishing
- - node.type.scheduler_api_test
+ - field.field.node.scheduler_api_node_test.field_approved_publishing
+ - field.field.node.scheduler_api_node_test.field_approved_unpublishing
+ - node.type.scheduler_api_node_test
module:
- user
-id: node.scheduler_api_test.default
+id: node.scheduler_api_node_test.default
targetEntityType: node
-bundle: scheduler_api_test
+bundle: scheduler_api_node_test
mode: default
content:
field_approved_publishing:
@@ -21,18 +21,20 @@ content:
format_custom_false: ''
format_custom_true: ''
third_party_settings: { }
+ region: content
field_approved_unpublishing:
type: boolean
- weight: 1
+ weight: 2
label: inline
settings:
format: default
format_custom_false: ''
format_custom_true: ''
third_party_settings: { }
+ region: content
links:
weight: 0
+ region: content
settings: { }
third_party_settings: { }
-hidden:
- langcode: true
+hidden: { }
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.commerce_product.scheduler_api_product_test.field_approved_publishing.yml b/tests/modules/scheduler_api_test/config/install/field.field.commerce_product.scheduler_api_product_test.field_approved_publishing.yml
new file mode 100644
index 0000000..02d5f1a
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.field.commerce_product.scheduler_api_product_test.field_approved_publishing.yml
@@ -0,0 +1,22 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - commerce_product.commerce_product_type.scheduler_api_product_test
+ - field.storage.commerce_product.field_approved_publishing
+id: commerce_product.scheduler_api_product_test.field_approved_publishing
+field_name: field_approved_publishing
+entity_type: commerce_product
+bundle: scheduler_api_product_test
+label: 'Product Approved for Publishing'
+description: ''
+required: false
+translatable: false
+default_value:
+ -
+ value: 0
+default_value_callback: ''
+settings:
+ on_label: 'Yes'
+ off_label: 'No'
+field_type: boolean
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.commerce_product.scheduler_api_product_test.field_approved_unpublishing.yml b/tests/modules/scheduler_api_test/config/install/field.field.commerce_product.scheduler_api_product_test.field_approved_unpublishing.yml
new file mode 100644
index 0000000..ef0dca2
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.field.commerce_product.scheduler_api_product_test.field_approved_unpublishing.yml
@@ -0,0 +1,22 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - commerce_product.commerce_product_type.scheduler_api_product_test
+ - field.storage.commerce_product.field_approved_unpublishing
+id: commerce_product.scheduler_api_product_test.field_approved_unpublishing
+field_name: field_approved_unpublishing
+entity_type: commerce_product
+bundle: scheduler_api_product_test
+label: 'Product Approved for Unpublishing'
+description: ''
+required: false
+translatable: false
+default_value:
+ -
+ value: 0
+default_value_callback: ''
+settings:
+ on_label: 'Yes'
+ off_label: 'No'
+field_type: boolean
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_approved_publishing.yml b/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_approved_publishing.yml
new file mode 100644
index 0000000..6acad3e
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_approved_publishing.yml
@@ -0,0 +1,22 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.media.field_approved_publishing
+ - media.type.scheduler_api_media_test
+id: media.scheduler_api_media_test.field_approved_publishing
+field_name: field_approved_publishing
+entity_type: media
+bundle: scheduler_api_media_test
+label: 'Media Approved for Publishing'
+description: ''
+required: false
+translatable: false
+default_value:
+ -
+ value: 0
+default_value_callback: ''
+settings:
+ on_label: 'Yes'
+ off_label: 'No'
+field_type: boolean
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_approved_unpublishing.yml b/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_approved_unpublishing.yml
new file mode 100644
index 0000000..ea52fe4
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_approved_unpublishing.yml
@@ -0,0 +1,22 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.media.field_approved_unpublishing
+ - media.type.scheduler_api_media_test
+id: media.scheduler_api_media_test.field_approved_unpublishing
+field_name: field_approved_unpublishing
+entity_type: media
+bundle: scheduler_api_media_test
+label: 'Media Approved for Unpublishing'
+description: ''
+required: false
+translatable: false
+default_value:
+ -
+ value: 0
+default_value_callback: ''
+settings:
+ on_label: 'Yes'
+ off_label: 'No'
+field_type: boolean
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_media_image_api.yml b/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_media_image_api.yml
new file mode 100644
index 0000000..8d59c3c
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.field.media.scheduler_api_media_test.field_media_image_api.yml
@@ -0,0 +1,37 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.media.field_media_image_api
+ - media.type.scheduler_api_media_test
+ module:
+ - image
+id: media.scheduler_api_media_test.field_media_image_api
+field_name: field_media_image_api
+entity_type: media
+bundle: scheduler_api_media_test
+label: 'Image for API test'
+description: ''
+required: false
+translatable: true
+default_value: { }
+default_value_callback: ''
+settings:
+ file_directory: '[date:custom:Y]-[date:custom:m]'
+ file_extensions: 'png gif jpg jpeg'
+ max_filesize: ''
+ max_resolution: ''
+ min_resolution: ''
+ alt_field: true
+ alt_field_required: false
+ title_field: false
+ title_field_required: false
+ default_image:
+ uuid: ''
+ alt: ''
+ title: ''
+ width: null
+ height: null
+ handler: 'default:file'
+ handler_settings: { }
+field_type: image
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_publishing.yml b/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_publishing.yml
index 9d1a167..e2b2a85 100644
--- a/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_publishing.yml
+++ b/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_publishing.yml
@@ -3,14 +3,14 @@ status: true
dependencies:
config:
- field.storage.node.field_approved_publishing
- - node.type.scheduler_api_test
+ - node.type.scheduler_api_node_test
enforced:
module:
- scheduler_api_test
-id: node.scheduler_api_test.field_approved_publishing
+id: node.scheduler_api_node_test.field_approved_publishing
field_name: field_approved_publishing
entity_type: node
-bundle: scheduler_api_test
+bundle: scheduler_api_node_test
label: 'Approved for Publishing'
description: ''
required: false
diff --git a/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_unpublishing.yml b/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_unpublishing.yml
index 1902663..c8d837a 100644
--- a/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_unpublishing.yml
+++ b/tests/modules/scheduler_api_test/config/install/field.field.node.scheduler_api_test.field_approved_unpublishing.yml
@@ -3,14 +3,14 @@ status: true
dependencies:
config:
- field.storage.node.field_approved_unpublishing
- - node.type.scheduler_api_test
+ - node.type.scheduler_api_node_test
enforced:
module:
- scheduler_api_test
-id: node.scheduler_api_test.field_approved_unpublishing
+id: node.scheduler_api_node_test.field_approved_unpublishing
field_name: field_approved_unpublishing
entity_type: node
-bundle: scheduler_api_test
+bundle: scheduler_api_node_test
label: 'Approved for Unpublishing'
description: ''
required: false
diff --git a/tests/modules/scheduler_api_test/config/install/field.storage.commerce_product.field_approved_publishing.yml b/tests/modules/scheduler_api_test/config/install/field.storage.commerce_product.field_approved_publishing.yml
new file mode 100644
index 0000000..8221204
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.storage.commerce_product.field_approved_publishing.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - commerce_product
+ enforced:
+ module:
+ - scheduler_api_test
+id: commerce_product.field_approved_publishing
+field_name: field_approved_publishing
+entity_type: commerce_product
+type: boolean
+settings: { }
+module: core
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/tests/modules/scheduler_api_test/config/install/field.storage.commerce_product.field_approved_unpublishing.yml b/tests/modules/scheduler_api_test/config/install/field.storage.commerce_product.field_approved_unpublishing.yml
new file mode 100644
index 0000000..477fc60
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.storage.commerce_product.field_approved_unpublishing.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - commerce_product
+ enforced:
+ module:
+ - scheduler_api_test
+id: commerce_product.field_approved_unpublishing
+field_name: field_approved_unpublishing
+entity_type: commerce_product
+type: boolean
+settings: { }
+module: core
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/tests/modules/scheduler_api_test/config/install/field.storage.media.field_approved_publishing.yml b/tests/modules/scheduler_api_test/config/install/field.storage.media.field_approved_publishing.yml
new file mode 100644
index 0000000..478e559
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.storage.media.field_approved_publishing.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - media
+ enforced:
+ module:
+ - scheduler_api_test
+id: media.field_approved_publishing
+field_name: field_approved_publishing
+entity_type: media
+type: boolean
+settings: { }
+module: core
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/tests/modules/scheduler_api_test/config/install/field.storage.media.field_approved_unpublishing.yml b/tests/modules/scheduler_api_test/config/install/field.storage.media.field_approved_unpublishing.yml
new file mode 100644
index 0000000..5b054d3
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.storage.media.field_approved_unpublishing.yml
@@ -0,0 +1,20 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - media
+ enforced:
+ module:
+ - scheduler_api_test
+id: media.field_approved_unpublishing
+field_name: field_approved_unpublishing
+entity_type: media
+type: boolean
+settings: { }
+module: core
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/tests/modules/scheduler_api_test/config/install/field.storage.media.field_media_image_api.yml b/tests/modules/scheduler_api_test/config/install/field.storage.media.field_media_image_api.yml
new file mode 100644
index 0000000..28daadc
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/field.storage.media.field_media_image_api.yml
@@ -0,0 +1,32 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - file
+ - image
+ - media
+ enforced:
+ module:
+ - scheduler_api_test
+id: media.field_media_image_api
+field_name: field_media_image_api
+entity_type: media
+type: image
+settings:
+ default_image:
+ uuid: null
+ alt: ''
+ title: ''
+ width: null
+ height: null
+ target_type: file
+ display_field: false
+ display_default: false
+ uri_scheme: public
+module: image
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/tests/modules/scheduler_api_test/config/install/media.type.scheduler_api_media_test.yml b/tests/modules/scheduler_api_test/config/install/media.type.scheduler_api_media_test.yml
new file mode 100644
index 0000000..a451b7a
--- /dev/null
+++ b/tests/modules/scheduler_api_test/config/install/media.type.scheduler_api_media_test.yml
@@ -0,0 +1,32 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - scheduler
+ enforced:
+ module:
+ - scheduler_api_test
+third_party_settings:
+ scheduler:
+ expand_fieldset: always
+ fields_display_mode: fieldset
+ publish_enable: true
+ publish_past_date: schedule
+ publish_past_date_created: false
+ publish_required: false
+ publish_revision: false
+ publish_touch: false
+ show_message_after_update: true
+ unpublish_enable: true
+ unpublish_required: false
+ unpublish_revision: false
+id: scheduler_api_media_test
+label: 'Scheduler API Media Test'
+description: 'Image media type used in Scheduler API testing'
+source: image
+queue_thumbnail_downloads: false
+new_revision: false
+source_configuration:
+ source_field: field_media_image_api
+field_map:
+ name: name
diff --git a/tests/modules/scheduler_api_test/config/install/node.type.scheduler_api_test.yml b/tests/modules/scheduler_api_test/config/install/node.type.scheduler_api_test.yml
index 4e75ce1..28d667c 100644
--- a/tests/modules/scheduler_api_test/config/install/node.type.scheduler_api_test.yml
+++ b/tests/modules/scheduler_api_test/config/install/node.type.scheduler_api_test.yml
@@ -13,19 +13,21 @@ third_party_settings:
- main
parent: 'main:'
scheduler:
- expand_fieldset: when_required
+ expand_fieldset: always
fields_display_mode: fieldset
publish_enable: true
publish_past_date: schedule
+ publish_past_date_created: false
publish_required: false
publish_revision: false
publish_touch: false
+ show_message_after_update: true
unpublish_enable: true
unpublish_required: false
unpublish_revision: false
-name: 'Scheduler API test type'
-type: scheduler_api_test
-description: 'Used for Scheduler API testing'
+type: scheduler_api_node_test
+name: 'Scheduler API Node Test'
+description: 'Node content type used in Scheduler API testing'
help: ''
new_revision: false
preview_mode: 1
diff --git a/tests/modules/scheduler_api_test/scheduler_api_legacy_test/scheduler_api_legacy_test.info.yml b/tests/modules/scheduler_api_test/scheduler_api_legacy_test/scheduler_api_legacy_test.info.yml
new file mode 100644
index 0000000..1e1bf73
--- /dev/null
+++ b/tests/modules/scheduler_api_test/scheduler_api_legacy_test/scheduler_api_legacy_test.info.yml
@@ -0,0 +1,9 @@
+name: 'Scheduler API Legacy Test'
+type: module
+description: 'Sub-module containing the legacy hook implementations.'
+package: Testing
+core: 8.x
+core_version_requirement: ^8 || ^9
+dependencies:
+ - scheduler:scheduler
+ - scheduler_api_test:scheduler_api_test
diff --git a/tests/modules/scheduler_api_test/scheduler_api_legacy_test/scheduler_api_legacy_test.module b/tests/modules/scheduler_api_test/scheduler_api_legacy_test/scheduler_api_legacy_test.module
new file mode 100644
index 0000000..ac3fc9b
--- /dev/null
+++ b/tests/modules/scheduler_api_test/scheduler_api_legacy_test/scheduler_api_legacy_test.module
@@ -0,0 +1,195 @@
+<?php
+
+/**
+ * @file
+ * Legacy hook implementations for the Scheduler API Test module.
+ *
+ * Scheduler provides eight hook functions. As part of the enhancements to cater
+ * for non-node entities the hook functions had to be renamed to allow generic
+ * and specific variants and to maintain predictable expansion for any future
+ * entity type. For backwards-compatibility the original hook function names are
+ * maintined for node entities only, and this file provides test coverage.
+ */
+
+use Drupal\node\NodeInterface;
+
+/**
+ * Implements hook_scheduler_nid_list().
+ */
+function scheduler_api_legacy_test_scheduler_nid_list($action) {
+ $nids = [];
+ $request_time = \Drupal::time()->getRequestTime();
+ // Check to see what test nodes exist.
+ $results = _scheduler_api_test_get_entities('node');
+ foreach ($results as $nid => $node) {
+ // If publishing and this is the publish test node, set a date and add
+ // the node id to the list.
+ if ($action == 'publish' && $node->title->value == 'API TEST nid_list publish me') {
+ $node->set('publish_on', $request_time)->save();
+ $nids[] = $nid;
+ }
+ // If unpublishing and this is the unpublish test node, set a date and add
+ // the node id to the list.
+ if ($action == 'unpublish' && $node->title->value == 'API TEST nid_list unpublish me') {
+ $node->set('unpublish_on', $request_time)->save();
+ $nids[] = $nid;
+ }
+ }
+ return $nids;
+}
+
+/**
+ * Implements hook_scheduler_nid_list_alter().
+ */
+function scheduler_api_legacy_test_scheduler_nid_list_alter(&$nids, $action) {
+ $request_time = \Drupal::time()->getRequestTime();
+ $results = _scheduler_api_test_get_entities('node');
+ foreach ($results as $nid => $node) {
+ if ($action == 'publish' && $node->title->value == 'API TEST nid_list_alter do not publish me') {
+ // Remove the node id.
+ $nids = array_diff($nids, [$nid]);
+ }
+ if ($action == 'publish' && $node->title->value == 'API TEST nid_list_alter publish me') {
+ // Set a publish_on date and add the node id.
+ $node->set('publish_on', $request_time)->save();
+ $nids[] = $nid;
+ }
+ if ($action == 'unpublish' && $node->title->value == 'API TEST nid_list_alter do not unpublish me') {
+ // Remove the node id.
+ $nids = array_diff($nids, [$nid]);
+ }
+ if ($action == 'unpublish' && $node->title->value == 'API TEST nid_list_alter unpublish me') {
+ // Set an unpublish_on date and add the node id.
+ $node->set('unpublish_on', $request_time)->save();
+ $nids[] = $nid;
+ }
+ }
+}
+
+/**
+ * Implements hook_scheduler_allow_publishing().
+ */
+function scheduler_api_legacy_test_scheduler_allow_publishing(NodeInterface $node) {
+ // If there is no 'Approved for Publishing' field then allow publishing.
+ if (!isset($node->field_approved_publishing)) {
+ $allowed = TRUE;
+ }
+ else {
+ // Only publish nodes that have 'Approved for Publishing' set.
+ $allowed = $node->field_approved_publishing->value;
+ // If publication is denied then inform the user why.
+ if (!$allowed) {
+ \Drupal::messenger()->addMessage(t('%title is scheduled for publishing, but will not be published until approved.', ['%title' => $node->title->value]), 'status', FALSE);
+ // If the time is in the past it means that the action has been prevented.
+ // Write a dblog message to show this. Give a link to view the node but
+ // cater for no nid as the node may be new and not yet saved.
+ if ($node->publish_on->value <= \Drupal::time()->getRequestTime()) {
+ \Drupal::logger('scheduler_api_test')->warning('Publishing of "%title" is prevented until approved.', [
+ '%title' => $node->title->value,
+ 'link' => $node->id() ? $node->toLink(t('View node'))->toString() : '',
+ ]);
+ }
+ }
+ }
+ return $allowed;
+}
+
+/**
+ * Implements hook_scheduler_allow_unpublishing().
+ */
+function scheduler_api_legacy_test_scheduler_allow_unpublishing(NodeInterface $node) {
+ // If there is no 'Approved for Unpublishing' field then allow unpublishing.
+ if (!isset($node->field_approved_unpublishing)) {
+ $allowed = TRUE;
+ }
+ else {
+ // Only unpublish nodes that have 'Approved for Unpublishing' set.
+ $allowed = $node->field_approved_unpublishing->value;
+ // If unpublication is denied then inform the user why.
+ if (!$allowed) {
+ \Drupal::messenger()->addMessage(t('%title is scheduled for unpublishing, but will not be unpublished until approved.', ['%title' => $node->title->value]), 'status', FALSE);
+ // If the time is in the past it means that the action has been prevented.
+ // Write a dblog message to show this. Give a link to view the node but
+ // cater for no nid as the node may be new and not yet saved.
+ if ($node->unpublish_on->value <= \Drupal::time()->getRequestTime()) {
+ \Drupal::logger('scheduler_api_test')->warning('Unpublishing of "%title" is prevented until approved.', [
+ '%title' => $node->title->value,
+ 'link' => $node->id() ? $node->toLink(t('View node'))->toString() : '',
+ ]);
+ }
+ }
+ }
+ return $allowed;
+}
+
+/**
+ * Implements hook_scheduler_hide_publish_on_field().
+ */
+function scheduler_api_legacy_test_scheduler_hide_publish_on_field($form, $form_state, $node) {
+ // Hide the publish_on field if the node title contains orange or green.
+ if (stristr($node->title->value, 'orange legacy') || stristr($node->title->value, 'green legacy')) {
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The publish_on field is hidden for orange or green node titles.'), 'status', FALSE);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implements hook_scheduler_hide_unpublish_on_field().
+ */
+function scheduler_api_legacy_test_scheduler_hide_unpublish_on_field($form, $form_state, $node) {
+ // Hide the publish_on field if the node title contains yellow or green.
+ if (stristr($node->title->value, 'yellow legacy') || stristr($node->title->value, 'green legacy')) {
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The unpublish_on field is hidden for yellow or green node titles.'), 'status', FALSE);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implements hook_scheduler_publish_action().
+ */
+function scheduler_api_legacy_test_scheduler_publish_action($node) {
+ if (stristr($node->title->value, 'red legacy')) {
+ // Nodes with red in the title are simulated to cause a failure and should
+ // then be skipped by Scheduler.
+ $node->set('title', $node->title->value . ' - publishing failed in API test module');
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Red nodes should cause Scheduler to abandon publishing.'), 'status', FALSE);
+ return -1;
+ }
+ elseif (stristr($node->title->value, 'yellow legacy')) {
+ // Nodes with yellow in the title are simulated to be processed by this
+ // hook, and will not be published by Scheduler.
+ $node->set('title', $node->title->value . ' - publishing processed by API test module');
+ $node->setPublished();
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Yellow nodes should not have publishing processed by Scheduler.'), 'status', FALSE);
+ return 1;
+ }
+ return 0;
+}
+
+/**
+ * Implements hook_scheduler_unpublish_action().
+ */
+function scheduler_api_legacy_test_scheduler_unpublish_action($node) {
+ if (stristr($node->title->value, 'blue legacy')) {
+ // Nodes with blue in the title are simulated to cause a failure and should
+ // then be skipped by Scheduler.
+ $node->set('title', $node->title->value . ' - unpublishing failed in API test module');
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Blue nodes should cause Scheduler to abandon unpublishing.'), 'status', FALSE);
+ return -1;
+ }
+ if (stristr($node->title->value, 'orange legacy')) {
+ // Nodes with orange in the title are simulated to be processed by this
+ // hook, and will not be published by Scheduler.
+ $node->set('title', $node->title->value . ' - unpublishing processed by API test module');
+ $node->setUnpublished();
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Orange nodes should not have unpublishing processed by Scheduler.'), 'status', FALSE);
+ return 1;
+ }
+ return 0;
+}
diff --git a/tests/modules/scheduler_api_test/scheduler_api_test.install b/tests/modules/scheduler_api_test/scheduler_api_test.install
index 0c2c7b4..3dd6122 100644
--- a/tests/modules/scheduler_api_test/scheduler_api_test.install
+++ b/tests/modules/scheduler_api_test/scheduler_api_test.install
@@ -12,25 +12,33 @@
* module config and content on uninstalling. Plus, when developing this module
* and enabling it manually as a real module, this code is needed to clean up,
* otherwise a re-install fails.
+ *
+ * The entity types, custom fields and storage are deleted automatically by
+ * having 'enforced: module: - scheduler_api_test' in the config/install/*.yml
+ * files. However, we have to delete the actual entity content here.
*/
function scheduler_api_test_uninstall() {
- // Delete any content that may have been created for the custom node type.
- $nids_query = \Drupal::database()->select('node', 'n')
- ->fields('n', ['nid'])
- ->condition('n.type', ['scheduler_api_test'], 'IN')
- ->execute();
- if ($nids = $nids_query->fetchCol()) {
- $storage = \Drupal::entityTypeManager()->getStorage('node');
- $entities = $storage->loadMultiple($nids);
- $storage->delete($entities);
- \Drupal::messenger()->addMessage(t('@number %type node(s) have been deleted.', [
- '@number' => count($nids),
- '%type' => 'scheduler_api_test',
- ]));
+ // Delete all content for the custom api types.
+ $api_data = [
+ 'node' => ['db_id' => 'nid', 'type' => 'scheduler_api_node_test', 'bundle_field' => 'type'],
+ 'media' => ['db_id' => 'mid', 'type' => 'scheduler_api_media_test', 'bundle_field' => 'bundle'],
+ 'commerce_product' => ['db_id' => 'product_id', 'type' => 'scheduler_api_product_test', 'bundle_field' => 'type'],
+ ];
+ // @todo Re-write this using proper entity queries not database select().
+ foreach ($api_data as $entityTypeId => $values) {
+ $ids_query = \Drupal::database()->select($entityTypeId, 'a')
+ ->fields('a', ["{$values['db_id']}"])
+ ->condition($values['bundle_field'], ["{$values['type']}"], 'IN')
+ ->execute();
+ if ($ids = $ids_query->fetchCol()) {
+ $storage = \Drupal::entityTypeManager()->getStorage($entityTypeId);
+ $entities = $storage->loadMultiple($ids);
+ $entity = reset($entities);
+ $storage->delete($entities);
+ \Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural(count($ids), '1 %bundle has been deleted.', '@count %bundle have been deleted.', [
+ '%bundle' => $entity->{$values['bundle_field']}->entity->label(),
+ ]));
+ }
}
-
- // The content type, custom fields and storage are deleted automatically by
- // having 'enforced: module: - scheduler_api_test' in the
- // config/install/*.yml files.
}
diff --git a/tests/modules/scheduler_api_test/scheduler_api_test.module b/tests/modules/scheduler_api_test/scheduler_api_test.module
index 30e035a..547f999 100644
--- a/tests/modules/scheduler_api_test/scheduler_api_test.module
+++ b/tests/modules/scheduler_api_test/scheduler_api_test.module
@@ -3,89 +3,214 @@
/**
* @file
* Hook implementations of the Scheduler API Test module.
+ *
+ * Scheduler provides eight hook functions. Each has a non-specific version with
+ * no _{type}_ in the name, which is invoked for all entity types, and a version
+ * with _{type}_ in the name, which is invoked only when that entity types is
+ * being processed. Hence for complete test coverage this module has eight plain
+ * implementations, eight implementations for Nodes and eight for Media.
*/
+use Drupal\commerce_product\Entity\Product;
+use Drupal\commerce_product\Entity\ProductInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\media\Entity\Media;
+use Drupal\media\MediaInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
- * Implements hook_scheduler_nid_list().
+ * Helper function to return all entities of a given type.
*/
-function scheduler_api_test_scheduler_nid_list($action) {
- $nids = [];
+function _scheduler_api_test_get_entities($entityTypeId) {
+ switch ($entityTypeId) {
+ case 'node':
+ $results = Node::loadMultiple(\Drupal::entityQuery('node')->accessCheck(FALSE)->execute());
+ break;
- // Check to see what test nodes exist.
- $query = \Drupal::entityQuery('node');
- $nodes = Node::loadMultiple($query->execute());
+ case 'media':
+ $results = Media::loadMultiple(\Drupal::entityQuery('media')->accessCheck(FALSE)->execute());
+ break;
+
+ case 'commerce_product':
+ $results = Product::loadMultiple(\Drupal::entityQuery('commerce_product')->accessCheck(FALSE)->execute());
+ break;
+
+ default:
+ throw new \Exception("Entity type id '{$entityTypeId}' is unrecognosed in _scheduler_api_test_get_entities().");
+ }
+ return $results;
+}
+
+/**
+ * Implements hook_scheduler_list().
+ */
+function scheduler_api_test_scheduler_list($process, $entityTypeId) {
+ $ids = [];
+ $request_time = \Drupal::time()->getRequestTime();
+ $results = _scheduler_api_test_get_entities($entityTypeId);
+ foreach ($results as $id => $entity) {
+ // If publishing and this is the 'publish me' test entity, set the date and
+ // add the id to the list.
+ if ($process == 'publish' && !$entity->isPublished() && $entity->label() == "Pink $entityTypeId list publish me") {
+ $entity->set('publish_on', $request_time)->save();
+ $ids[] = $id;
+ }
+ // If unpublishing and this is the 'unpublish me' test entity, set the date
+ // and add the id to the list.
+ if ($process == 'unpublish' && $entity->isPublished() && $entity->label() == "Pink $entityTypeId list unpublish me") {
+ $entity->set('unpublish_on', $request_time)->save();
+ $ids[] = $id;
+ }
+ }
+ return $ids;
+}
+
+/**
+ * Implements hook_scheduler_node_list().
+ */
+function scheduler_api_test_scheduler_node_list($process, $entityTypeId) {
+ $ids = [];
+ $request_time = \Drupal::time()->getRequestTime();
+ $results = _scheduler_api_test_get_entities($entityTypeId);
+ foreach ($results as $id => $entity) {
+ // If publishing and this is the 'publish me' test entity, set the date and
+ // add the id to the list.
+ if ($process == 'publish' && !$entity->isPublished() && $entity->label() == "Purple $entityTypeId list publish me") {
+ $entity->set('publish_on', $request_time)->save();
+ $ids[] = $id;
+ }
+ // If unpublishing and this is the 'unpublish me' test entity, set the date
+ // and add the id to the list.
+ if ($process == 'unpublish' && $entity->isPublished() && $entity->label() == "Purple $entityTypeId list unpublish me") {
+ $entity->set('unpublish_on', $request_time)->save();
+ $ids[] = $id;
+ }
+ }
+ return $ids;
+}
+
+/**
+ * Implements hook_scheduler_media_list().
+ */
+function scheduler_api_test_scheduler_media_list($process, $entityTypeId) {
+ // This hook does exactly the same as the node version, so re-use that.
+ return scheduler_api_test_scheduler_node_list($process, $entityTypeId);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_list().
+ */
+function scheduler_api_test_scheduler_commerce_product_list($process, $entityTypeId) {
+ // This hook does exactly the same as the node version, so re-use that.
+ return scheduler_api_test_scheduler_node_list($process, $entityTypeId);
+}
+
+/**
+ * Implements hook_scheduler_list_alter().
+ */
+function scheduler_api_test_scheduler_list_alter(&$ids, $process, $entityTypeId) {
$request_time = \Drupal::time()->getRequestTime();
- foreach ($nodes as $nid => $node) {
- // If publishing and this is the publish test node, set a date and add
- // the node id to the list.
- if ($action == 'publish' && $node->title->value == 'API TEST nid_list publish me') {
- $node->set('publish_on', $request_time)->save();
- $nids[] = $nid;
+ $results = _scheduler_api_test_get_entities($entityTypeId);
+ foreach ($results as $id => $entity) {
+ if ($process == 'publish' && $entity->label() == "Pink $entityTypeId list_alter do not publish me") {
+ // Remove the id.
+ $ids = array_diff($ids, [$id]);
+ }
+ if ($process == 'publish' && $entity->label() == "Pink $entityTypeId list_alter publish me") {
+ // Set a publish_on date and add the id.
+ $entity->set('publish_on', $request_time)->save();
+ $ids[] = $id;
}
- // If unpublishing and this is the unpublish test node, set a date and add
- // the node id to the list.
- if ($action == 'unpublish' && $node->title->value == 'API TEST nid_list unpublish me') {
- $node->set('unpublish_on', $request_time)->save();
- $nids[] = $nid;
+ if ($process == 'unpublish' && $entity->label() == "Pink $entityTypeId list_alter do not unpublish me") {
+ // Remove the id.
+ $ids = array_diff($ids, [$id]);
+ }
+ if ($process == 'unpublish' && $entity->label() == "Pink $entityTypeId list_alter unpublish me") {
+ // Set an unpublish_on date and add the id.
+ $entity->set('unpublish_on', $request_time)->save();
+ $ids[] = $id;
}
}
- return $nids;
}
/**
- * Implements hook_scheduler_nid_list_alter().
+ * Implements hook_scheduler_node_list_alter().
*/
-function scheduler_api_test_scheduler_nid_list_alter(&$nids, $action) {
- $query = \Drupal::entityQuery('node');
- $nodes = Node::loadMultiple($query->execute());
+function scheduler_api_test_scheduler_node_list_alter(&$ids, $process, $entityTypeId) {
$request_time = \Drupal::time()->getRequestTime();
- foreach ($nodes as $nid => $node) {
- if ($action == 'publish' && $node->title->value == 'API TEST nid_list_alter do not publish me') {
- // Remove the node id.
- $nids = array_diff($nids, [$nid]);
+ $results = _scheduler_api_test_get_entities($entityTypeId);
+ foreach ($results as $id => $entity) {
+ if ($process == 'publish' && $entity->label() == "Purple $entityTypeId list_alter do not publish me") {
+ // Remove the id.
+ $ids = array_diff($ids, [$id]);
}
- if ($action == 'publish' && $node->title->value == 'API TEST nid_list_alter publish me') {
- // Set a publish_on date and add the node id.
- $node->set('publish_on', $request_time)->save();
- $nids[] = $nid;
+ if ($process == 'publish' && $entity->label() == "Purple $entityTypeId list_alter publish me") {
+ // Set a publish_on date and add the id.
+ $entity->set('publish_on', $request_time)->save();
+ $ids[] = $id;
}
- if ($action == 'unpublish' && $node->title->value == 'API TEST nid_list_alter do not unpublish me') {
- // Remove the node id.
- $nids = array_diff($nids, [$nid]);
+ if ($process == 'unpublish' && $entity->label() == "Purple $entityTypeId list_alter do not unpublish me") {
+ // Remove the id.
+ $ids = array_diff($ids, [$id]);
}
- if ($action == 'unpublish' && $node->title->value == 'API TEST nid_list_alter unpublish me') {
- // Set an unpublish_on date and add the node id.
- $node->set('unpublish_on', $request_time)->save();
- $nids[] = $nid;
+ if ($process == 'unpublish' && $entity->label() == "Purple $entityTypeId list_alter unpublish me") {
+ // Set an unpublish_on date and add the id.
+ $entity->set('unpublish_on', $request_time)->save();
+ $ids[] = $id;
}
}
- return $nids;
}
/**
- * Implements hook_scheduler_allow_publishing().
+ * Implements hook_scheduler_media_list_alter().
*/
-function scheduler_api_test_scheduler_allow_publishing(NodeInterface $node) {
- // If there is no 'Approved for Publishing' field then allow publishing.
- if (!isset($node->field_approved_publishing)) {
+function scheduler_api_test_scheduler_media_list_alter(&$ids, $process, $entityTypeId) {
+ // This hook does exactly the same as the node version, so re-use that.
+ scheduler_api_test_scheduler_node_list_alter($ids, $process, $entityTypeId);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_list_alter().
+ */
+function scheduler_api_test_scheduler_commerce_product_list_alter(&$ids, $process, $entityTypeId) {
+ // This hook does exactly the same as the node version, so re-use that.
+ scheduler_api_test_scheduler_node_list_alter($ids, $process, $entityTypeId);
+}
+
+/**
+ * Implements hook_scheduler_publishing_allowed().
+ */
+function scheduler_api_test_scheduler_publishing_allowed(EntityInterface $entity) {
+ // @todo Fill in this function and add test coverage.
+}
+
+/**
+ * Generic function to check if the entity is allowed to be published.
+ */
+function _api_publishing_allowed(EntityInterface $entity) {
+ // If there is no 'Approved for Publishing' field or we are not dealing with
+ // an entity designed for this test then allow publishing.
+ if (!isset($entity->field_approved_publishing) || !stristr($entity->label(), "blue {$entity->getEntityTypeId()}")) {
$allowed = TRUE;
}
else {
- // Only publish nodes that have 'Approved for Publishing' set.
- $allowed = $node->field_approved_publishing->value;
- // If publication is denied then inform the user why.
+ // Only publish entities that have 'Approved for Publishing' set.
+ $allowed = $entity->field_approved_publishing->value;
+ // If publishing is denied then inform the user why.
if (!$allowed) {
- \Drupal::messenger()->addMessage(t('%title is scheduled for publishing, but will not be published until approved.', ['%title' => $node->title->value]), 'status', FALSE);
+ // Show a message when the entity is saved.
+ \Drupal::messenger()->addMessage(t('%title is scheduled for publishing @publish_time, but will not be published until approved.', [
+ '%title' => $entity->label(),
+ '@publish_time' => \Drupal::service('date.formatter')->format($entity->publish_on->value, 'long'),
+ ]), 'status', FALSE);
// If the time is in the past it means that the action has been prevented.
- // Write a dblog message to show this. Give a link to view the node but
- // cater for no nid as the node may be new and not yet saved.
- if ($node->publish_on->value <= \Drupal::time()->getRequestTime()) {
+ // Write a dblog message to show this. Give a link to view the entity but
+ // cater for no id as the entity may be new and not yet saved.
+ if ($entity->publish_on->value <= \Drupal::time()->getRequestTime()) {
\Drupal::logger('scheduler_api_test')->warning('Publishing of "%title" is prevented until approved.', [
- '%title' => $node->title->value,
- 'link' => $node->id() ? $node->toLink(t('View node'))->toString() : '',
+ '%title' => $entity->label(),
+ 'link' => !empty($entity->id()) ? $entity->toLink(t('View'))->toString() : '',
]);
}
}
@@ -94,26 +219,62 @@ function scheduler_api_test_scheduler_allow_publishing(NodeInterface $node) {
}
/**
- * Implements hook_scheduler_allow_unpublishing().
+ * Implements hook_scheduler_node_publishing_allowed().
+ */
+function scheduler_api_test_scheduler_node_publishing_allowed(NodeInterface $node) {
+ // Use the generic publishing_allowed helper function.
+ return _api_publishing_allowed($node);
+}
+
+/**
+ * Implements hook_scheduler_media_publishing_allowed().
+ */
+function scheduler_api_test_scheduler_media_publishing_allowed(MediaInterface $media) {
+ // Use the generic publishing_allowed helper function.
+ return _api_publishing_allowed($media);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_publishing_allowed().
+ */
+function scheduler_api_test_scheduler_commerce_product_publishing_allowed(ProductInterface $product) {
+ // Use the generic publishing_allowed helper function.
+ return _api_publishing_allowed($product);
+}
+
+/**
+ * Implements hook_scheduler_unpublishing_allowed().
*/
-function scheduler_api_test_scheduler_allow_unpublishing(NodeInterface $node) {
- // If there is no 'Approved for Unpublishing' field then allow unpublishing.
- if (!isset($node->field_approved_unpublishing)) {
+function scheduler_api_test_scheduler_unpublishing_allowed(EntityInterface $entity) {
+ // @todo Fill in this function and add test coverage.
+}
+
+/**
+ * Generic function to check if the entity is allowed to be unpublished.
+ */
+function _api_unpublishing_allowed(EntityInterface $entity) {
+ // If there is no 'Approved for Unpublishing' field or we are not dealing with
+ // an entity designed for this test then allow unpublishing.
+ if (!isset($entity->field_approved_unpublishing) || !stristr($entity->label(), "red {$entity->getEntityTypeId()}")) {
$allowed = TRUE;
}
else {
- // Only unpublish nodes that have 'Approved for Unpublishing' set.
- $allowed = $node->field_approved_unpublishing->value;
- // If unpublication is denied then inform the user why.
+ // Only unpublish entities that have 'Approved for Unpublishing' set.
+ $allowed = $entity->field_approved_unpublishing->value;
+ // If unpublishing is denied then inform the user why.
if (!$allowed) {
- \Drupal::messenger()->addMessage(t('%title is scheduled for unpublishing, but will not be unpublished until approved.', ['%title' => $node->title->value]), 'status', FALSE);
+ // Show a message when the entity is saved.
+ \Drupal::messenger()->addMessage(t('%title is scheduled for unpublishing @unpublish_time, but will not be unpublished until approved.', [
+ '%title' => $entity->label(),
+ '@unpublish_time' => \Drupal::service('date.formatter')->format($entity->unpublish_on->value, 'long'),
+ ]), 'status', FALSE);
// If the time is in the past it means that the action has been prevented.
- // Write a dblog message to show this. Give a link to view the node but
- // cater for no nid as the node may be new and not yet saved.
- if ($node->unpublish_on->value <= \Drupal::time()->getRequestTime()) {
+ // Write a dblog message to show this. Give a link to view the entity but
+ // cater for no id as the entity may be new and not yet saved.
+ if ($entity->unpublish_on->value <= \Drupal::time()->getRequestTime()) {
\Drupal::logger('scheduler_api_test')->warning('Unpublishing of "%title" is prevented until approved.', [
- '%title' => $node->title->value,
- 'link' => $node->id() ? $node->toLink(t('View node'))->toString() : '',
+ '%title' => $entity->label(),
+ 'link' => !empty($entity->id()) ? $entity->toLink(t('View'))->toString() : '',
]);
}
}
@@ -122,12 +283,36 @@ function scheduler_api_test_scheduler_allow_unpublishing(NodeInterface $node) {
}
/**
- * Implements hook_scheduler_hide_publish_on_field().
+ * Implements hook_scheduler_node_unpublishing_allowed().
+ */
+function scheduler_api_test_scheduler_node_unpublishing_allowed(NodeInterface $node) {
+ // Use the generic unpublishing_allowed helper function.
+ return _api_unpublishing_allowed($node);
+}
+
+/**
+ * Implements hook_scheduler_media_unpublishing_allowed().
+ */
+function scheduler_api_test_scheduler_media_unpublishing_allowed(MediaInterface $media) {
+ // Use the generic unpublishing_allowed helper function.
+ return _api_unpublishing_allowed($media);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_unpublishing_allowed().
*/
-function scheduler_api_test_scheduler_hide_publish_on_field($form, $form_state, $node) {
- // Hide the publish_on field if the node title contains orange or green.
- if (stristr($node->title->value, 'orange') || stristr($node->title->value, 'green')) {
- \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The publish_on field is hidden for orange or green node titles.'), 'status', FALSE);
+function scheduler_api_test_scheduler_commerce_product_unpublishing_allowed(ProductInterface $product) {
+ // Use the generic unpublishing_allowed helper function.
+ return _api_unpublishing_allowed($product);
+}
+
+/**
+ * Implements hook_scheduler_hide_publish_date().
+ */
+function scheduler_api_test_scheduler_hide_publish_date($form, $form_state, $entity) {
+ // Hide the publish_on field if the title contains 'orange {type}'.
+ if (stristr($entity->label(), "orange {$entity->getEntityTypeId()}")) {
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The publish_on field is hidden for orange.'), 'status', FALSE);
return TRUE;
}
else {
@@ -136,12 +321,12 @@ function scheduler_api_test_scheduler_hide_publish_on_field($form, $form_state,
}
/**
- * Implements hook_scheduler_hide_unpublish_on_field().
+ * Generic function to hide the publish_on date field.
*/
-function scheduler_api_test_scheduler_hide_unpublish_on_field($form, $form_state, $node) {
- // Hide the publish_on field if the node title contains yellow or green.
- if (stristr($node->title->value, 'yellow') || stristr($node->title->value, 'green')) {
- \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The unpublish_on field is hidden for yellow or green node titles.'), 'status', FALSE);
+function _api_hide_publish_date($form, $form_state, $entity) {
+ // Hide the publish_on field if the title contains 'green {type}'.
+ if (stristr($entity->label(), "green {$entity->getEntityTypeId()}")) {
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The publish_on field is hidden for green.'), 'status', FALSE);
return TRUE;
}
else {
@@ -150,45 +335,187 @@ function scheduler_api_test_scheduler_hide_unpublish_on_field($form, $form_state
}
/**
- * Implements hook_scheduler_publish_action().
+ * Implements hook_scheduler_node_hide_publish_date().
*/
-function scheduler_api_test_scheduler_publish_action($node) {
- if (stristr($node->title->value, 'red')) {
- // Nodes with red in the title are simulated to cause a failure and should
- // then be skipped by Scheduler.
- $node->set('title', $node->title->value . ' - publishing failed in API test module');
- \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Red nodes should cause Scheduler to abandon publishing.'), 'status', FALSE);
+function scheduler_api_test_scheduler_node_hide_publish_date($form, $form_state, $entity) {
+ // Use the generic hide_publish_date helper function.
+ return _api_hide_publish_date($form, $form_state, $entity);
+}
+
+/**
+ * Implements hook_scheduler_media_hide_publish_date().
+ */
+function scheduler_api_test_scheduler_media_hide_publish_date($form, $form_state, $entity) {
+ // Use the generic hide_publish_date helper function.
+ return _api_hide_publish_date($form, $form_state, $entity);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_hide_publish_date().
+ */
+function scheduler_api_test_scheduler_commerce_product_hide_publish_date($form, $form_state, $entity) {
+ // Use the generic hide_publish_date helper function.
+ return _api_hide_publish_date($form, $form_state, $entity);
+}
+
+/**
+ * Implements hook_scheduler_hide_unpublish_date().
+ */
+function scheduler_api_test_scheduler_hide_unpublish_date($form, $form_state, $entity) {
+ // Hide the unpublish_on field if the title contains 'yellow {type}'.
+ if (stristr($entity->label(), "yellow {$entity->getEntityTypeId()}")) {
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The unpublish_on field is hidden for yellow.'), 'status', FALSE);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Generic function to hide the unpublish_on date field.
+ */
+function _api_hide_unpublish_date($form, $form_state, $entity) {
+ // Hide the unpublish_on field if the title contains 'green {type}'.
+ if (stristr($entity->label(), "green {$entity->getEntityTypeId()}")) {
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: The unpublish_on field is hidden for green.'), 'status', FALSE);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Implements hook_scheduler_node_hide_unpublish_date().
+ */
+function scheduler_api_test_scheduler_node_hide_unpublish_date($form, $form_state, $entity) {
+ // Use the generic hide_unpublish_date helper function.
+ return _api_hide_unpublish_date($form, $form_state, $entity);
+}
+
+/**
+ * Implements hook_scheduler_media_hide_unpublish_date().
+ */
+function scheduler_api_test_scheduler_media_hide_unpublish_date($form, $form_state, $entity) {
+ // Use the generic hide_unpublish_date helper function.
+ return _api_hide_unpublish_date($form, $form_state, $entity);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_hide_unpublish_date().
+ */
+function scheduler_api_test_scheduler_commerce_product_hide_unpublish_date($form, $form_state, $entity) {
+ // Use the generic hide_unpublish_date helper function.
+ return _api_hide_unpublish_date($form, $form_state, $entity);
+}
+
+/**
+ * Implements hook_scheduler_publish_process().
+ */
+function scheduler_api_test_scheduler_publish_process(EntityInterface $entity) {
+ // Any entity with 'red {type}' in the title is simulated to cause a failure
+ // and should then be skipped by Scheduler.
+ if (stristr($entity->label(), "red {$entity->getEntityTypeId()}")) {
+ $label_field = $entity->getEntityType()->get('entity_keys')['label'];
+ $entity->set($label_field, $entity->label() . ' - publishing failed in API test module')->save();
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Red should cause Scheduler to abandon publishing.'), 'status', FALSE);
return -1;
}
- elseif (stristr($node->title->value, 'yellow')) {
- // Nodes with yellow in the title are simulated to be processed by this
- // hook, and will not be published by Scheduler.
- $node->set('title', $node->title->value . ' - publishing processed by API test module');
- $node->setPublished();
- \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Yellow nodes should not have publishing processed by Scheduler.'), 'status', FALSE);
+ return 0;
+}
+
+/**
+ * Generic function to process third-party publishing.
+ */
+function _api_publish_process(EntityInterface $entity) {
+ // Entities with 'yellow {type}' in the title are simulated to be processed
+ // by this hook, and will not be published by Scheduler.
+ if (stristr($entity->label(), "yellow {$entity->getEntityTypeId()}")) {
+ $label_field = $entity->getEntityType()->get('entity_keys')['label'];
+ $entity->set($label_field, $entity->label() . ' - publishing processed by API test module');
+ $entity->setPublished()->save();
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Yellow should not have publishing processed by Scheduler.'), 'status', FALSE);
return 1;
}
return 0;
}
/**
- * Implements hook_scheduler_unpublish_action().
+ * Implements hook_scheduler_node_publish_process().
*/
-function scheduler_api_test_scheduler_unpublish_action($node) {
- if (stristr($node->title->value, 'blue')) {
- // Nodes with blue in the title are simulated to cause a failure and should
- // then be skipped by Scheduler.
- $node->set('title', $node->title->value . ' - unpublishing failed in API test module');
- \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Blue nodes should cause Scheduler to abandon unpublishing.'), 'status', FALSE);
+function scheduler_api_test_scheduler_node_publish_process(NodeInterface $node) {
+ // Use the generic publish_process helper function.
+ return _api_publish_process($node);
+}
+
+/**
+ * Implements hook_scheduler_media_publish_process().
+ */
+function scheduler_api_test_scheduler_media_publish_process(MediaInterface $media) {
+ // Use the generic publish_process helper function.
+ return _api_publish_process($media);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_publish_process().
+ */
+function scheduler_api_test_scheduler_commerce_product_publish_process(ProductInterface $product) {
+ // Use the generic publish_process helper function.
+ return _api_publish_process($product);
+}
+
+/**
+ * Implements hook_scheduler_unpublish_process().
+ */
+function scheduler_api_test_scheduler_unpublish_process(EntityInterface $entity) {
+ // Any entity with 'blue {type}' in the title is simulated to cause a failure
+ // and should then be skipped by Scheduler.
+ if (stristr($entity->label(), "blue {$entity->getEntityTypeId()}")) {
+ $label_field = $entity->getEntityType()->get('entity_keys')['label'];
+ $entity->set($label_field, $entity->label() . ' - unpublishing failed in API test module')->save();
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Blue should cause Scheduler to abandon unpublishing.'), 'status', FALSE);
return -1;
}
- if (stristr($node->title->value, 'orange')) {
- // Nodes with orange in the title are simulated to be processed by this
- // hook, and will not be published by Scheduler.
- $node->set('title', $node->title->value . ' - unpublishing processed by API test module');
- $node->setUnpublished();
- \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Orange nodes should not have unpublishing processed by Scheduler.'), 'status', FALSE);
+ return 0;
+}
+
+/**
+ * Generic function to process third-party unpublishing.
+ */
+function _api_unpublish_process(EntityInterface $entity) {
+ // Entities with 'orange {type}' in the title are simulated to be processed by
+ // this hook, and will not be unpublished by Scheduler.
+ if (stristr($entity->label(), "orange {$entity->getEntityTypeId()}")) {
+ $label_field = $entity->getEntityType()->get('entity_keys')['label'];
+ $entity->set($label_field, $entity->label() . ' - unpublishing processed by API test module')->save();
+ $entity->setUnpublished();
+ \Drupal::messenger()->addMessage(t('Scheduler_Api_Test: Orange should not have unpublishing processed by Scheduler.'), 'status', FALSE);
return 1;
}
return 0;
}
+
+/**
+ * Implements hook_scheduler_node_unpublish_process().
+ */
+function scheduler_api_test_scheduler_node_unpublish_process(NodeInterface $node) {
+ // Use the generic unpublish_process helper function.
+ return _api_unpublish_process($node);
+}
+
+/**
+ * Implements hook_scheduler_media_unpublish_process().
+ */
+function scheduler_api_test_scheduler_media_unpublish_process(MediaInterface $media) {
+ // Use the generic unpublish_process helper function.
+ return _api_unpublish_process($media);
+}
+
+/**
+ * Implements hook_scheduler_commerce_product_unpublish_process().
+ */
+function scheduler_api_test_scheduler_commerce_product_unpublish_process(ProductInterface $product) {
+ // Use the generic unpublish_process helper function.
+ return _api_unpublish_process($product);
+}
diff --git a/tests/modules/scheduler_api_test/src/EventSubscriber.php b/tests/modules/scheduler_api_test/src/EventSubscriber.php
index 4efdc6c..9f425cc 100644
--- a/tests/modules/scheduler_api_test/src/EventSubscriber.php
+++ b/tests/modules/scheduler_api_test/src/EventSubscriber.php
@@ -2,8 +2,10 @@
namespace Drupal\scheduler_api_test;
+use Drupal\scheduler\Event\SchedulerCommerceProductEvents;
+use Drupal\scheduler\Event\SchedulerMediaEvents;
+use Drupal\scheduler\Event\SchedulerNodeEvents;
use Drupal\scheduler\SchedulerEvent;
-use Drupal\scheduler\SchedulerEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@@ -12,15 +14,18 @@ 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().
+ * scheduler_entity_presave().
*
- * The tests use the standard 'sticky' and 'promote' fields as a simple way to
- * check the processing. Use extra conditional checks on $node->isPublished() to
+ * The node event tests use the 'sticky' and 'promote' fields as a simple way to
+ * check the processing. There are extra conditional checks on isPublished() to
* make the tests stronger so they fail if the calls are in the wrong place.
*
+ * The media tests cannot use 'sticky' and 'promote' as these fields do not
+ * exist, so the media name is altered instead.
+ *
* To allow this API test module to be enabled interactively (for development
* and testing) we must avoid unwanted side-effects on other non-test nodes.
- * This is done simply by checking that the node title starts with 'API TEST'.
+ * This is done simply by checking that the titles start with 'API TEST'.
*
* @group scheduler_api_test
*/
@@ -30,23 +35,45 @@ class EventSubscriber implements EventSubscriberInterface {
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
+
+ // Initialise the array to avoid 'variable is undefined' phpcs error.
+ $events = [];
+
// The values in the arrays give the function names below.
- $events[SchedulerEvents::PRE_PUBLISH][] = ['apiTestPrePublish'];
- $events[SchedulerEvents::PUBLISH][] = ['apiTestPublish'];
- $events[SchedulerEvents::PRE_UNPUBLISH][] = ['apiTestPreUnpublish'];
- $events[SchedulerEvents::UNPUBLISH][] = ['apiTestUnpublish'];
- $events[SchedulerEvents::PRE_PUBLISH_IMMEDIATELY][] = ['apiTestPrePublishImmediately'];
- $events[SchedulerEvents::PUBLISH_IMMEDIATELY][] = ['apiTestPublishImmediately'];
+ // These six events are the originals, dispatched for Nodes.
+ $events[SchedulerNodeEvents::PRE_PUBLISH][] = ['apiTestNodePrePublish'];
+ $events[SchedulerNodeEvents::PUBLISH][] = ['apiTestNodePublish'];
+ $events[SchedulerNodeEvents::PRE_UNPUBLISH][] = ['apiTestNodePreUnpublish'];
+ $events[SchedulerNodeEvents::UNPUBLISH][] = ['apiTestNodeUnpublish'];
+ $events[SchedulerNodeEvents::PRE_PUBLISH_IMMEDIATELY][] = ['apiTestNodePrePublishImmediately'];
+ $events[SchedulerNodeEvents::PUBLISH_IMMEDIATELY][] = ['apiTestNodePublishImmediately'];
+
+ // These six events are dispatched for Media entity types only.
+ $events[SchedulerMediaEvents::PRE_PUBLISH][] = ['apiTestMediaPrePublish'];
+ $events[SchedulerMediaEvents::PUBLISH][] = ['apiTestMediaPublish'];
+ $events[SchedulerMediaEvents::PRE_UNPUBLISH][] = ['apiTestMediaPreUnpublish'];
+ $events[SchedulerMediaEvents::UNPUBLISH][] = ['apiTestMediaUnpublish'];
+ $events[SchedulerMediaEvents::PRE_PUBLISH_IMMEDIATELY][] = ['apiTestMediaPrePublishImmediately'];
+ $events[SchedulerMediaEvents::PUBLISH_IMMEDIATELY][] = ['apiTestMediaPublishImmediately'];
+
+ // These six events are dispatched for Product entity types only.
+ $events[SchedulerCommerceproductEvents::PRE_PUBLISH][] = ['apiTestProductPrePublish'];
+ $events[SchedulerCommerceproductEvents::PUBLISH][] = ['apiTestProductPublish'];
+ $events[SchedulerCommerceproductEvents::PRE_UNPUBLISH][] = ['apiTestProductPreUnpublish'];
+ $events[SchedulerCommerceproductEvents::UNPUBLISH][] = ['apiTestProductUnpublish'];
+ $events[SchedulerCommerceproductEvents::PRE_PUBLISH_IMMEDIATELY][] = ['apiTestProductPrePublishImmediately'];
+ $events[SchedulerCommerceproductEvents::PUBLISH_IMMEDIATELY][] = ['apiTestProductPublishImmediately'];
+
return $events;
}
/**
* Operations to perform before Scheduler publishes a node.
*
- * @param \Drupal\scheduler\SchedulerEvent $event
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
* The scheduler event.
*/
- public function apiTestPrePublish(SchedulerEvent $event) {
+ public function apiTestNodePrePublish(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();
// Before publishing a node make it sticky.
@@ -57,65 +84,65 @@ class EventSubscriber implements EventSubscriberInterface {
}
/**
- * Operations to perform before Scheduler unpublishes a node.
+ * Operations to perform after Scheduler publishes a node.
*
- * @param \Drupal\scheduler\SchedulerEvent $event
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
* The scheduler event.
*/
- public function apiTestPreUnpublish(SchedulerEvent $event) {
+ public function apiTestNodePublish(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();
+ // After publishing a node promote it to the front page.
if ($node->isPublished() && strpos($node->title->value, 'API TEST') === 0) {
- // Before unpublishing a node make it unsticky.
- $node->setSticky(FALSE);
+ $node->setPromoted(TRUE);
$event->setNode($node);
}
}
/**
- * Operations before Scheduler publishes a node immediately not via cron.
+ * Operations to perform before Scheduler unpublishes a node.
*
- * @param \Drupal\scheduler\SchedulerEvent $event
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
* The scheduler event.
*/
- public function apiTestPrePublishImmediately(SchedulerEvent $event) {
+ public function apiTestNodePreUnpublish(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();
- // Before publishing immediately set the node to sticky.
- if (!$node->isPromoted() && strpos($node->title->value, 'API TEST') === 0) {
- $node->setSticky(TRUE);
+ // Before unpublishing a node make it unsticky.
+ if ($node->isPublished() && strpos($node->title->value, 'API TEST') === 0) {
+ $node->setSticky(FALSE);
$event->setNode($node);
}
}
/**
- * Operations to perform after Scheduler publishes a node.
+ * Operations to perform after Scheduler unpublishes a node.
*
- * @param \Drupal\scheduler\SchedulerEvent $event
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
* The scheduler event.
*/
- public function apiTestPublish(SchedulerEvent $event) {
+ public function apiTestNodeUnpublish(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();
- // After publishing a node promote it to the front page.
- if ($node->isPublished() && strpos($node->title->value, 'API TEST') === 0) {
- $node->setPromoted(TRUE);
+ // After unpublishing a node remove it from the front page.
+ if (!$node->isPublished() && strpos($node->title->value, 'API TEST') === 0) {
+ $node->setPromoted(FALSE);
$event->setNode($node);
}
}
/**
- * Operations to perform after Scheduler unpublishes a node.
+ * Operations before Scheduler publishes a node immediately not via cron.
*
- * @param \Drupal\scheduler\SchedulerEvent $event
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
* The scheduler event.
*/
- public function apiTestUnpublish(SchedulerEvent $event) {
+ public function apiTestNodePrePublishImmediately(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();
- // After unpublishing a node remove it from the front page.
- if (!$node->isPublished() && strpos($node->title->value, 'API TEST') === 0) {
- $node->setPromoted(FALSE);
+ // Before publishing immediately set the node to sticky.
+ if (!$node->isPromoted() && strpos($node->title->value, 'API TEST') === 0) {
+ $node->setSticky(TRUE);
$event->setNode($node);
}
}
@@ -123,10 +150,10 @@ class EventSubscriber implements EventSubscriberInterface {
/**
* Operations after Scheduler publishes a node immediately not via cron.
*
- * @param \Drupal\scheduler\SchedulerEvent $event
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
* The scheduler event.
*/
- public function apiTestPublishImmediately(SchedulerEvent $event) {
+ public function apiTestNodePublishImmediately(SchedulerEvent $event) {
/** @var \Drupal\node\Entity\Node $node */
$node = $event->getNode();
// After publishing immediately set the node to promoted and change the
@@ -138,4 +165,190 @@ class EventSubscriber implements EventSubscriberInterface {
}
}
+ /**
+ * Operations to perform before Scheduler publishes a media item.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestMediaPrePublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ if (!$entity->isPublished() && strpos($entity->label(), 'API TEST MEDIA') === 0) {
+ // Media entities do not have the 'sticky' and 'promote' fields. Instead
+ // we can alter the name, for checking in the test.
+ $entity->setName('API TEST MEDIA - changed by "PRE_PUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform after Scheduler publishes a media item.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestMediaPublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ // The name will be changed here only if it has already been changed in the
+ // PRE_PUBLISH event function. This will show that both events worked.
+ if ($entity->isPublished() && $entity->label() == 'API TEST MEDIA - changed by "PRE_PUBLISH" event') {
+ $entity->setName('API TEST MEDIA - altered a second time by "PUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform before Scheduler unpublishes a media item.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestMediaPreUnpublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ if ($entity->isPublished() && strpos($entity->label(), 'API TEST MEDIA') === 0) {
+ // Media entities do not have the 'sticky' and 'promote' fields. Instead
+ // we can alter the name, for checking in the test.
+ $entity->setName('API TEST MEDIA - changed by "PRE_UNPUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform after Scheduler unpublishes a media item.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestMediaUnpublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ // The name will be changed here only if it has already been changed in the
+ // PRE_UNPUBLISH event function. This will show that both events worked.
+ if (!$entity->isPublished() && $entity->label() == 'API TEST MEDIA - changed by "PRE_UNPUBLISH" event') {
+ $entity->setName('API TEST MEDIA - altered a second time by "UNPUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations before Scheduler publishes a media immediately not via cron.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestMediaPrePublishImmediately(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ if (!$entity->isPublished() && strpos($entity->label(), 'API TEST MEDIA') === 0) {
+ $entity->setName('API TEST MEDIA - changed by "PRE_PUBLISH_IMMEDIATELY" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations after Scheduler publishes a media immediately not via cron.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestMediaPublishImmediately(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ // The name will be changed here only if it has already been changed in the
+ // PRE_PUBLISH_IMMEDIATELY event function, to show that both events worked.
+ if ($entity->label() == 'API TEST MEDIA - changed by "PRE_PUBLISH_IMMEDIATELY" event') {
+ $entity->setName('API TEST MEDIA - altered a second time by "PUBLISH_IMMEDIATELY" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform before Scheduler publishes a commerce product.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestProductPrePublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ if (!$entity->isPublished() && strpos($entity->label(), 'API TEST COMMERCE_PRODUCT') === 0) {
+ // Product entities do not have the 'sticky' and 'promote' fields. Instead
+ // we can alter the name, for checking in the test.
+ $entity->setTitle('API TEST COMMERCE_PRODUCT - changed by "PRE_PUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform after Scheduler publishes a commerce product.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestProductPublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ // The name will be changed here only if it has already been changed in the
+ // PRE_PUBLISH event function. This will show that both events worked.
+ if ($entity->isPublished() && $entity->label() == 'API TEST COMMERCE_PRODUCT - changed by "PRE_PUBLISH" event') {
+ $entity->setTitle('API TEST COMMERCE_PRODUCT - altered a second time by "PUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform before Scheduler unpublishes a commerce product.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestProductPreUnpublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ if ($entity->isPublished() && strpos($entity->label(), 'API TEST COMMERCE_PRODUCT') === 0) {
+ $entity->setTitle('API TEST COMMERCE_PRODUCT - changed by "PRE_UNPUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations to perform after Scheduler unpublishes a commerce product.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestProductUnpublish(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ // The name will be changed here only if it has already been changed in the
+ // PRE_UNPUBLISH event function. This will show that both events worked.
+ if (!$entity->isPublished() && $entity->label() == 'API TEST COMMERCE_PRODUCT - changed by "PRE_UNPUBLISH" event') {
+ $entity->setTitle('API TEST COMMERCE_PRODUCT - altered a second time by "UNPUBLISH" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations before Scheduler publishes a product immediately not via cron.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestProductPrePublishImmediately(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ if (!$entity->isPublished() && strpos($entity->label(), 'API TEST COMMERCE_PRODUCT') === 0) {
+ $entity->setTitle('API TEST COMMERCE_PRODUCT - changed by "PRE_PUBLISH_IMMEDIATELY" event');
+ $event->setEntity($entity);
+ }
+ }
+
+ /**
+ * Operations after Scheduler publishes a product immediately not via cron.
+ *
+ * @param \Drupal\scheduler\Event\SchedulerEvent $event
+ * The scheduler event.
+ */
+ public function apiTestProductPublishImmediately(SchedulerEvent $event) {
+ $entity = $event->getEntity();
+ // The name will be changed here only if it has already been changed in the
+ // PRE_PUBLISH_IMMEDIATELY event function, to show that both events worked.
+ if ($entity->label() == 'API TEST COMMERCE_PRODUCT - changed by "PRE_PUBLISH_IMMEDIATELY" event') {
+ $entity->setTitle('API TEST COMMERCE_PRODUCT - altered a second time by "PUBLISH_IMMEDIATELY" event');
+ $event->setEntity($entity);
+ }
+ }
+
}
diff --git a/tests/modules/scheduler_extras/scheduler_extras.module b/tests/modules/scheduler_extras/scheduler_extras.module
index 9264b69..93a846f 100644
--- a/tests/modules/scheduler_extras/scheduler_extras.module
+++ b/tests/modules/scheduler_extras/scheduler_extras.module
@@ -3,23 +3,30 @@
/**
* @file
* Hook implementations for the Scheduler Extras test module.
+ *
+ * This module is used in SchedulerDefaultTimeTest to check that the default
+ * time is set correctly when the time element of the datetime input is hidden.
*/
use Drupal\Core\Form\FormStateInterface;
/**
- * Implements hook_form_FORM_ID_alter() for node_form().
+ * Implements hook_form_alter().
*/
-function scheduler_extras_form_node_form_alter(&$form, FormStateInterface $form_state) {
- // This is used in SchedulerDefaultTimeTest to check that the default time is
- // set correctly even when the time elememt of the datetime input is hidden.
- $type = $form_state->getFormObject()->getEntity()->type->entity->get('type');
- if ($type == 'hidden_time') {
- if (isset($form['publish_on'])) {
- $form['publish_on']['widget'][0]['value']['#date_time_element'] = 'none';
- }
- if (isset($form['unpublish_on'])) {
- $form['unpublish_on']['widget'][0]['value']['#date_time_element'] = 'none';
- }
+function scheduler_extras_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ // Only continue if the form is form adding the standard test entity types.
+ if (!in_array($form_id, [
+ 'node_testpage_form',
+ 'media_test_video_add_form',
+ 'commerce_product_test_product_add_form',
+ ])) {
+ return;
+ }
+ // Hide the time element when the scheduler field exists.
+ if (isset($form['publish_on'])) {
+ $form['publish_on']['widget'][0]['value']['#date_time_element'] = 'none';
+ }
+ if (isset($form['unpublish_on'])) {
+ $form['unpublish_on']['widget'][0]['value']['#date_time_element'] = 'none';
}
}
diff --git a/tests/src/Functional/SchedulerAdminSettingsTest.php b/tests/src/Functional/SchedulerAdminSettingsTest.php
index 688ca0c..65700a1 100644
--- a/tests/src/Functional/SchedulerAdminSettingsTest.php
+++ b/tests/src/Functional/SchedulerAdminSettingsTest.php
@@ -19,6 +19,14 @@ class SchedulerAdminSettingsTest extends SchedulerBrowserTestBase {
public function testAdminSettings() {
$this->drupalLogin($this->adminUser);
+ // Check that we get the warning when no media types exist.
+ $this->drupalGet('admin/config/content/scheduler');
+ $this->assertSession()->pageTextContains('No entity types returned by media module for use in Media Scheduler Plugin');
+
+ // Call the setUp functions for all entity types.
+ $this->schedulerMediaSetUp();
+ $this->SchedulerCommerceProductSetUp();
+
// Verify that the default values are as expected.
$this->assertFalse($this->config('scheduler.settings')->get('allow_date_only'), 'The default setting for allow_date_only is False.');
$this->assertEquals($this->config('scheduler.settings')->get('default_time'), '00:00:00', 'The default config setting for default_time is 00:00:00');
@@ -30,7 +38,9 @@ class SchedulerAdminSettingsTest extends SchedulerBrowserTestBase {
'allow_date_only' => TRUE,
'default_time' => '6:30',
];
- $this->drupalPostForm('admin/config/content/scheduler', $settings, 'Save configuration');
+ $this->drupalGet('admin/config/content/scheduler');
+ $this->submitForm($settings, 'Save configuration');
+
// Verify that the values have been saved correctly.
$this->assertTrue($this->config('scheduler.settings')->get('allow_date_only'), 'The config setting for allow_date_only is stored correctly.');
$this->assertEquals('06:30:00', $this->config('scheduler.settings')->get('default_time'), 'The config setting for default_time is stored correctly.');
@@ -40,7 +50,8 @@ class SchedulerAdminSettingsTest extends SchedulerBrowserTestBase {
'allow_date_only' => TRUE,
'default_time' => '123',
];
- $this->drupalPostForm('admin/config/content/scheduler', $settings, 'Save configuration');
+ $this->drupalGet('admin/config/content/scheduler');
+ $this->submitForm($settings, 'Save configuration');
// Verify that the value has not been saved and an error is displayed.
$this->assertEquals('06:30:00', $this->config('scheduler.settings')->get('default_time'), 'The config setting for default_time has not changed.');
$this->assertSession()->pageTextContains('The default time should be in the format HH:MM:SS');
@@ -49,7 +60,8 @@ class SchedulerAdminSettingsTest extends SchedulerBrowserTestBase {
$settings = [
'hide_seconds' => TRUE,
];
- $this->drupalPostForm('admin/config/content/scheduler', $settings, 'Save configuration');
+ $this->drupalGet('admin/config/content/scheduler');
+ $this->submitForm($settings, 'Save configuration');
// Verify that the hide seconds option is saved and the default time is
// stored in HH:MM format with no seconds.
$this->assertTrue($this->config('scheduler.settings')->get('hide_seconds'), 'The config setting for hide_seconds is stored correctly.');
@@ -59,7 +71,8 @@ class SchedulerAdminSettingsTest extends SchedulerBrowserTestBase {
$settings = [
'default_time' => '456',
];
- $this->drupalPostForm('admin/config/content/scheduler', $settings, 'Save configuration');
+ $this->drupalGet('admin/config/content/scheduler');
+ $this->submitForm($settings, 'Save configuration');
// Verify that the value has not been saved, and that an error message is
// displayed showing the correct format HH:MM not HH:MM:SS.
$this->assertEquals('06:30', $this->config('scheduler.settings')->get('default_time'), 'The config setting for default_time has not changed.');
diff --git a/tests/src/Functional/SchedulerApiTest.php b/tests/src/Functional/SchedulerApiTest.php
index 3632884..c9a61fd 100644
--- a/tests/src/Functional/SchedulerApiTest.php
+++ b/tests/src/Functional/SchedulerApiTest.php
@@ -5,22 +5,26 @@ namespace Drupal\Tests\scheduler\Functional;
use Drupal\node\Entity\NodeType;
/**
- * Tests the API of the Scheduler module.
+ * Tests the legacy API hook functions of the Scheduler module.
+ *
+ * This class covers the eight original hook functions for node entity types
+ * only. These are maintained for backwards-compatibility.
*
* @group scheduler
*/
-class SchedulerApiTest extends SchedulerBrowserTestBase {
+class SchedulerHooksLegacyTest extends SchedulerBrowserTestBase {
/**
* Additional modules required.
*
* @var array
- *
- * @todo 'menu_ui' is in the exported node.type definition, and 'path' is in
- * the entity_form_display. Could these be removed from the config files and
- * then not needed here?
*/
- protected static $modules = ['scheduler_api_test', 'menu_ui', 'path'];
+ protected static $modules = [
+ 'scheduler_api_test',
+ 'scheduler_api_legacy_test',
+ 'menu_ui',
+ 'path',
+ ];
/**
* {@inheritdoc}
@@ -30,19 +34,132 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
// Load the custom node type. It will be enabled for Scheduler automatically
// as that is pre-configured in node.type.scheduler_api_test.yml.
- $this->customName = 'scheduler_api_test';
+ $this->customName = 'scheduler_api_node_test';
$this->customNodetype = NodeType::load($this->customName);
// Check that the custom node type has loaded OK.
- $this->assertNotNull($this->customNodetype, 'Custom node type "' . $this->customName . '" was created during install');
+ $this->assertNotNull($this->customNodetype, 'Custom node type "' . $this->customName . '" was created during install');
- // Create a web user for this content type.
+ // Create a web user that has permission to create and edit and schedule
+ // the custom entity type.
$this->webUser = $this->drupalCreateUser([
'create ' . $this->customName . ' content',
'edit any ' . $this->customName . ' content',
'schedule publishing of nodes',
]);
+ $this->webUser->set('name', 'Wenlock the Web user')->save();
+
+ }
+
+ /**
+ * Covers hook_scheduler_nid_list($action)
+ *
+ * Hook_scheduler_nid_list() allows other modules to add more node ids into
+ * the list to be processed. In real scenarios, the third-party module would
+ * likely have more complex data structures and/or tables from which to
+ * identify nodes to add. In this test, to keep it simple, we identify nodes
+ * by the text of the title.
+ */
+ public function testNidList() {
+ $this->drupalLogin($this->schedulerUser);
+
+ // Create test nodes. Use the ordinary page type for this test, as having
+ // the 'approved' fields here would unnecessarily complicate the processing.
+ // Node 1 is not published and has no publishing date set. The test API
+ // module will add node 1 into the list to be published.
+ $node1 = $this->drupalCreateNode([
+ 'type' => $this->type,
+ 'status' => FALSE,
+ 'title' => 'API TEST nid_list publish me',
+ ]);
+ // Node 2 is published and has no unpublishing date set. The test API module
+ // will add node 2 into the list to be unpublished.
+ $node2 = $this->drupalCreateNode([
+ 'type' => $this->type,
+ 'status' => TRUE,
+ 'title' => 'API TEST nid_list unpublish me',
+ ]);
+
+ // Before cron, check node 1 is unpublished and node 2 is published.
+ $this->assertFalse($node1->isPublished(), 'Before cron, node 1 "' . $node1->title->value . '" is unpublished.');
+ $this->assertTrue($node2->isPublished(), 'Before cron, node 2 "' . $node2->title->value . '" is published.');
+
+ // Run cron and refresh the nodes.
+ scheduler_cron();
+ $this->nodeStorage->resetCache();
+ $node1 = $this->nodeStorage->load($node1->id());
+ $node2 = $this->nodeStorage->load($node2->id());
+
+ // Check node 1 is published and node 2 is unpublished.
+ $this->assertTrue($node1->isPublished(), 'After cron, node 1 "' . $node1->title->value . '" is published.');
+ $this->assertFalse($node2->isPublished(), 'After cron, node 2 "' . $node2->title->value . '" is unpublished.');
+ }
+
+ /**
+ * Covers hook_scheduler_nid_list_alter($action)
+ *
+ * This hook allows other modules to add or remove node ids from the list to
+ * be processed. As in testNidList() we make it simple by using the title text
+ * to identify which nodes to act on.
+ */
+ public function testNidListAlter() {
+ $this->drupalLogin($this->schedulerUser);
+
+ // Create test nodes. Use the ordinary page type for this test, as having
+ // the 'approved' fields here would unnecessarily complicate the processing.
+ // Node 1 is set for scheduled publishing, but will be removed by the test
+ // API hook_nid_list_alter().
+ $node1 = $this->drupalCreateNode([
+ 'type' => $this->type,
+ 'status' => FALSE,
+ 'title' => 'API TEST nid_list_alter do not publish me',
+ 'publish_on' => strtotime('-1 day'),
+ ]);
+
+ // Node 2 is not published and has no publishing date set. The test API
+ // module will add node 2 into the list to be published.
+ $node2 = $this->drupalCreateNode([
+ 'type' => $this->type,
+ 'status' => FALSE,
+ 'title' => 'API TEST nid_list_alter publish me',
+ ]);
+
+ // Node 3 is set for scheduled unpublishing, but will be removed by the test
+ // API hook_nid_list_alter().
+ $node3 = $this->drupalCreateNode([
+ 'type' => $this->type,
+ 'status' => TRUE,
+ 'title' => 'API TEST nid_list_alter do not unpublish me',
+ 'unpublish_on' => strtotime('-1 day'),
+ ]);
+
+ // Node 4 is published and has no unpublishing date set. The test API module
+ // will add node 4 into the list to be unpublished.
+ $node4 = $this->drupalCreateNode([
+ 'type' => $this->type,
+ 'status' => TRUE,
+ 'title' => 'API TEST nid_list_alter unpublish me',
+ ]);
+
+ // Check node 1 and 2 are unpublished and node 3 and 4 are published.
+ $this->assertFalse($node1->isPublished(), 'Before cron, node 1 "' . $node1->title->value . '" is unpublished.');
+ $this->assertFalse($node2->isPublished(), 'Before cron, node 2 "' . $node2->title->value . '" is unpublished.');
+ $this->assertTrue($node3->isPublished(), 'Before cron, node 3 "' . $node3->title->value . '" is published.');
+ $this->assertTrue($node4->isPublished(), 'Before cron, node 4 "' . $node4->title->value . '" is published.');
+
+ // Run cron and refresh the nodes.
+ scheduler_cron();
+ $this->nodeStorage->resetCache();
+ $node1 = $this->nodeStorage->load($node1->id());
+ $node2 = $this->nodeStorage->load($node2->id());
+ $node3 = $this->nodeStorage->load($node3->id());
+ $node4 = $this->nodeStorage->load($node4->id());
+ // Check node 2 and 3 are published and node 1 and 4 are unpublished.
+ $this->assertFalse($node1->isPublished(), 'After cron, node 1 "' . $node1->title->value . '" is still unpublished.');
+ $this->assertTrue($node2->isPublished(), 'After cron, node 2 "' . $node2->title->value . '" is published.');
+ $this->assertTrue($node3->isPublished(), 'After cron, node 3 "' . $node3->title->value . '" is still published.');
+ $this->assertFalse($node4->isPublished(), 'After cron, node 4 "' . $node4->title->value . '" is unpublished.');
}
/**
@@ -51,12 +168,10 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
* This hook can allow or deny the publishing of individual nodes. This test
* uses the customised content type which has checkboxes 'Approved for
* publication' and 'Approved for unpublication'.
- *
- * @todo Create and update the nodes through the interface so we can check if
- * the correct messages are displayed.
*/
public function testAllowedPublishing() {
$this->drupalLogin($this->webUser);
+
// Check the 'approved for publishing' field is shown on the node form.
$this->drupalGet('node/add/' . $this->customName);
$this->assertSession()->fieldExists('edit-field-approved-publishing-value');
@@ -68,8 +183,9 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
'publish_on[0][value][date]' => date('Y-m-d', time() + 3),
'publish_on[0][value][time]' => date('H:i:s', time() + 3),
];
- $this->drupalPostForm('node/add/' . $this->customName, $edit, 'Save');
- $this->assertSession()->pageTextContains('is scheduled for publishing, but will not be published until approved.');
+ $this->drupalGet("node/add/{$this->customName}");
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextMatches('/is scheduled for publishing.* but will not be published until approved/');
// Create a node that is scheduled but not approved for publication. Then
// simulate a cron run, and check that the node is still not published.
@@ -77,7 +193,7 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
scheduler_cron();
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
- $this->assertFalse($node->isPublished(), 'An unapproved node is not published during cron processing.');
+ $this->assertFalse($node->isPublished(), "Unapproved '{$node->label()}' should not be published during cron processing.");
// Create a node and approve it for publication, simulate a cron run and
// check that the node is published. This is a stronger test than simply
@@ -85,30 +201,31 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
// state that may be in after the cron run above.
$node = $this->createUnapprovedNode('publish_on');
$this->approveNode($node->id(), 'field_approved_publishing');
- $this->assertFalse($node->isPublished(), 'A new approved node is initially not published.');
+ $this->assertFalse($node->isPublished(), "New approved '{$node->label()}' should not be initially published.");
scheduler_cron();
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isPublished(), 'An approved node is published during cron processing.');
+ $this->assertTrue($node->isPublished(), "Approved '{$node->label()}' should be published during cron processing.");
// Turn on immediate publication of nodes with publication dates in the past
// and repeat the tests. It is not needed to simulate cron runs here.
$this->customNodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
$node = $this->createUnapprovedNode('publish_on');
- $this->assertFalse($node->isPublished(), 'An unapproved node with a date in the past is not published immediately after saving.');
+ $this->assertFalse($node->isPublished(), "New unapproved '{$node->label()}' with a date in the past should not be published immediately after saving.");
// Check that the node can be approved and published programatically.
$this->approveNode($node->id(), 'field_approved_publishing');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isPublished(), 'An approved node with a date in the past is published immediately via $node->set()->save().');
+ $this->assertTrue($node->isPublished(), "New approved '{$node->label()}' with a date in the past should be published immediately when created programatically.");
// Check that a node can be approved and published via edit form.
$node = $this->createUnapprovedNode('publish_on');
- $this->drupalPostForm('node/' . $node->id() . '/edit', ['field_approved_publishing[value]' => '1'], 'Save');
+ $this->drupalGet("node/{$node->id()}/edit");
+ $this->submitForm(['field_approved_publishing[value]' => '1'], 'Save');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isPublished(), 'An approved node with a date in the past is published immediately after saving via edit form.');
+ $this->assertTrue($node->isPublished(), "Approved '{$node->label()}' with a date in the past should be published immediately after saving via edit form.");
// Show the dblog messages.
$this->drupalLogin($this->adminUser);
@@ -124,6 +241,7 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
*/
public function testAllowedUnpublishing() {
$this->drupalLogin($this->webUser);
+
// Check the 'approved for unpublishing' field is shown on the node form.
$this->drupalGet('node/add/' . $this->customName);
$this->assertSession()->fieldExists('edit-field-approved-unpublishing-value');
@@ -135,8 +253,9 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
'unpublish_on[0][value][date]' => date('Y-m-d', time() + 3),
'unpublish_on[0][value][time]' => date('H:i:s', time() + 3),
];
- $this->drupalPostForm('node/add/' . $this->customName, $edit, 'Save');
- $this->assertSession()->pageTextContains('is scheduled for unpublishing, but will not be unpublished until approved.');
+ $this->drupalGet("node/add/{$this->customName}");
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextMatches('/is scheduled for unpublishing.* but will not be unpublished until approved/');
// Create a node that is scheduled but not approved for unpublication. Then
// simulate a cron run, and check that the node is still published.
@@ -144,17 +263,17 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
scheduler_cron();
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isPublished(), 'An unapproved node is not unpublished during cron processing.');
+ $this->assertTrue($node->isPublished(), "Unapproved '{$node->label()}' should not be unpublished during cron processing.");
// Create a node, then approve it for unpublishing, simulate a cron run and
// check that the node is now unpublished.
$node = $this->createUnapprovedNode('unpublish_on');
$this->approveNode($node->id(), 'field_approved_unpublishing');
- $this->assertTrue($node->isPublished(), 'A new approved node is initially published.');
+ $this->assertTrue($node->isPublished(), "New approved '{$node->label()}' should initially remain published.");
scheduler_cron();
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
- $this->assertFalse($node->isPublished(), 'An approved node is unpublished during cron processing.');
+ $this->assertFalse($node->isPublished(), "Approved '{$node->label()}' should be unpublished during cron processing.");
// Show the dblog messages.
$this->drupalLogin($this->adminUser);
@@ -175,6 +294,7 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
*/
protected function createUnapprovedNode($date_field) {
$settings = [
+ 'title' => (($date_field == 'publish_on') ? 'Blue' : 'Red') . " legacy node {$this->randomMachineName(10)}",
'status' => ($date_field == 'unpublish_on'),
$date_field => strtotime('-1 day'),
'field_approved_publishing' => 0,
@@ -196,185 +316,8 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
protected function approveNode($nid, $field_name) {
$this->nodeStorage->resetCache([$nid]);
$node = $this->nodeStorage->load($nid);
- $node->set($field_name, TRUE)->save();
- }
-
- /**
- * Covers six events.
- *
- * The events allow other modules to react to the Scheduler process being run.
- * The API test implementations of the event listeners alter the nodes
- * 'promote' and 'sticky' settings and changes the title.
- */
- public function testApiNodeAction() {
- $this->drupalLogin($this->schedulerUser);
-
- // Create a test node. Having the 'approved' fields here would complicate
- // the tests, so use the ordinary page type.
- $settings = [
- 'publish_on' => strtotime('-1 day'),
- 'type' => $this->type,
- 'promote' => FALSE,
- 'sticky' => FALSE,
- 'title' => 'API TEST node action',
- ];
- $node = $this->drupalCreateNode($settings);
-
- // Check that the 'sticky' and 'promote' fields are off for the new node.
- $this->assertFalse($node->isSticky(), 'The unpublished node is not sticky.');
- $this->assertFalse($node->isPromoted(), 'The unpublished node is not promoted.');
-
- // Run cron and check that hook_scheduler_api() has executed correctly, by
- // verifying that the node has become promoted and is sticky.
- scheduler_cron();
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isSticky(), 'API action "PRE_PUBLISH" has changed the node to sticky.');
- $this->assertTrue($node->isPromoted(), 'API action "PUBLISH" has changed the node to promoted.');
-
- // Now set a date for unpublishing the node. Ensure 'sticky' and 'promote'
- // are set, so that the assertions are not affected by any failures above.
- $node->set('unpublish_on', strtotime('-1 day'))
- ->set('sticky', TRUE)->set('promote', TRUE)->save();
-
- // Run cron and check that hook_scheduler_api() has executed correctly, by
- // verifying that the node is not promoted and no longer sticky.
- scheduler_cron();
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- $this->assertFalse($node->isSticky(), 'API action "PRE_UNPUBLISH" has changed the node to not sticky.');
- $this->assertFalse($node->isPromoted(), 'API action "UNPUBLISH" has changed the node to not promoted.');
-
- // Turn on immediate publication when a publish date is in the past.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
-
- // Ensure 'sticky' and 'promote' are not set, so that the assertions are not
- // affected by any failures above.
- $node->set('sticky', FALSE)->set('promote', FALSE)->save();
-
- // Edit the node and set a publish-on date in the past.
- $edit = [
- 'publish_on[0][value][date]' => date('Y-m-d', strtotime('-2 day', $this->requestTime)),
- 'publish_on[0][value][time]' => date('H:i:s', strtotime('-2 day', $this->requestTime)),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- // Verify that the values have been altered as expected.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isSticky(), 'API action "PRE_PUBLISH_IMMEDIATELY" has changed the node to sticky.');
- $this->assertTrue($node->isPromoted(), 'API action "PUBLISH_IMMEDIATELY" has changed the node to promoted.');
- $this->assertEquals('Published immediately', $node->title->value, 'API action "PUBLISH_IMMEDIATELY" has changed the node title correctly.');
- }
-
- /**
- * Covers hook_scheduler_nid_list($action)
- *
- * Hook_scheduler_nid_list() allows other modules to add more node ids into
- * the list to be processed. In real scenarios, the third-party module would
- * likely have more complex data structures and/or tables from which to
- * identify nodes to add. In this test, to keep it simple, we identify nodes
- * by the text of the title.
- */
- public function testNidList() {
- $this->drupalLogin($this->schedulerUser);
-
- // Create test nodes. Use the ordinary page type for this test, as having
- // the 'approved' fields here would unnecessarily complicate the processing.
- // Node 1 is not published and has no publishing date set. The test API
- // module will add node 1 into the list to be published.
- $node1 = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- 'title' => 'API TEST nid_list publish me',
- ]);
- // Node 2 is published and has no unpublishing date set. The test API module
- // will add node 2 into the list to be unpublished.
- $node2 = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => TRUE,
- 'title' => 'API TEST nid_list unpublish me',
- ]);
-
- // Before cron, check node 1 is unpublished and node 2 is published.
- $this->assertFalse($node1->isPublished(), 'Before cron, node 1 "' . $node1->title->value . '" is unpublished.');
- $this->assertTrue($node2->isPublished(), 'Before cron, node 2 "' . $node2->title->value . '" is published.');
-
- // Run cron and refresh the nodes.
- scheduler_cron();
- $this->nodeStorage->resetCache();
- $node1 = $this->nodeStorage->load($node1->id());
- $node2 = $this->nodeStorage->load($node2->id());
-
- // Check node 1 is published and node 2 is unpublished.
- $this->assertTrue($node1->isPublished(), 'After cron, node 1 "' . $node1->title->value . '" is published.');
- $this->assertFalse($node2->isPublished(), 'After cron, node 2 "' . $node2->title->value . '" is unpublished.');
- }
-
- /**
- * Covers hook_scheduler_nid_list_alter($action)
- *
- * This hook allows other modules to add or remove node ids from the list to
- * be processed. As in testNidList() we make it simple by using the title text
- * to identify which nodes to act on.
- */
- public function testNidListAlter() {
- $this->drupalLogin($this->schedulerUser);
-
- // Create test nodes. Use the ordinary page type for this test, as having
- // the 'approved' fields here would unnecessarily complicate the processing.
- // Node 1 is set for scheduled publishing, but will be removed by the test
- // API hook_nid_list_alter().
- $node1 = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- 'title' => 'API TEST nid_list_alter do not publish me',
- 'publish_on' => strtotime('-1 day'),
- ]);
-
- // Node 2 is not published and has no publishing date set. The test API
- // module will add node 2 into the list to be published.
- $node2 = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- 'title' => 'API TEST nid_list_alter publish me',
- ]);
-
- // Node 3 is set for scheduled unpublishing, but will be removed by the test
- // API hook_nid_list_alter().
- $node3 = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => TRUE,
- 'title' => 'API TEST nid_list_alter do not unpublish me',
- 'unpublish_on' => strtotime('-1 day'),
- ]);
-
- // Node 4 is published and has no unpublishing date set. The test API module
- // will add node 4 into the list to be unpublished.
- $node4 = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => TRUE,
- 'title' => 'API TEST nid_list_alter unpublish me',
- ]);
-
- // Check node 1 and 2 are unpublished and node 3 and 4 are published.
- $this->assertFalse($node1->isPublished(), 'Before cron, node 1 "' . $node1->title->value . '" is unpublished.');
- $this->assertFalse($node2->isPublished(), 'Before cron, node 2 "' . $node2->title->value . '" is unpublished.');
- $this->assertTrue($node3->isPublished(), 'Before cron, node 3 "' . $node3->title->value . '" is published.');
- $this->assertTrue($node4->isPublished(), 'Before cron, node 4 "' . $node4->title->value . '" is published.');
-
- // Run cron and refresh the nodes.
- scheduler_cron();
- $this->nodeStorage->resetCache();
- $node1 = $this->nodeStorage->load($node1->id());
- $node2 = $this->nodeStorage->load($node2->id());
- $node3 = $this->nodeStorage->load($node3->id());
- $node4 = $this->nodeStorage->load($node4->id());
-
- // Check node 2 and 3 are published and node 1 and 4 are unpublished.
- $this->assertFalse($node1->isPublished(), 'After cron, node 1 "' . $node1->title->value . '" is still unpublished.');
- $this->assertTrue($node2->isPublished(), 'After cron, node 2 "' . $node2->title->value . '" is published.');
- $this->assertTrue($node3->isPublished(), 'After cron, node 3 "' . $node3->title->value . '" is still published.');
- $this->assertFalse($node4->isPublished(), 'After cron, node 4 "' . $node4->title->value . '" is unpublished.');
+ $node->set($field_name, TRUE);
+ $node->set('title', $node->label() . " - approved for publishing: {$node->field_approved_publishing->value}, for unpublishing: {$node->field_approved_unpublishing->value}")->save();
}
/**
@@ -389,21 +332,24 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
// Create test nodes.
$node1 = $this->drupalCreateNode([
'type' => $this->type,
- 'title' => 'Red will not have either field hidden',
+ 'title' => 'Red Legacy will not have either field hidden',
]);
$node2 = $this->drupalCreateNode([
'type' => $this->type,
- 'title' => 'Orange will have the publish-on field hidden',
+ 'title' => 'Orange Legacy will have the publish-on field hidden',
]);
$node3 = $this->drupalCreateNode([
'type' => $this->type,
- 'title' => 'Yellow will have the unpublish-on field hidden',
+ 'title' => 'Yellow Legacy will have the unpublish-on field hidden',
]);
$node4 = $this->drupalCreateNode([
'type' => $this->type,
- 'title' => 'Green will have both Scheduler fields hidden',
+ 'title' => 'Green Legacy will have both Scheduler fields hidden',
]);
+ // Set the scheduler fieldset to always expand, for ease during development.
+ $this->nodetype->setThirdPartySetting('scheduler', 'expand_fieldset', 'always')->save();
+
/** @var \Drupal\Tests\WebAssert $assert */
$assert = $this->assertSession();
@@ -434,26 +380,26 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
* This covers hook_scheduler_publish_action and
* hook_scheduler_unpublish_action.
*/
- public function testHookPublishUnpublishAction() {
+ public function testPublishUnpublishAction() {
$this->drupalLogin($this->schedulerUser);
// Create test nodes.
$node1 = $this->drupalCreateNode([
'type' => $this->type,
'status' => FALSE,
- 'title' => 'Red will cause a failure on publishing',
+ 'title' => 'Red Legacy will cause a failure on publishing',
'publish_on' => strtotime('-1 day'),
]);
$node2 = $this->drupalCreateNode([
'type' => $this->type,
'status' => TRUE,
- 'title' => 'Orange will be unpublished by the API test module not Scheduler',
+ 'title' => 'Orange Legacy will be unpublished by the API test module not Scheduler',
'unpublish_on' => strtotime('-1 day'),
]);
$node3 = $this->drupalCreateNode([
'type' => $this->type,
'status' => FALSE,
- 'title' => 'Yellow will be published by the API test module not Scheduler',
+ 'title' => 'Yellow Legacy will be published by the API test module not Scheduler',
'publish_on' => strtotime('-1 day'),
]);
// 'green' nodes will have both fields hidden so is harder to test manually.
@@ -461,7 +407,7 @@ class SchedulerApiTest extends SchedulerBrowserTestBase {
$node4 = $this->drupalCreateNode([
'type' => $this->type,
'status' => TRUE,
- 'title' => 'Blue will cause a failure on unpublishing',
+ 'title' => 'Blue Legacy will cause a failure on unpublishing',
'unpublish_on' => strtotime('-1 day'),
]);
diff --git a/tests/src/Functional/SchedulerBasicMediaTest.php b/tests/src/Functional/SchedulerBasicMediaTest.php
new file mode 100644
index 0000000..98614e7
--- /dev/null
+++ b/tests/src/Functional/SchedulerBasicMediaTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+/**
+ * Tests the modules primary functions with a Media entity type.
+ *
+ * @group scheduler
+ */
+class SchedulerBasicMediaTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Tests scheduled publishing of a media entity.
+ *
+ * Covers scheduler_entity_presave(), scheduler_cron(),
+ * schedulerManager::publish.
+ */
+ public function testMediaPublishing() {
+ // Specify values for the entity.
+ $values = [
+ 'name' => 'Publish This Media',
+ 'publish_on' => $this->requestTime + 3600,
+ ];
+ // Create a media entity with the scheduler fields populated as required.
+ $entity = $this->createMediaItem($values);
+ $this->assertNotEmpty($entity, 'The entity was created sucessfully.');
+
+ // Assert that the entity has a publish_on date.
+ $this->assertNotEmpty($entity->publish_on, 'The entity has a publish_on date');
+
+ // Assert that the entity is not published before cron.
+ $this->assertFalse($entity->isPublished(), 'The entity is unpublished before cron run');
+
+ // Modify the scheduler field to a time in the past, then run cron.
+ $entity->publish_on = $this->requestTime - 1;
+ $entity->save();
+ $this->cronRun();
+
+ // Refresh the cache, reload the entity and check the entity is published.
+ $this->mediaStorage->resetCache([$entity->id()]);
+ $entity = $this->mediaStorage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), 'The entity is published after cron run');
+ }
+
+ /**
+ * Tests scheduled unpublishing of a media entity.
+ *
+ * Covers scheduler_entity_presave(), scheduler_cron(),
+ * schedulerManager::unpublish.
+ */
+ public function testMediaUnpublishing() {
+ // Specify values for the entity.
+ $values = [
+ 'name' => 'Unpublish This Media',
+ 'unpublish_on' => $this->requestTime + 3600,
+ ];
+ // Create a media entity with the scheduler fields populated as required.
+ $entity = $this->createMediaItem($values);
+ $this->assertNotEmpty($entity, 'The entity was created sucessfully.');
+
+ // Assert that the entity has an unpublish_on date.
+ $this->assertNotEmpty($entity->unpublish_on, 'The entity has an unpublish_on date');
+
+ // Assert that the entity is published before cron.
+ $this->assertTrue($entity->isPublished(), 'The entity is published before cron run');
+
+ // Modify the scheduler field to a time in the past, then run cron.
+ $entity->unpublish_on = $this->requestTime - 1;
+ $entity->save();
+ $this->cronRun();
+
+ // Refresh the cache, reload the entity and check the entity is unpublished.
+ $this->mediaStorage->resetCache([$entity->id()]);
+ $entity = $this->mediaStorage->load($entity->id());
+ $this->assertFalse($entity->isPublished(), 'The entity is unpublished after cron run');
+
+ }
+
+}
diff --git a/tests/src/Functional/SchedulerBasicProductTest.php b/tests/src/Functional/SchedulerBasicProductTest.php
new file mode 100644
index 0000000..8db92a7
--- /dev/null
+++ b/tests/src/Functional/SchedulerBasicProductTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+/**
+ * Tests the modules primary functions with a Commerce Product entity type.
+ *
+ * @group scheduler
+ */
+class SchedulerBasicProductTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Tests scheduled publishing of a commerce product entity.
+ *
+ * Covers scheduler_entity_presave(), scheduler_cron(),
+ * schedulerManager::publish.
+ */
+ public function testProductPublishing() {
+ // Specify values for the entity.
+ $values = [
+ 'title' => 'Publish This Product',
+ 'publish_on' => $this->requestTime + 3600,
+ ];
+ // Create a product entity with the scheduler fields populated as required.
+ $entity = $this->createProduct($values);
+ $this->assertNotEmpty($entity, 'The entity was created sucessfully.');
+
+ // Assert that the entity has a publish_on date.
+ $this->assertNotEmpty($entity->publish_on, 'The entity has a publish_on date');
+
+ // Assert that the entity is not published before cron.
+ $this->assertFalse($entity->isPublished(), 'The entity is unpublished before cron run');
+
+ // Modify the scheduler field to a time in the past, then run cron.
+ $entity->publish_on = $this->requestTime - 1;
+ $entity->save();
+ $this->cronRun();
+
+ // Refresh the cache, reload the entity and check the entity is published.
+ $this->productStorage->resetCache([$entity->id()]);
+ $entity = $this->productStorage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), 'The entity is published after cron run');
+ }
+
+ /**
+ * Tests scheduled unpublishing of a commerce product entity.
+ *
+ * Covers scheduler_entity_presave(), scheduler_cron(),
+ * schedulerManager::unpublish.
+ */
+ public function testProductUnpublishing() {
+ // Specify values for the entity.
+ $values = [
+ 'title' => 'Unpublish This Product',
+ 'unpublish_on' => $this->requestTime + 3600,
+ ];
+ // Create a product with the scheduler fields populated as required.
+ $entity = $this->createProduct($values);
+ $this->assertNotEmpty($entity, 'The entity was created sucessfully.');
+
+ // Assert that the entity has an unpublish_on date.
+ $this->assertNotEmpty($entity->unpublish_on, 'The entity has an unpublish_on date');
+
+ // Assert that the entity is published before cron.
+ $this->assertTrue($entity->isPublished(), 'The entity is published before cron run');
+
+ // Modify the scheduler field to a time in the past, then run cron.
+ $entity->unpublish_on = $this->requestTime - 1;
+ $entity->save();
+ $this->cronRun();
+
+ // Refresh the cache, reload the entity and check the entity is unpublished.
+ $this->productStorage->resetCache([$entity->id()]);
+ $entity = $this->productStorage->load($entity->id());
+ $this->assertFalse($entity->isPublished(), 'The entity is unpublished after cron run');
+
+ }
+
+}
diff --git a/tests/src/Functional/SchedulerBasicTest.php b/tests/src/Functional/SchedulerBasicTest.php
index ab28633..d827ede 100644
--- a/tests/src/Functional/SchedulerBasicTest.php
+++ b/tests/src/Functional/SchedulerBasicTest.php
@@ -43,7 +43,8 @@ class SchedulerBasicTest extends SchedulerBrowserTestBase {
* Schedules content, runs cron and asserts status.
*/
protected function helpTestScheduler($edit) {
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
+ $this->drupalGet("node/add/{$this->type}");
+ $this->submitForm($edit, 'Save');
// Verify that the node was created.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotEmpty($node, sprintf('"%s" was created sucessfully.', $edit['title[0][value]']));
diff --git a/tests/src/Functional/SchedulerBrowserTestBase.php b/tests/src/Functional/SchedulerBrowserTestBase.php
index 0a84666..86aede1 100644
--- a/tests/src/Functional/SchedulerBrowserTestBase.php
+++ b/tests/src/Functional/SchedulerBrowserTestBase.php
@@ -3,6 +3,8 @@
namespace Drupal\Tests\scheduler\Functional;
use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\scheduler\Traits\SchedulerCommerceProductSetupTrait;
+use Drupal\Tests\scheduler\Traits\SchedulerMediaSetupTrait;
use Drupal\Tests\scheduler\Traits\SchedulerSetupTrait;
/**
@@ -11,6 +13,8 @@ use Drupal\Tests\scheduler\Traits\SchedulerSetupTrait;
abstract class SchedulerBrowserTestBase extends BrowserTestBase {
use SchedulerSetupTrait;
+ use SchedulerMediaSetupTrait;
+ use SchedulerCommerceProductSetupTrait;
/**
* The standard modules to load for all browser tests.
@@ -19,7 +23,12 @@ abstract class SchedulerBrowserTestBase extends BrowserTestBase {
*
* @var array
*/
- protected static $modules = ['scheduler', 'dblog'];
+ protected static $modules = [
+ 'scheduler',
+ 'dblog',
+ 'media',
+ 'commerce_product',
+ ];
/**
* The profile to install as a basis for testing.
@@ -38,9 +47,19 @@ abstract class SchedulerBrowserTestBase extends BrowserTestBase {
*/
protected function setUp(): void {
parent::setUp();
-
+ // Call the common set-up functions defined in the traits.
$this->schedulerSetUp();
-
+ // $this->getName() includes the test class and the dataProvider key. We can
+ // use this to save time and resources by avoiding calls to the media and
+ // product setup functions when they are not needed. The exception is the
+ // permissions tests, which use all entities for all tests.
+ $testName = $this->getName();
+ if (stristr($testName, 'media') || stristr($testName, 'permission')) {
+ $this->schedulerMediaSetUp();
+ }
+ if (stristr($this->getName(), 'product') || stristr($testName, 'permission')) {
+ $this->SchedulerCommerceProductSetUp();
+ }
}
}
diff --git a/tests/src/Functional/SchedulerDefaultTimeTest.php b/tests/src/Functional/SchedulerDefaultTimeTest.php
index ad2c512..82a1aa7 100644
--- a/tests/src/Functional/SchedulerDefaultTimeTest.php
+++ b/tests/src/Functional/SchedulerDefaultTimeTest.php
@@ -5,17 +5,15 @@ namespace Drupal\Tests\scheduler\Functional;
/**
* Tests the default time functionality.
*
+ * The test helper module scheduler_extras is used in testDefaultWithHiddenTime.
+ * To reduce complexity, and avoid having to create custom entity types, it acts
+ * on the standard test entity types. Hence the module is only enabled in that
+ * test, not via a protected static $modules declaration.
+ *
* @group scheduler
*/
class SchedulerDefaultTimeTest extends SchedulerBrowserTestBase {
- /**
- * Additional modules required.
- *
- * @var array
- */
- protected static $modules = ['scheduler_extras'];
-
/**
* The default time.
*
@@ -64,10 +62,13 @@ class SchedulerDefaultTimeTest extends SchedulerBrowserTestBase {
*
* This test covers the default scenario where the dates are optional and not
* required. A javascript test covers the cases where the dates are required.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testDefaultTime() {
+ public function testDefaultTime($entityTypeId, $bundle) {
$this->drupalLogin($this->schedulerUser);
$config = $this->config('scheduler.settings');
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
// We cannot easily test the full validation message as they contain the
// current time which can be one or two seconds in the past. The best we can
@@ -80,24 +81,28 @@ class SchedulerDefaultTimeTest extends SchedulerBrowserTestBase {
$config->set('allow_date_only', FALSE)->save();
// Test that entering a time is required.
+ $title = 'No time ' . $this->randomMachineName(8);
$edit = [
- 'title[0][value]' => 'No time ' . $this->randomMachineName(8),
+ "{$titleField}[0][value]" => $title,
'publish_on[0][value][date]' => $this->publishTime->format('Y-m-d'),
'unpublish_on[0][value][date]' => $this->unpublishTime->format('Y-m-d'),
];
- // Create a node and check that the expected error messages are shown.
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
+ // Create an entity and check that the expected error messages are shown.
+ $add_url = $this->entityAddUrl($entityTypeId, $bundle);
+ $this->drupalGet($add_url);
+ $this->submitForm($edit, 'Save');
// By default it is required to enter a time when scheduling content for
// publishing and for unpublishing.
- $this->assertSession()->pageTextNotMatches('/' . $edit['title[0][value]'] . ' is scheduled to be published .* and unpublished .*/');
+ $this->assertSession()->pageTextNotMatches('/' . $title . ' is scheduled to be published .* and unpublished .*/');
$this->assertSession()->pageTextContains($publish_validation_message);
$this->assertSession()->pageTextContains($unpublish_validation_message);
// Allow the user to enter only a date with no time.
$config->set('allow_date_only', TRUE)->save();
- // Create a node and check that the validation messages are not shown.
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
+ // Create an entity and check that the validation messages are not shown.
+ $this->drupalGet($add_url);
+ $this->submitForm($edit, 'Save');
$this->assertSession()->pageTextNotContains($publish_validation_message);
$this->assertSession()->pageTextNotContains($unpublish_validation_message);
@@ -105,66 +110,57 @@ class SchedulerDefaultTimeTest extends SchedulerBrowserTestBase {
$date_format_storage = $this->container->get('entity_type.manager')->getStorage('date_format');
$long_pattern = $date_format_storage->load('long')->getPattern();
- // Check that the scheduled information is shown after saving.
+ // Check that the scheduled information is shown after saving and that the
+ // time is correct.
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s and unpublished %s',
- $edit['title[0][value]'], $this->publishTime->format($long_pattern), $this->unpublishTime->format($long_pattern)));
+ $title, $this->publishTime->format($long_pattern), $this->unpublishTime->format($long_pattern)));
- // Protect this section in case the node was not created.
- if ($node = $this->drupalGetNodeByTitle($edit['title[0][value]'])) {
- // Check that the correct scheduled dates are stored in the node.
- $this->assertEquals($this->publishTime->getTimestamp(), (int) $node->publish_on->value, 'The node publish_on value is stored correctly.');
- $this->assertEquals($this->unpublishTime->getTimestamp(), (int) $node->unpublish_on->value, 'The node unpublish_on value is stored correctly.');
+ if ($entity = $this->getEntityByTitle($entityTypeId, $title)) {
+ // Check that the correct scheduled dates are stored in the entity.
+ $this->assertEquals($this->publishTime->getTimestamp(), (int) $entity->publish_on->value, 'The publish_on value is stored correctly.');
+ $this->assertEquals($this->unpublishTime->getTimestamp(), (int) $entity->unpublish_on->value, 'The unpublish_on value is stored correctly.');
// Check that the default time has been added to the form on edit.
- $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->drupalGet($entity->toUrl('edit-form'));
$this->assertSession()->FieldValueEquals('publish_on[0][value][time]', $this->defaultTime);
$this->assertSession()->FieldValueEquals('unpublish_on[0][value][time]', $this->defaultTime);
}
else {
- $this->fail('The expected node was not found.');
+ $this->fail('The expected entity was not found.');
}
}
/**
* Test that the default times are set if the form time elements are hidden.
+ *
+ * This test uses the 'scheduler_extras' helper module, which hides the time
+ * elements of both of the scheduler date input fields.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testDefaultTimeWithHiddenTime() {
- // Create a content type that will have both of the time form elements
- // hidden. See hook_form_node_form_alter() in scheduler_extras test module.
- $type = 'hidden_time';
- $typeName = 'Content with hidden times';
- /** @var NodeTypeInterface $nodetype */
- $nodetype = $this->drupalCreateContentType([
- 'type' => $type,
- 'name' => $typeName,
- ]);
-
- // Add scheduler functionality to the content type.
- $nodetype->setThirdPartySetting('scheduler', 'publish_enable', TRUE)
- ->setThirdPartySetting('scheduler', 'unpublish_enable', TRUE)
- ->save();
-
- // Log in as a user with permission to create and schedule this type.
- $this->drupalLogin($this->drupalCreateUser([
- 'create ' . $type . ' content',
- 'edit own ' . $type . ' content',
- 'delete own ' . $type . ' content',
- 'view own unpublished content',
- 'schedule publishing of nodes',
- 'view scheduled content',
- ]));
+ public function testDefaultWithHiddenTime($entityTypeId, $bundle) {
+ \Drupal::service('module_installer')->install(['scheduler_extras']);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $this->drupalLogin($this->schedulerUser);
// Allow the user to enter only a date with no time.
$this->config('scheduler.settings')->set('allow_date_only', TRUE)->save();
- // Test that entering a time is required.
+ // Define date values but no time values.
+ $title = 'Hidden Time Elements ' . $this->randomMachineName(8);
$edit = [
- 'title[0][value]' => 'Hidden Time Elements ' . $this->randomMachineName(8),
+ "{$titleField}[0][value]" => $title,
'publish_on[0][value][date]' => $this->publishTime->format('Y-m-d'),
'unpublish_on[0][value][date]' => $this->unpublishTime->format('Y-m-d'),
];
- // Create a node and check that the expected default time has been saved.
- $this->drupalPostForm('node/add/' . $type, $edit, 'Save');
+
+ // Create an entity and check that the time fields are hidden.
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->assertSession()->FieldExists('publish_on[0][value][date]');
+ $this->assertSession()->FieldExists('unpublish_on[0][value][date]');
+ $this->assertSession()->FieldNotExists('publish_on[0][value][time]');
+ $this->assertSession()->FieldNotExists('unpublish_on[0][value][time]');
+ $this->submitForm($edit, 'Save');
// Get the pattern of the 'long' default date format.
$date_format_storage = $this->container->get('entity_type.manager')->getStorage('date_format');
@@ -172,16 +168,15 @@ class SchedulerDefaultTimeTest extends SchedulerBrowserTestBase {
// Check that the message has the correct default time after saving.
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s and unpublished %s',
- $edit['title[0][value]'], $this->publishTime->format($long_pattern), $this->unpublishTime->format($long_pattern)));
+ $title, $this->publishTime->format($long_pattern), $this->unpublishTime->format($long_pattern)));
- // Protect this section in case the node was not created.
- if ($node = $this->drupalGetNodeByTitle($edit['title[0][value]'])) {
+ if ($entity = $this->getEntityByTitle($entityTypeId, $title)) {
// Check that the correct scheduled dates are stored in the node.
- $this->assertEquals($this->publishTime->getTimestamp(), (int) $node->publish_on->value, 'The node publish_on value is stored correctly.');
- $this->assertEquals($this->unpublishTime->getTimestamp(), (int) $node->unpublish_on->value, 'The node unpublish_on value is stored correctly.');
+ $this->assertEquals($this->publishTime->getTimestamp(), (int) $entity->publish_on->value, 'The publish_on value is stored correctly.');
+ $this->assertEquals($this->unpublishTime->getTimestamp(), (int) $entity->unpublish_on->value, 'The unpublish_on value is stored correctly.');
}
else {
- $this->fail('The expected node was not found.');
+ $this->fail('The expected entity was not found.');
}
}
diff --git a/tests/src/Functional/SchedulerDeleteEntityTest.php b/tests/src/Functional/SchedulerDeleteEntityTest.php
new file mode 100644
index 0000000..f2ad0e2
--- /dev/null
+++ b/tests/src/Functional/SchedulerDeleteEntityTest.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+/**
+ * Tests deletion of entities enabled for Scheduler.
+ *
+ * This checks how the deletion of an entity interacts with the Scheduler
+ * 'required' options and scheduled dates in the past.
+ *
+ * @group scheduler
+ */
+class SchedulerDeleteEntityTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Tests the deletion of an entity when the scheduler dates are required.
+ *
+ * Check that it is possible to delete an entity that does not have a
+ * publishing date set, when scheduled publishing is required.
+ * Likewise for unpublishing.
+ *
+ * @see https://www.drupal.org/project/scheduler/issues/1614880
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testDeleteEntityWhenSchedulingIsRequired($entityTypeId, $bundle) {
+ // Log in.
+ $this->drupalLogin($this->adminUser);
+
+ // Create a published and an unpublished entity, with no scheduled dates.
+ $published_entity = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ ]);
+ $unpublished_entity = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ ]);
+
+ // Make scheduled publishing and unpublishing required.
+ $bundle_field_name = $published_entity->getEntityType()->get('entity_keys')['bundle'];
+ $published_entity->$bundle_field_name->entity->setThirdPartySetting('scheduler', 'publish_required', TRUE)
+ ->setThirdPartySetting('scheduler', 'unpublish_required', TRUE)
+ ->save();
+ $entity_type_label = $published_entity->getEntityType()->getSingularLabel();
+
+ // Check that deleting the entity does not throw form validation errors.
+ $this->drupalGet($published_entity->toUrl('edit-form'));
+ $this->clickLink('Delete');
+ // The text 'error message' is used in a header h2 html tag which is
+ // normally made hidden from browsers but will be in the page source.
+ // It is also good when testing for the absense of something to also test
+ // for the presence of text, hence the second assertion for each check.
+ $this->assertSession()->pageTextNotContains('Error message');
+ $this->assertSession()->pageTextContains("Are you sure you want to delete the $entity_type_label {$published_entity->label()}");
+
+ // Do the same test for the unpublished entity.
+ $this->drupalGet($unpublished_entity->toUrl('edit-form'));
+ $this->clickLink('Delete');
+ $this->assertSession()->pageTextNotContains('Error message');
+ $this->assertSession()->pageTextContains("Are you sure you want to delete the $entity_type_label {$unpublished_entity->label()}");
+ }
+
+ /**
+ * Tests the deletion of scheduled entities.
+ *
+ * Check that entities can be deleted with no validation errors even if the
+ * dates are in the past.
+ *
+ * @see https://www.drupal.org/project/scheduler/issues/2627370
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testDeleteEntityWithPastDates($entityTypeId, $bundle) {
+ // Log in.
+ $this->drupalLogin($this->adminUser);
+
+ // Create entities with publish_on and unpublish_on dates in the past.
+ $published_entity = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'unpublish_on' => strtotime('- 2 day'),
+ ]);
+ $unpublished_entity = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'publish_on' => strtotime('- 2 day'),
+ ]);
+ $entity_type_label = $published_entity->getEntityType()->getSingularLabel();
+
+ // Attempt to delete the published entity and check for no validation error.
+ $this->drupalGet($published_entity->toUrl('edit-form'));
+ $this->clickLink('Delete');
+ $this->assertSession()->pageTextNotContains('Error message');
+ $this->assertSession()->pageTextContains("Are you sure you want to delete the $entity_type_label {$published_entity->label()}");
+
+ // Attempt to delete the unpublished entity and check no validation error.
+ $this->drupalGet($unpublished_entity->toUrl('edit-form'));
+ $this->clickLink('Delete');
+ $this->assertSession()->pageTextNotContains('Error message');
+ $this->assertSession()->pageTextContains("Are you sure you want to delete the $entity_type_label {$unpublished_entity->label()}");
+ }
+
+}
diff --git a/tests/src/Functional/SchedulerDeleteNodeTest.php b/tests/src/Functional/SchedulerDeleteNodeTest.php
deleted file mode 100644
index bd7a16f..0000000
--- a/tests/src/Functional/SchedulerDeleteNodeTest.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-namespace Drupal\Tests\scheduler\Functional;
-
-/**
- * Tests deletion of nodes enabled for Scheduler.
- *
- * This checks how the deletion of a node interacts with the Scheduler
- * 'required' options and scheduled dates in the past.
- *
- * @group scheduler
- */
-class SchedulerDeleteNodeTest extends SchedulerBrowserTestBase {
-
- /**
- * Tests the deletion of a scheduled node.
- *
- * Check that it is possible to delete a node that does not have a publishing
- * date set, when scheduled publishing is required. Likewise for unpublishing.
- *
- * @see https://drupal.org/node/1614880
- */
- public function testDeleteNodeWhenSchedulingIsRequired() {
- // Log in.
- $this->drupalLogin($this->adminUser);
-
- // Create a published and an unpublished node, both without scheduled dates.
- $published_node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => 1,
- ]);
- $unpublished_node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => 0,
- ]);
-
- // Make scheduled publishing and unpublishing required.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_required', TRUE)
- ->setThirdPartySetting('scheduler', 'unpublish_required', TRUE)
- ->save();
-
- // Check that deleting the nodes does not throw form validation errors.
- $this->drupalGet('node/' . $published_node->id() . '/edit');
- $this->clickLink('Delete');
- // The text 'error message' is used in a header h2 html tag which is
- // normally made hidden from browsers but will be in the page source.
- // It is also good when testing for the absense of something to also test
- // for the presence of text, hence the second assertion for each check.
- $this->assertSession()->pageTextNotContains('Error message');
- $this->assertSession()->pageTextContains('Are you sure you want to delete the content');
-
- // Do the same test for the unpublished node.
- $this->drupalGet('node/' . $unpublished_node->id() . '/edit');
- $this->clickLink('Delete');
- $this->assertSession()->pageTextNotContains('Error message');
- $this->assertSession()->pageTextContains('Are you sure you want to delete the content');
- }
-
- /**
- * Tests the deletion of a scheduled node.
- *
- * Check that nodes can be deleted with no validation errors if the dates are
- * in the past.
- *
- * @see http://drupal.org/node/2627370
- */
- public function testDeleteNodeWithPastDates() {
- // Log in.
- $this->drupalLogin($this->adminUser);
-
- // Create nodes with publish_on and unpublish_on dates in the past.
- $published_node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => TRUE,
- 'unpublish_on' => strtotime('- 2 day'),
- ]);
- $unpublished_node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- 'publish_on' => strtotime('- 2 day'),
- ]);
-
- // Attempt to delete the published node and check for no validation error.
- $this->drupalGet('node/' . $published_node->id() . '/edit');
- $this->clickLink('Delete');
- $this->assertSession()->pageTextNotContains('Error message');
- $this->assertSession()->pageTextContains('Are you sure you want to delete the content');
-
- // Attempt to delete the unpublished node and check for no validation error.
- $this->drupalGet('node/' . $unpublished_node->id() . '/edit');
- $this->clickLink('Delete');
- $this->assertSession()->pageTextNotContains('Error message');
- $this->assertSession()->pageTextContains('Are you sure you want to delete the content');
- }
-
-}
diff --git a/tests/src/Functional/SchedulerDevelGenerateTest.php b/tests/src/Functional/SchedulerDevelGenerateTest.php
index b483441..5449c47 100644
--- a/tests/src/Functional/SchedulerDevelGenerateTest.php
+++ b/tests/src/Functional/SchedulerDevelGenerateTest.php
@@ -26,51 +26,51 @@ class SchedulerDevelGenerateTest extends SchedulerBrowserTestBase {
protected function setUp(): void {
parent::setUp();
- // Create a user with devel permission. Only 'administer devel_generate' is
- // actually required for these tests, but the others are useful.
- // 'access content overview' is needed for /admin/content (but it is empty)
- // 'access content' is required to actually see the content list data.
- // 'view scheduled content' is required for /admin/content/scheduled.
- $this->develUser = $this->drupalCreateUser([
+ // Add Devel Generate permission to the admin user.
+ $this->addPermissionsToUser($this->adminUser, [
'administer devel_generate',
- 'view scheduled content',
- 'access content',
- 'access content overview',
]);
+
}
/**
- * Helper function to count scheduled nodes and assert the expected number.
+ * Helper function to count scheduled entities and assert the expected number.
*
* @param string $type
- * The machine-name for the content type to be checked.
- * @param string $field
+ * The machine-name for the entity type to be checked.
+ * @param string $bundle_field
+ * The name of the field which contains the bundle.
+ * @param string $bundle
+ * The machine-name for the bundle/content type to be checked.
+ * @param string $scheduler_field
* The field name to count, either 'publish_on' or 'unpublish_on'.
- * @param int $num_nodes
- * The total number of nodes that should exist.
+ * @param int $num_total
+ * The total number of entities that should exist.
* @param int $num_scheduled
- * The number of those nodes which should be scheduled with a $field.
+ * The number of entities which should have a value in $scheduler_field.
* @param int $time_range
* Optional time range from the devel form. The generated scheduler dates
* should be in a range of +/- this value from the current time.
*/
- protected function countScheduledNodes($type, $field, $num_nodes, $num_scheduled, $time_range = NULL) {
- // Check that the expected number of nodes have been created.
- $count = $this->nodeStorage->getQuery()
- ->condition('type', $type)
+ protected function countScheduledEntities($type, $bundle_field, $bundle, $scheduler_field, $num_total, $num_scheduled, $time_range = NULL) {
+ $storage = $this->entityStorageObject($type);
+
+ // Check that the expected number of entities have been created.
+ $count = $storage->getQuery()
+ ->condition($bundle_field, $bundle)
->count()
->execute();
- $this->assertEquals($num_nodes, $count, sprintf('The expected number of %s is %s, found %s', $type, $num_nodes, $count));
+ $this->assertEquals($num_total, $count, sprintf('The expected number of %s %s is %s, found %s', $bundle, $type, $num_total, $count));
- // Check that the expected number of nodes have been scheduled.
- $count = $this->nodeStorage->getQuery()
- ->condition('type', $type)
- ->exists($field)
+ // Check that the expected number of entities have been scheduled.
+ $count = $storage->getQuery()
+ ->condition($bundle_field, $bundle)
+ ->exists($scheduler_field)
->count()
->execute();
- $this->assertEquals($num_scheduled, $count, sprintf('The expected number of scheduled %s is %s, found %s', $field, $num_scheduled, $count));
+ $this->assertEquals($num_scheduled, $count, sprintf('The expected number of %s %s with scheduled %s is %s, found %s', $bundle, $type, $scheduler_field, $num_total, $count));
- if (isset($time_range)) {
+ if (isset($time_range) && $num_scheduled > 0) {
// Define the minimum and maximum times that we expect the scheduled dates
// to be within. REQUEST_TIME remains static for the duration of this test
// but even though devel_generate also uses uses REQUEST_TIME this will
@@ -80,98 +80,118 @@ class SchedulerDevelGenerateTest extends SchedulerBrowserTestBase {
$min = $this->requestTime - $time_range;
$max = time() + $time_range;
- $query = $this->nodeStorage->getAggregateQuery();
+ $query = $storage->getAggregateQuery();
$result = $query
- ->condition('type', $type)
- ->aggregate($field, 'min')
- ->aggregate($field, 'max')
+ ->condition($bundle_field, $bundle)
+ ->aggregate($scheduler_field, 'min')
+ ->aggregate($scheduler_field, 'max')
->execute();
- $min_found = $result[0]["{$field}_min"];
- $max_found = $result[0]["{$field}_max"];
+ $min_found = $result[0]["{$scheduler_field}_min"];
+ $max_found = $result[0]["{$scheduler_field}_max"];
- // Assert that the found values are within the expcted range.
- $this->assertGreaterThanOrEqual($min, $min_found, sprintf('The minimum value for %s is %s, smaller than the expected %s', $field, $this->dateFormatter->format($min_found, 'custom', 'j M, H:i:s'), $this->dateFormatter->format($min, 'custom', 'j M, H:i:s')));
- $this->assertLessThanOrEqual($max, $max_found, sprintf('The maximum value for %s is %s which is larger than expected %s', $field, $this->dateFormatter->format($max_found, 'custom', 'j M, H:i:s'), $this->dateFormatter->format($max, 'custom', 'j M, H:i:s')));
+ // Assert that the found values are within the expected range.
+ $this->assertGreaterThanOrEqual($min, $min_found, sprintf('The minimum value found for %s is %s, earlier than the expected %s', $scheduler_field, $this->dateFormatter->format($min_found, 'custom', 'j M, H:i:s'), $this->dateFormatter->format($min, 'custom', 'j M, H:i:s')));
+ $this->assertLessThanOrEqual($max, $max_found, sprintf('The maximum value found for %s is %s, later than the expected %s', $scheduler_field, $this->dateFormatter->format($max_found, 'custom', 'j M, H:i:s'), $this->dateFormatter->format($max, 'custom', 'j M, H:i:s')));
}
}
/**
- * Test the functionality that Scheduler adds during content generation.
+ * Test the functionality that Scheduler adds during entity generation.
+ *
+ * @dataProvider dataDevelGenerate()
*/
- public function testDevelGenerate() {
- $this->drupalLogin($this->develUser);
+ public function testDevelGenerate($entityTypeId, $url_part, $enabled) {
+ $this->drupalLogin($this->adminUser);
+ $entityType = $this->entityTypeObject($entityTypeId, $enabled ? NULL : 'non-enabled');
+ $bundle = $entityType->id();
+ $bundle_field = $this->container->get('entity_type.manager')
+ ->getDefinition($entityTypeId)->get('entity_keys')['bundle'];
// Use the minimum required settings to see what happens when everything
// else is left as default.
$generate_settings = [
- "edit-node-types-$this->type" => TRUE,
+ "{$entityTypeId}_types[$bundle]" => TRUE,
];
- $this->drupalPostForm('admin/config/development/generate/content', $generate_settings, 'Generate');
- // Display the full content list and the scheduled list. Calls to these
- // pages are for information and debug only. They could be removed.
- $this->drupalGet('admin/content');
- $this->drupalGet('admin/content/scheduled');
+ $this->drupalGet("admin/config/development/generate/{$url_part}");
+ $this->submitForm($generate_settings, 'Generate');
+ // Display the full content list and the scheduled list for the entity type
+ // being generated. Calls to these pages are for information and debug only.
+ if ($entityTypeId == 'media') {
+ $admin_content_urls = ['admin/content/media', 'admin/content/media/scheduled'];
+ }
+ else {
+ $admin_content_urls = ['admin/content', 'admin/content/scheduled'];
+ }
+ foreach ($admin_content_urls as $url) {
+ $this->drupalGet($url);
+ }
// Delete all content for this type and generate new content with only
// publish-on dates. Use 100% as this is how we can count the expected
- // number of scheduled nodes. The time range of 3600 is one hour.
- // The number of nodes has to be lower than 50 until Devel issue with
+ // number of scheduled entities. The time range of 3600 is one hour.
+ // The number of entities has to be lower than 50 until the Devel issue with
// undefined index 'users' is available and we switch to using 8.x-3.0
// See https://www.drupal.org/project/devel/issues/3076613
$generate_settings = [
- "edit-node-types-$this->type" => TRUE,
+ "{$entityTypeId}_types[$bundle]" => TRUE,
'num' => 40,
'kill' => TRUE,
'time_range' => 3600,
'scheduler_publishing' => 100,
'scheduler_unpublishing' => 0,
];
- $this->drupalPostForm('admin/config/development/generate/content', $generate_settings, 'Generate');
- $this->drupalGet('admin/content');
- $this->drupalGet('admin/content/scheduled');
+ $this->drupalGet("admin/config/development/generate/{$url_part}");
+ $this->submitForm($generate_settings, 'Generate');
+ // Display the full content list and the scheduled content list.
+ foreach ($admin_content_urls as $url) {
+ $this->drupalGet($url);
+ }
- // Check we have the expected number of nodes scheduled for publishing only
- // and verify that that the dates are within the time range specified.
- $this->countScheduledNodes($this->type, 'publish_on', 40, 40, $generate_settings['time_range']);
- $this->countScheduledNodes($this->type, 'unpublish_on', 40, 0);
+ // Check we have the expected number of entities scheduled for publishing
+ // only, and verify that that the dates are within the time range specified.
+ $this->countScheduledEntities($entityTypeId, $bundle_field, $bundle, 'publish_on', 40, $enabled ? 40 : 0, $generate_settings['time_range']);
+ $this->countScheduledEntities($entityTypeId, $bundle_field, $bundle, 'unpublish_on', 40, 0);
// Do similar for unpublish_on date. Delete all then generate new content
// with only unpublish-on dates. Time range 86400 is one day.
$generate_settings = [
- "edit-node-types-$this->type" => TRUE,
+ "{$entityTypeId}_types[$bundle]" => TRUE,
'num' => 30,
'kill' => TRUE,
'time_range' => 86400,
'scheduler_publishing' => 0,
'scheduler_unpublishing' => 100,
];
- $this->drupalPostForm('admin/config/development/generate/content', $generate_settings, 'Generate');
- $this->drupalGet('admin/content');
- $this->drupalGet('admin/content/scheduled');
+ $this->drupalGet("admin/config/development/generate/{$url_part}");
+ $this->submitForm($generate_settings, 'Generate');
+ // Display the full content list and the scheduled content list.
+ foreach ($admin_content_urls as $url) {
+ $this->drupalGet($url);
+ }
- // Check we have the expected number of nodes scheduled for unpublishing
+ // Check we have the expected number of entities scheduled for unpublishing
// only, and verify that that the dates are within the time range specified.
- $this->countScheduledNodes($this->type, 'publish_on', 30, 0);
- $this->countScheduledNodes($this->type, 'unpublish_on', 30, 30, $generate_settings['time_range']);
+ $this->countScheduledEntities($entityTypeId, $bundle_field, $bundle, 'publish_on', 30, 0);
+ $this->countScheduledEntities($entityTypeId, $bundle_field, $bundle, 'unpublish_on', 30, $enabled ? 30 : 0, $generate_settings['time_range']);
- // Generate new content using the type which is not enabled for Scheduler.
- // The nodes should be created but no dates should be added even though the
- // scheduler values are set to 100.
- $non_scheduler_id = $this->nonSchedulerNodeType->id();
- $generate_settings = [
- "edit-node-types-$non_scheduler_id" => TRUE,
- 'num' => 20,
- 'kill' => TRUE,
- 'scheduler_publishing' => 100,
- 'scheduler_unpublishing' => 100,
+ }
+
+ /**
+ * Provides data for testDevelGenerate().
+ *
+ * @return array
+ * Each array item has the values:
+ * [entity type id, generate url part, enabled for Scheduler].
+ */
+ public function dataDevelGenerate() {
+ $data = [
+ '#node-1' => ['node', 'content', TRUE],
+ '#node-2' => ['node', 'content', FALSE],
+ '#media-1' => ['media', 'media', TRUE],
+ '#media-2' => ['media', 'media', FALSE],
];
- $this->drupalPostForm('admin/config/development/generate/content', $generate_settings, 'Generate');
- $this->drupalGet('admin/content');
- $this->drupalGet('admin/content/scheduled');
- // Check we have the expected number of nodes but that none are scheduled.
- $this->countScheduledNodes($non_scheduler_id, 'publish_on', 20, 0);
- $this->countScheduledNodes($non_scheduler_id, 'unpublish_on', 20, 0);
+ return $data;
}
}
diff --git a/tests/src/Functional/SchedulerDrushTest.php b/tests/src/Functional/SchedulerDrushTest.php
index 664fcad..04576d3 100644
--- a/tests/src/Functional/SchedulerDrushTest.php
+++ b/tests/src/Functional/SchedulerDrushTest.php
@@ -14,7 +14,7 @@ class SchedulerDrushTest extends SchedulerBrowserTestBase {
use DrushTestTrait;
/**
- * Tests the Scheduler Drush messages.
+ * Tests the messages from Scheduler Drush cron.
*/
public function testDrushCronMessages() {
// Run the plain command using the full scheduler:cron command name, and
@@ -40,22 +40,22 @@ class SchedulerDrushTest extends SchedulerBrowserTestBase {
}
/**
- * Tests scheduled publishing via Drush command.
+ * Tests scheduled publishing and unpublishing of entities via Drush.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testDrushCronPublishing() {
- // Create a node which is scheduled for publishing.
- $title1 = $this->randomMachineName(20);
- $this->drupalCreateNode([
+ public function testDrushCronPublishing($entityTypeId, $bundle) {
+ // Create an entity which is scheduled for publishing.
+ $title1 = $this->randomMachineName(20) . ' for publishing';
+ $entity = $this->createEntity($entityTypeId, $bundle, [
'title' => $title1,
- 'type' => $this->type,
'publish_on' => strtotime('-3 hours'),
]);
- // Create a node which is scheduled for unpublishing.
- $title2 = $this->randomMachineName(20);
- $this->drupalCreateNode([
+ // Create an entity which is scheduled for unpublishing.
+ $title2 = $this->randomMachineName(20) . ' for unpublishing';
+ $entity = $this->createEntity($entityTypeId, $bundle, [
'title' => $title2,
- 'type' => $this->type,
'unpublish_on' => strtotime('-2 hours'),
]);
@@ -63,8 +63,22 @@ class SchedulerDrushTest extends SchedulerBrowserTestBase {
// and unpublishing messages are found.
$this->drush('scheduler:cron');
$messages = $this->getErrorOutput();
- $this->assertStringContainsString(sprintf('%s: scheduled publishing of %s', $this->typeName, $title1), $messages, 'Scheduled publishing message not found', TRUE);
- $this->assertStringContainsString(sprintf('%s: scheduled unpublishing of %s', $this->typeName, $title2), $messages, 'Scheduled unpublishing message not found', TRUE);
+ $bundle_field = $entity->getEntityType()->get('entity_keys')['bundle'];
+ $type_label = $entity->$bundle_field->entity->label();
+ $this->assertStringContainsString(sprintf('%s: scheduled publishing of %s', $type_label, $title1), $messages, 'Scheduled publishing message not found', TRUE);
+ $this->assertStringContainsString(sprintf('%s: scheduled unpublishing of %s', $type_label, $title2), $messages, 'Scheduled unpublishing message not found', TRUE);
+ }
+
+ /**
+ * Tests the Entity Update command.
+ */
+ public function testDrushEntityUpdate() {
+ // This test could be expanded to check the full functionality of the
+ // entityUpdate() function. But initially, just call the function to check
+ // that it runs, and produces the 'nothing to update' message.
+ $this->drush('scheduler:entity-update');
+ $messages = $this->getErrorOutput();
+ $this->assertStringContainsString('Scheduler entity update - nothing to update', $messages, 'Error! Entity update message not found', TRUE);
}
}
diff --git a/tests/src/Functional/SchedulerEntityAccessTest.php b/tests/src/Functional/SchedulerEntityAccessTest.php
new file mode 100644
index 0000000..acaf8e3
--- /dev/null
+++ b/tests/src/Functional/SchedulerEntityAccessTest.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+/**
+ * Tests that Scheduler cron has full access to the scheduled entities.
+ *
+ * This test uses a additional test module 'scheduler_access_test' which has a
+ * custom entity access definition to deny viewing of all entities by any user
+ * except user 1.
+ *
+ * The purpose of checking for '403' is only to demonstrate that the helper
+ * module is doing its thing, it is not testing any part of the Scheduler
+ * functionality. If we tested with an anonymous visitor then both the published
+ * and unpublished entities would give 403 but the unpublished entity would
+ * return this regardless of what the helper module was doing. Likewise if we
+ * run the test with a logged in user who does not have 'view own unpublished..'
+ * then the unpublished entity would give 403 regardless. However, if the user
+ * does have 'view own unpublished ..' then due to the design of checkAccess()
+ * within NodeAccessControlHandler this entirely takes precedence and overrides
+ * any prevention of access attempted via contrib hook_node_access_records() and
+ * hook_node_grants(). It is clearer in the dblog output if this test is run
+ * using a logged-in user rather than the anonymous user, and hence create a new
+ * user who does not have the permission 'view own unpublished {type}'.
+ *
+ * @group scheduler
+ */
+class SchedulerEntityAccessTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Additional modules required.
+ *
+ * @var array
+ */
+ protected static $modules = ['scheduler_access_test'];
+
+ /**
+ * Tests Scheduler cron functionality when access to the entity is denied.
+ *
+ * @dataProvider dataEntityAccess()
+ */
+ public function testEntityAccess($entityTypeId, $bundle, $field, $status) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ // scheduler_access_test_install() sets node_access_needs_rebuild(TRUE) and
+ // this works when testing the module interactively, but in a phpunit run
+ // the node access table is not rebuilt. Hence do that explicitly here.
+ node_access_rebuild();
+
+ // Login as a user who is only able to view the published entities.
+ $this->drupalLogin($this->drupalCreateUser());
+
+ // Create an entity with the necessary scheduler date.
+ $process = $status ? 'unpublishing' : 'publishing';
+ $settings = [
+ 'status' => $status,
+ 'title' => "$entityTypeId $bundle for $process",
+ $field => $this->requestTime + 1,
+ ];
+ $entity = $this->createEntity($entityTypeId, $bundle, $settings);
+ $this->drupalGet($entity->toUrl());
+ // Before running cron, viewing the entity should give "403 Not Authorized"
+ // regardless of whether it is published or unpublished.
+ $this->assertSession()->statusCodeEquals(403);
+
+ // Delay so that the date entered is now in the past, then run cron.
+ sleep(2);
+ $this->cronRun();
+
+ // Reload the entity.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ // Check that the entity has been published or unpublished as required.
+ $this->assertTrue($entity->isPublished() === !$status, "Scheduled $process of $entityTypeId via cron.");
+ // Check that the entity is still not viewable.
+ $this->drupalGet($entity->toUrl());
+ // After cron, viewing the entity should still give "403 Not Authorized".
+ $this->assertSession()->statusCodeEquals(403);
+
+ // Log in as admin and check that the dblog cron message is shown.
+ $this->drupalLogin($this->adminUser);
+ $this->drupalGet('admin/reports/dblog');
+ $this->assertSession()->pageTextContains($this->entityTypeObject($entityTypeId)->label() . ": scheduled $process");
+ }
+
+ /**
+ * Provides data for testEntityAccess.
+ *
+ * The data in dataStandardEntityTypes() is expanded to test each entity type
+ * for both publishing and unpublishing.
+ *
+ * @return array
+ * Each row has values: [entity type id, bundle id, field name, status].
+ */
+ public function dataEntityAccess() {
+ $data = [];
+ foreach ($this->dataStandardEntityTypes() as $key => $values) {
+ // Media and Commerce Products do not have a hook access and grant system
+ // like Nodes so the test would fail for non-node entities.
+ // @todo Investigate how scheduler_access_test module can be expanded to
+ // deny access to Media and Products using another method.
+ if ($values[0] == 'media' || $values[0] == 'commerce_product') {
+ continue;
+ }
+ $data["$key-1"] = array_merge($values, ['publish_on', FALSE]);
+ $data["$key-1"] = array_merge($values, ['unpublish_on', TRUE]);
+ }
+ return $data;
+ }
+
+}
diff --git a/tests/src/Functional/SchedulerEventsTest.php b/tests/src/Functional/SchedulerEventsTest.php
new file mode 100644
index 0000000..07a5016
--- /dev/null
+++ b/tests/src/Functional/SchedulerEventsTest.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+/**
+ * Tests the six generic events that Scheduler dispatches.
+ *
+ * @group scheduler
+ */
+class SchedulerEventsTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Additional modules required.
+ *
+ * @var array
+ *
+ * @todo 'menu_ui' is in the exported node.type definition, and 'path' is in
+ * the entity_form_display. Could these be removed from the config files and
+ * then not needed here?
+ */
+ protected static $modules = ['scheduler_api_test', 'menu_ui', 'path'];
+
+ /**
+ * Covers six events for nodes.
+ *
+ * The events allow other modules to react to the Scheduler process being run.
+ * The API test implementations of the event listeners alter the nodes
+ * 'promote' and 'sticky' settings and changes the title.
+ */
+ public function testNodeEvents() {
+ $this->drupalLogin($this->schedulerUser);
+
+ // Create a test node.
+ $settings = [
+ 'publish_on' => strtotime('-1 day'),
+ 'type' => $this->type,
+ 'promote' => FALSE,
+ 'sticky' => FALSE,
+ 'title' => 'API TEST node action',
+ ];
+ $node = $this->drupalCreateNode($settings);
+
+ // Check that the 'sticky' and 'promote' fields are off for the new node.
+ $this->assertFalse($node->isSticky(), 'The unpublished node is not sticky.');
+ $this->assertFalse($node->isPromoted(), 'The unpublished node is not promoted.');
+
+ // Run cron and check that the events have been dispatched correctly, by
+ // verifying that the node is now sticky and has been promoted.
+ scheduler_cron();
+ $this->nodeStorage->resetCache([$node->id()]);
+ $node = $this->nodeStorage->load($node->id());
+ $this->assertTrue($node->isSticky(), 'API event "PRE_PUBLISH" has changed the node to sticky.');
+ $this->assertTrue($node->isPromoted(), 'API event "PUBLISH" has changed the node to promoted.');
+
+ // Now set a date for unpublishing the node. Ensure 'sticky' and 'promote'
+ // are set, so that the assertions are not affected by any failures above.
+ $node->set('unpublish_on', strtotime('-1 day'))
+ ->set('sticky', TRUE)->set('promote', TRUE)->save();
+
+ // Run cron and check that the events have been dispatched correctly, by
+ // verifying that the node is no longer sticky and not promoted.
+ scheduler_cron();
+ $this->nodeStorage->resetCache([$node->id()]);
+ $node = $this->nodeStorage->load($node->id());
+ $this->assertFalse($node->isSticky(), 'API event "PRE_UNPUBLISH" has changed the node to not sticky.');
+ $this->assertFalse($node->isPromoted(), 'API event "UNPUBLISH" has changed the node to not promoted.');
+
+ // Turn on immediate publication when a publish date is in the past.
+ $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
+
+ // Ensure 'sticky' and 'promote' are not set, so that the assertions are not
+ // affected by any failures above.
+ $node->set('sticky', FALSE)->set('promote', FALSE)->save();
+
+ // Edit the node and set a publish-on date in the past.
+ $edit = [
+ 'publish_on[0][value][date]' => date('Y-m-d', strtotime('-2 day', $this->requestTime)),
+ 'publish_on[0][value][time]' => date('H:i:s', strtotime('-2 day', $this->requestTime)),
+ ];
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->submitForm($edit, 'Save');
+ // Verify that the values have been altered as expected.
+ $this->nodeStorage->resetCache([$node->id()]);
+ $node = $this->nodeStorage->load($node->id());
+ $this->assertTrue($node->isSticky(), 'API event "PRE_PUBLISH_IMMEDIATELY" has changed the node to sticky.');
+ $this->assertTrue($node->isPromoted(), 'API event "PUBLISH_IMMEDIATELY" has changed the node to promoted.');
+ $this->assertEquals('Published immediately', $node->title->value, 'API action "PUBLISH_IMMEDIATELY" has changed the node title correctly.');
+ }
+
+ /**
+ * Tests six scheduler events for entity types other than node.
+ *
+ * Currently this covers Media and Commerce Products.
+ *
+ * @dataProvider dataSchedulerEvents()
+ */
+ public function testSchedulerEvents($entityTypeId, $bundle) {
+ $this->drupalLogin($this->schedulerUser);
+ $storage = $this->entityStorageObject($entityTypeId);
+ $title_prefix = strtoupper("API TEST $entityTypeId");
+
+ // Create an entity of the required type, scheduled for publishing.
+ $entity = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => $title_prefix,
+ 'publish_on' => strtotime('-1 day'),
+ ]);
+ // Run cron and check that the events have been dispatched correctly. The
+ // name is first changed by a PRE_PUBLISH event subscriber, then a second
+ // time by a PUBLISH event watcher. Checking the final value tests both.
+ scheduler_cron();
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertEquals($entity->label(), $title_prefix . ' - altered a second time by "PUBLISH" event');
+
+ // Create an entity of the required type, scheduled for unpublishing.
+ $entity = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => $title_prefix,
+ 'unpublish_on' => strtotime('-1 day'),
+ ]);
+ // Run cron and check that the events have been dispatched correctly.
+ scheduler_cron();
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertEquals($entity->label(), $title_prefix . ' - altered a second time by "UNPUBLISH" event');
+
+ // Turn on immediate publishing when a publish date is in the past.
+ $this->entityTypeObject($entityTypeId, $bundle)
+ ->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
+
+ // Create an unpublished and unscheduled entity.
+ $entity = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => $title_prefix,
+ 'status' => FALSE,
+ ]);
+ // Edit the media item, setting a publish-on date in the past.
+ $edit = [
+ 'publish_on[0][value][date]' => date('Y-m-d', strtotime('-2 day', $this->requestTime)),
+ 'publish_on[0][value][time]' => date('H:i:s', strtotime('-2 day', $this->requestTime)),
+ ];
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
+ // Verify that the values have been altered as expected, without cron.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertEquals($entity->label(), $title_prefix . ' - altered a second time by "PUBLISH_IMMEDIATELY" event');
+ }
+
+ /**
+ * Provides test data for scheduler events test.
+ *
+ * The original node events test is different (and no benefit in re-writing)
+ * so this test excludes the node entity type.
+ *
+ * @return array
+ * Each array item has the values: [entity type id, bundle id].
+ */
+ public function dataSchedulerEvents() {
+ $data = $this->dataStandardEntityTypes();
+ unset($data['#node']);
+ return $data;
+ }
+
+}
diff --git a/tests/src/Functional/SchedulerFieldsDisplayTest.php b/tests/src/Functional/SchedulerFieldsDisplayTest.php
index 806ca70..08e164e 100644
--- a/tests/src/Functional/SchedulerFieldsDisplayTest.php
+++ b/tests/src/Functional/SchedulerFieldsDisplayTest.php
@@ -2,6 +2,8 @@
namespace Drupal\Tests\scheduler\Functional;
+use Drupal\Core\Url;
+
/**
* Tests the display of date entry fields and form elements.
*
@@ -22,68 +24,84 @@ class SchedulerFieldsDisplayTest extends SchedulerBrowserTestBase {
protected function setUp(): void {
parent::setUp();
- // Create a custom user with admin permissions but also permission to use
- // the field_ui module 'node form display' tab.
- $this->adminUser2 = $this->drupalCreateUser([
- 'access content',
- 'administer content types',
+ // Give adminUser the permissions to use the field_ui 'manage form display'
+ // tab for each entity type being tested.
+ $this->addPermissionsToUser($this->adminUser, [
'administer node form display',
- 'create ' . $this->type . ' content',
- 'schedule publishing of nodes',
+ 'administer media form display',
+ 'administer commerce_product form display',
]);
}
/**
* Tests date input is displayed as vertical tab or an expandable fieldset.
*
- * This test covers scheduler_form_node_form_alter().
+ * This test covers _scheduler_entity_form_alter().
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testVerticalTabOrFieldset() {
+ public function testVerticalTabOrFieldset($entityTypeId, $bundle) {
$this->drupalLogin($this->adminUser);
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
/** @var \Drupal\Tests\WebAssert $assert */
$assert = $this->assertSession();
+ // For rendering of vertical tabs, node and media entity forms have a div
+ // with class 'js-form-type-vertical-tabs'. However, the Commerce Product
+ // module does things differently and does not have this class, but instead
+ // has a class 'layout-region-product-secondary' (for vertical tabs) and
+ // 'layout-region-product-main' if in the main form not in vertical tabs. So
+ // to cover all entity types we can check for either of these classes as an
+ // ancestor of the 'edit-scheduler-settings' section.
+ $vertical_tab_xpath = '//div[contains(@class, "form-type-vertical-tabs") or contains(@class, "-secondary")]//details[@id = "edit-scheduler-settings"]';
+
+ // The 'open' and 'closed' xpath searches do apply to vertical tabs, even if
+ // the theme does not actually make use of it (such as in Bartik and Stark).
+ $details_open_xpath = '//details[@id = "edit-scheduler-settings" and @open = "open"]';
+ $details_closed_xpath = '//details[@id = "edit-scheduler-settings" and not(@open = "open")]';
+
// Check that the dates are shown in a vertical tab by default.
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//div[contains(@class, "form-type-vertical-tabs")]//details[@id = "edit-scheduler-settings"]');
+ $add_url = $this->entityAddUrl($entityTypeId, $bundle);
+ $this->drupalGet($add_url);
+ $assert->elementExists('xpath', $vertical_tab_xpath);
+ $assert->elementExists('xpath', $details_closed_xpath);
// Check that the dates are shown as a fieldset when configured to do so,
// and that fieldset is collapsed by default.
- $this->nodetype->setThirdPartySetting('scheduler', 'fields_display_mode', 'fieldset')->save();
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementNotExists('xpath', '//div[contains(@class, "form-type-vertical-tabs")]//details[@id = "edit-scheduler-settings"]');
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and not(@open = "open")]');
+ $entityType->setThirdPartySetting('scheduler', 'fields_display_mode', 'fieldset')->save();
+ $this->drupalGet($add_url);
+ $assert->elementNotExists('xpath', $vertical_tab_xpath);
+ $assert->elementExists('xpath', $details_closed_xpath);
// Check that the fieldset is expanded if either of the scheduling dates
// are required.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_required', TRUE)->save();
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $entityType->setThirdPartySetting('scheduler', 'publish_required', TRUE)->save();
+ $this->drupalGet($add_url);
+ $assert->elementExists('xpath', $details_open_xpath);
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_required', FALSE)
+ $entityType->setThirdPartySetting('scheduler', 'publish_required', FALSE)
->setThirdPartySetting('scheduler', 'unpublish_required', TRUE)->save();
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $this->drupalGet($add_url);
+ $assert->elementExists('xpath', $details_open_xpath);
// Check that the fieldset is expanded if the 'always' option is set.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_required', FALSE)
+ $entityType->setThirdPartySetting('scheduler', 'publish_required', FALSE)
->setThirdPartySetting('scheduler', 'unpublish_required', FALSE)
->setThirdPartySetting('scheduler', 'expand_fieldset', 'always')->save();
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $this->drupalGet($add_url);
+ $assert->elementExists('xpath', $details_open_xpath);
- // Check that the fieldset is expanded if the node already has a publish-on
- // date. This requires editing an existing scheduled node.
- $this->nodetype->setThirdPartySetting('scheduler', 'expand_fieldset', 'when_required')->save();
+ // Check that the fieldset is expanded if the entity already has a
+ // publish-on date. This requires editing an existing scheduled entity.
+ $entityType->setThirdPartySetting('scheduler', 'expand_fieldset', 'when_required')->save();
$options = [
'title' => 'Contains Publish-on date ' . $this->randomMachineName(10),
- 'type' => $this->type,
'publish_on' => strtotime('+1 day'),
];
- $node = $this->drupalCreateNode($options);
- $this->drupalGet('node/' . $node->id() . '/edit');
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $entity = $this->createEntity($entityTypeId, $bundle, $options);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $assert->elementExists('xpath', $details_open_xpath);
// Repeat the check with a timestamp value of zero. This is a valid date
// so the fieldset should be opened. It will not be used much on real sites
@@ -91,82 +109,92 @@ class SchedulerFieldsDisplayTest extends SchedulerBrowserTestBase {
// we get zero. Debugging Rules is easier if the fieldset opens as expected.
$options = [
'title' => 'Contains Publish-on date with timestamp value zero - ' . $this->randomMachineName(10),
- 'type' => $this->type,
'publish_on' => 0,
];
- $node = $this->drupalCreateNode($options);
- $this->drupalGet('node/' . $node->id() . '/edit');
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $entity = $this->createEntity($entityTypeId, $bundle, $options);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $assert->elementExists('xpath', $details_open_xpath);
- // Check that the fieldset is expanded if the node has an unpublish-on date.
+ // Check that the fieldset is expanded if there is an unpublish-on date.
$options = [
'title' => 'Contains Unpublish-on date ' . $this->randomMachineName(10),
- 'type' => $this->type,
'unpublish_on' => strtotime('+1 day'),
];
- $node = $this->drupalCreateNode($options);
- $this->drupalGet('node/' . $node->id() . '/edit');
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $entity = $this->createEntity($entityTypeId, $bundle, $options);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $assert->elementExists('xpath', $details_open_xpath);
// Repeat with a timestamp value of zero.
$options = [
'title' => 'Contains Unpublish-on date with timestamp value zero - ' . $this->randomMachineName(10),
- 'type' => $this->type,
'unpublish_on' => 0,
];
- $node = $this->drupalCreateNode($options);
- $this->drupalGet('node/' . $node->id() . '/edit');
- $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings" and @open = "open"]');
+ $entity = $this->createEntity($entityTypeId, $bundle, $options);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $assert->elementExists('xpath', $details_open_xpath);
// Check that the display reverts to a vertical tab again when specifically
// configured to do so.
- $this->nodetype->setThirdPartySetting('scheduler', 'fields_display_mode', 'vertical_tab')->save();
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//div[contains(@class, "form-type-vertical-tabs")]//details[@id = "edit-scheduler-settings"]');
+ $entityType->setThirdPartySetting('scheduler', 'fields_display_mode', 'vertical_tab')->save();
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $assert->elementExists('xpath', $vertical_tab_xpath);
+ $assert->elementExists('xpath', $details_open_xpath);
}
/**
* Tests the settings entry in the content type form display.
*
* This test covers scheduler_entity_extra_field_info().
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testManageFormDisplay() {
- $this->drupalLogin($this->adminUser2);
+ public function testManageFormDisplay($entityTypeId, $bundle) {
+ $this->drupalLogin($this->adminUser);
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
- // Check that the weight input field is displayed when the content type is
+ // Check that the weight input field is displayed when the entity bundle is
// enabled for scheduling. This field still exists even with tabledrag on.
- $this->drupalGet('admin/structure/types/manage/' . $this->type . '/form-display');
+ $form_display_url = Url::fromRoute("entity.entity_form_display.{$entityTypeId}.default", [$entityType->getEntityTypeId() => $bundle]);
+ $this->drupalGet($form_display_url);
$this->assertSession()->fieldExists('edit-fields-scheduler-settings-weight');
- // Check that the weight input field is not displayed when the content type
+ // Check that the weight input field is not displayed when the entity bundle
// is not enabled for scheduling.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_enable', FALSE)
+ $this->entityTypeObject($entityTypeId, $bundle)
+ ->setThirdPartySetting('scheduler', 'publish_enable', FALSE)
->setThirdPartySetting('scheduler', 'unpublish_enable', FALSE)->save();
- $this->drupalGet('admin/structure/types/manage/' . $this->type . '/form-display');
+ $this->drupalGet($form_display_url);
+ $this->assertSession()->pageTextContains('Manage form display');
$this->assertSession()->FieldNotExists('edit-fields-scheduler-settings-weight');
}
/**
* Tests the edit form when scheduler fields have been disabled.
*
- * This test covers scheduler_form_node_form_alter().
+ * This test covers _scheduler_entity_type_form_alter().
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testDisabledFields() {
- $this->drupalLogin($this->adminUser2);
+ public function testDisabledFields($entityTypeId, $bundle) {
+ $this->drupalLogin($this->adminUser);
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
/** @var \Drupal\Tests\WebAssert $assert */
$assert = $this->assertSession();
- // 1. Set the publish_on field to 'hidden' in the node edit form.
+ // 1. Set the publish_on field to 'hidden' in the entity edit form.
$edit = [
'fields[publish_on][region]' => 'hidden',
];
- $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/form-display', $edit, 'Save');
+ $form_display_url = Url::fromRoute("entity.entity_form_display.{$entityTypeId}.default", [$entityType->getEntityTypeId() => $bundle]);
+ $this->drupalGet($form_display_url);
+ $this->submitForm($edit, 'Save');
- // Check that a scheduler vertical tab is displayed.
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//div[contains(@class, "form-type-vertical-tabs")]//details[@id = "edit-scheduler-settings"]');
- // Check the publish_on field is not shown, but the unpublish_on field is.
+ // Check that the scheduler details element is shown and that the
+ // unpublish_on field is shown, but the publish_on field is not shown.
+ $add_url = $this->entityAddUrl($entityTypeId, $bundle);
+ $this->drupalGet($add_url);
+ $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings"]');
$this->assertSession()->FieldNotExists('publish_on[0][value][date]');
$this->assertSession()->FieldExists('unpublish_on[0][value][date]');
@@ -175,12 +203,13 @@ class SchedulerFieldsDisplayTest extends SchedulerBrowserTestBase {
'fields[publish_on][region]' => 'content',
'fields[unpublish_on][region]' => 'hidden',
];
- $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/form-display', $edit, 'Save');
+ $this->drupalGet($form_display_url);
+ $this->submitForm($edit, 'Save');
- // Check that a scheduler vertical tab is displayed.
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementExists('xpath', '//div[contains(@class, "form-type-vertical-tabs")]//details[@id = "edit-scheduler-settings"]');
- // Check the publish_on field is not shown, but the unpublish_on field is.
+ // Check that the scheduler details element is shown and that the
+ // publish_on field is shown, but the unpublish_on field is not shown.
+ $this->drupalGet($add_url);
+ $assert->elementExists('xpath', '//details[@id = "edit-scheduler-settings"]');
$this->assertSession()->FieldExists('publish_on[0][value][date]');
$this->assertSession()->FieldNotExists('unpublish_on[0][value][date]');
@@ -189,25 +218,30 @@ class SchedulerFieldsDisplayTest extends SchedulerBrowserTestBase {
'fields[publish_on][region]' => 'hidden',
'fields[unpublish_on][region]' => 'hidden',
];
- $this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/form-display', $edit, 'Save');
+ $this->drupalGet($form_display_url);
+ $this->submitForm($edit, 'Save');
- // Check that no vertical tab is displayed.
- $this->drupalGet('node/add/' . $this->type);
- $assert->elementNotExists('xpath', '//div[contains(@class, "form-type-vertical-tabs")]//details[@id = "edit-scheduler-settings"]');
- // Check the neither field is displayed.
+ // Check that the scheduler details element is not shown when both of the
+ // date fields are set to be hidden.
+ $this->drupalGet($add_url);
+ $assert->elementNotExists('xpath', '//details[@id = "edit-scheduler-settings"]');
$this->assertSession()->FieldNotExists('publish_on[0][value][date]');
$this->assertSession()->FieldNotExists('unpublish_on[0][value][date]');
}
/**
* Test the option to hide the seconds on the time input fields.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testHideSeconds() {
+ public function testHideSeconds($entityTypeId, $bundle) {
$this->drupalLogin($this->schedulerUser);
$config = $this->config('scheduler.settings');
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
// Check that the default is to show the seconds on the input fields.
- $this->drupalGet('node/add/' . $this->type);
+ $add_url = $this->entityAddUrl($entityTypeId, $bundle);
+ $this->drupalGet($add_url);
$publish_time_field = $this->xpath('//input[@id="edit-publish-on-0-value-time"]');
$unpublish_time_field = $this->xpath('//input[@id="edit-unpublish-on-0-value-time"]');
$this->assertEquals(1, $publish_time_field[0]->getAttribute('step'), 'The input time step for publish-on is 1, so the seconds will be visible and usable.');
@@ -218,7 +252,7 @@ class SchedulerFieldsDisplayTest extends SchedulerBrowserTestBase {
$config->set('hide_seconds', TRUE)->save();
// Get the node-add page and check the input fields.
- $this->drupalGet('node/add/' . $this->type);
+ $this->drupalGet($add_url);
$publish_time_field = $this->xpath('//input[@id="edit-publish-on-0-value-time"]');
$unpublish_time_field = $this->xpath('//input[@id="edit-unpublish-on-0-value-time"]');
$this->assertEquals(60, $publish_time_field[0]->getAttribute('step'), 'The input time step for publish-on is 60, so the seconds will be hidden and not usable.');
@@ -227,18 +261,17 @@ class SchedulerFieldsDisplayTest extends SchedulerBrowserTestBase {
// Save with both dates entered, including seconds in the times.
$edit = [
- 'title[0][value]' => 'Hide the seconds',
- 'body[0][value]' => $this->randomString(30),
+ "{$titleField}[0][value]" => 'Hide the seconds',
'publish_on[0][value][date]' => date('Y-m-d', strtotime('+1 day', $this->requestTime)),
'publish_on[0][value][time]' => '01:02:03',
'unpublish_on[0][value][date]' => date('Y-m-d', strtotime('+1 day', $this->requestTime)),
'unpublish_on[0][value][time]' => '04:05:06',
];
$this->submitForm($edit, 'Save');
- $node = $this->drupalGetNodeByTitle('Hide the seconds');
+ $entity = $this->getEntityByTitle($entityTypeId, 'Hide the seconds');
// Edit and check that the seconds have been set to zero.
- $this->drupalGet("node/{$node->id()}/edit");
+ $this->drupalGet($entity->toUrl('edit-form'));
$this->assertSession()->FieldValueEquals('publish_on[0][value][time]', '01:02:00');
$this->assertSession()->FieldValueEquals('unpublish_on[0][value][time]', '04:05:00');
diff --git a/tests/src/Functional/SchedulerHooksTest.php b/tests/src/Functional/SchedulerHooksTest.php
new file mode 100644
index 0000000..4bc1aaf
--- /dev/null
+++ b/tests/src/Functional/SchedulerHooksTest.php
@@ -0,0 +1,539 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+use Drupal\commerce_product\Entity\ProductType;
+use Drupal\node\Entity\NodeType;
+use Drupal\media\Entity\MediaType;
+
+/**
+ * Tests the API hook functions of the Scheduler module.
+ *
+ * This class covers the eight hook functions that Scheduler provides, allowing
+ * other modules to interact with editting, scheduling and processing via cron.
+ *
+ * @group scheduler
+ */
+class SchedulerHooksTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Additional modules required.
+ *
+ * @var array
+ *
+ * @todo 'menu_ui' is in the exported node.type definition, and 'path' is in
+ * the entity_form_display. Could these be removed from the config files and
+ * then not needed here?
+ */
+ protected static $modules = ['scheduler_api_test', 'menu_ui', 'path'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Load the custom node type. These entity types are enabled for Scheduler
+ // automatically as that is pre-configured in the {type}.yml files.
+ $customNodeName = 'scheduler_api_node_test';
+ $customNodetype = NodeType::load($customNodeName);
+
+ // Check that the custom node type has loaded OK.
+ $this->assertNotNull($customNodetype, "Custom node type $customNodeName failed to load during setUp");
+
+ // Load the custom media type.
+ $customMediaName = 'scheduler_api_media_test';
+ $customMediatype = MediaType::load($customMediaName);
+
+ // Check that the custom media type has loaded OK.
+ $this->assertNotNull($customMediatype, "Custom media type $customMediaName failed to load during setUp");
+
+ // Load the custom product type.
+ $customProductName = 'scheduler_api_product_test';
+ $customProductType = ProductType::load($customProductName);
+
+ // Check that the custom product type has loaded OK.
+ $this->assertNotNull($customProductType, "Custom product type $customProductName failed to load during setUp");
+
+ // Create a web user that has permission to create and edit and schedule
+ // the custom entity types.
+ $this->webUser = $this->drupalCreateUser([
+ "create $customNodeName content",
+ "edit any $customNodeName content",
+ 'schedule publishing of nodes',
+ "create $customMediaName media",
+ "edit any $customMediaName media",
+ 'schedule publishing of media',
+ "create $customProductName commerce_product",
+ "update any $customProductName commerce_product",
+ 'schedule publishing of commerce_product',
+ // 'administer commerce_store' is needed to see and use any store, i.e
+ // cannot add a product without this. Is it a bug?
+ 'administer commerce_store',
+ ]);
+ $this->webUser->set('name', 'Wenlock the Web user')->save();
+ }
+
+ /**
+ * Provides test data containing the custom entity types.
+ *
+ * @return array
+ * Each array item has the values: [entity type id, bundle id].
+ */
+ public function dataCustomEntityTypes() {
+ $data = [
+ '#node' => ['node', 'scheduler_api_node_test'],
+ '#media' => ['media', 'scheduler_api_media_test'],
+ '#commerce_product' => ['commerce_product', 'scheduler_api_product_test'],
+ ];
+ return $data;
+ }
+
+ /**
+ * Covers hook_scheduler_list() and hook_scheduler_{type}_list()
+ *
+ * These hooks allow other modules to add more entity ids into the list being
+ * processed. In real scenarios, the third-party module would likely have more
+ * complex data structures and/or tables from which to identify the ids to
+ * add. In this test, to keep it simple, we identify entities simply by title.
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testList($entityTypeId, $bundle) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $this->drupalLogin($this->schedulerUser);
+
+ // Create test entities using the standard scheduler test entity types.
+ // Entity 1 is not published and has no publishing date set. The test API
+ // module will add this entity into the list to be published using an
+ // implementation of general hook_scheduler_list() function. Entity 2 is
+ // similar but will be added via the hook_scheduler_{type}_list() function.
+ $entity1 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Pink $entityTypeId list publish me",
+ ]);
+ $entity2 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Purple $entityTypeId list publish me",
+ ]);
+
+ // Entity 3 is published and has no unpublishing date set. The test API
+ // module will add this entity into the list to be unpublished.
+ $entity3 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Pink $entityTypeId list unpublish me",
+ ]);
+ $entity4 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Purple $entityTypeId list unpublish me",
+ ]);
+
+ // Before cron, check that entity 1 and 2 are unpublished and entity 3 and 4
+ // are published.
+ $this->assertFalse($entity1->isPublished(), "Before cron, $entityTypeId 1 '{$entity1->label()}' should be unpublished.");
+ $this->assertFalse($entity2->isPublished(), "Before cron, $entityTypeId 2 '{$entity2->label()}' should be unpublished.");
+ $this->assertTrue($entity3->isPublished(), "Before cron, $entityTypeId 3 '{$entity3->label()}' should be published.");
+ $this->assertTrue($entity4->isPublished(), "Before cron, $entityTypeId 4 '{$entity4->label()}' should be published.");
+
+ // Run cron and refresh the entities.
+ scheduler_cron();
+ $storage->resetCache();
+ for ($i = 1; $i <= 4; $i++) {
+ ${"entity$i"} = $storage->load(${"entity$i"}->id());
+ }
+
+ // Check tha entity 1 and 2 have been published.
+ $this->assertTrue($entity1->isPublished(), "After cron, $entityTypeId 1 '{$entity1->label()}' should be published.");
+ $this->assertTrue($entity2->isPublished(), "After cron, $entityTypeId 2 '{$entity2->label()}' should be published.");
+
+ // Check that entity 3 and 4 have been unpublished.
+ $this->assertFalse($entity3->isPublished(), "After cron, $entityTypeId 3 '{$entity3->label()}' should be unpublished.");
+ $this->assertFalse($entity4->isPublished(), "After cron, $entityTypeId 4 '{$entity4->label()}' should be unpublished.");
+ }
+
+ /**
+ * Covers hook_scheduler_list_alter() and hook_scheduler_{type}_list_alter()
+ *
+ * These hook allows other modules to add or remove entity ids from the list
+ * to be processed.
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testListAlter($entityTypeId, $bundle) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $this->drupalLogin($this->schedulerUser);
+
+ // Create test entities using the standard scheduler test entity types.
+ // Entity 1 is set for scheduled publishing, but will be removed by the test
+ // API generic hook_scheduler_list_alter() function. Entity 2 is similar but
+ // is removed via the specifc hook_scheduler_{type}_list_alter() function.
+ $entity1 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Pink $entityTypeId list_alter do not publish me",
+ 'publish_on' => strtotime('-1 day'),
+ ]);
+ $entity2 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Purple $entityTypeId list_alter do not publish me",
+ 'publish_on' => strtotime('-1 day'),
+ ]);
+
+ // Entity 3 is not published and has no publishing date set. The test module
+ // generic hook_scheduler_list_alter() function will add a date and add the
+ // id into the list to be published. Entity 4 is similar but the date and id
+ // is added by the specifc hook_scheduler_{type}_list_alter() function.
+ $entity3 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Pink $entityTypeId list_alter publish me",
+ ]);
+ $entity4 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Purple $entityTypeId list_alter publish me",
+ ]);
+
+ // Entity 5 is set for scheduled unpublishing, but will be removed by the
+ // generic hook_scheduler_list_alter() function. Entity 6 is similar but is
+ // removed by the specifc hook_scheduler_{type}_list_alter() function.
+ $entity5 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Pink $entityTypeId list_alter do not unpublish me",
+ 'unpublish_on' => strtotime('-1 day'),
+ ]);
+ $entity6 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Purple $entityTypeId list_alter do not unpublish me",
+ 'unpublish_on' => strtotime('-1 day'),
+ ]);
+
+ // Entity 7 is published and has no unpublishing date set. The generic
+ // hook_scheduler_list_alter() will add a date and add the id into the list
+ // to be unpublished. Entity 8 is similar but the date and id will be added
+ // by the specifc hook_scheduler_{type}_list_alter() function.
+ $entity7 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Pink $entityTypeId list_alter unpublish me",
+ ]);
+ $entity8 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Purple $entityTypeId list_alter unpublish me",
+ ]);
+
+ // Before cron, check entities 1-4 are unpublished and 5-8 are published.
+ $this->assertFalse($entity1->isPublished(), "Before cron, $entityTypeId 1 '{$entity1->label()}' should be unpublished.");
+ $this->assertFalse($entity2->isPublished(), "Before cron, $entityTypeId 2 '{$entity2->label()}' should be unpublished.");
+ $this->assertFalse($entity3->isPublished(), "Before cron, $entityTypeId 3 '{$entity3->label()}' should be unpublished.");
+ $this->assertFalse($entity4->isPublished(), "Before cron, $entityTypeId 4 '{$entity4->label()}' should be unpublished.");
+ $this->assertTrue($entity5->isPublished(), "Before cron, $entityTypeId 5 '{$entity5->label()}' should be published.");
+ $this->assertTrue($entity6->isPublished(), "Before cron, $entityTypeId 6 '{$entity6->label()}' should be published.");
+ $this->assertTrue($entity7->isPublished(), "Before cron, $entityTypeId 7 '{$entity7->label()}' should be published.");
+ $this->assertTrue($entity8->isPublished(), "Before cron, $entityTypeId 8 '{$entity8->label()}' should be published.");
+
+ // Run cron and refresh the entities from storage.
+ scheduler_cron();
+ $storage->resetCache();
+ for ($i = 1; $i <= 8; $i++) {
+ ${"entity$i"} = $storage->load(${"entity$i"}->id());
+ }
+
+ // After cron, check that entities 1-2 remain unpublished, 3-4 have now
+ // been published, 5-6 remain published and 7-8 have been unpublished.
+ $this->assertFalse($entity1->isPublished(), "After cron, $entityTypeId 1 '{$entity1->label()}' should be unpublished.");
+ $this->assertFalse($entity2->isPublished(), "After cron, $entityTypeId 2 '{$entity2->label()}' should be unpublished.");
+ $this->assertTrue($entity3->isPublished(), "After cron, $entityTypeId 3 '{$entity3->label()}' should be published.");
+ $this->assertTrue($entity4->isPublished(), "After cron, $entityTypeId 4 '{$entity4->label()}' should be published.");
+ $this->assertTrue($entity5->isPublished(), "After cron, $entityTypeId 5 '{$entity5->label()}' should be published.");
+ $this->assertTrue($entity6->isPublished(), "After cron, $entityTypeId 6 '{$entity6->label()}' should be published.");
+ $this->assertFalse($entity7->isPublished(), "After cron, $entityTypeId 7 '{$entity7->label()}' should be unpublished.");
+ $this->assertFalse($entity8->isPublished(), "After cron, $entityTypeId 8 '{$entity8->label()}' should be unpublished.");
+ }
+
+ /**
+ * Covers hook_scheduler_{type}_publishing_allowed()
+ *
+ * This hook is used to deny the publishing of individual entities. The test
+ * uses the customised content type which has checkboxes 'Approved for
+ * publishing' and 'Approved for unpublishing'.
+ *
+ * @dataProvider dataCustomEntityTypes()
+ */
+ public function testPublishingAllowed($entityTypeId, $bundle) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $this->drupalLogin($this->webUser);
+
+ // Check the 'approved for publishing' field is shown on the entity form.
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->assertSession()->fieldExists('edit-field-approved-publishing-value');
+
+ // Check that the message is shown when scheduling an entity for publishing
+ // which is not yet allowed to be published.
+ $edit = [
+ "{$titleField}[0][value]" => "Blue $entityTypeId - Set publish-on date without approval",
+ 'publish_on[0][value][date]' => date('Y-m-d', time() + 3),
+ 'publish_on[0][value][time]' => date('H:i:s', time() + 3),
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextMatches('/is scheduled for publishing.* but will not be published until approved/');
+
+ // Create an entity that is scheduled but not approved for publishing. Then
+ // run cron for scheduler, and check that the entity is still not published.
+ $entity = $this->createUnapprovedEntity($entityTypeId, $bundle, 'publish_on');
+ scheduler_cron();
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertFalse($entity->isPublished(), "Unapproved '{$entity->label()}' should not be published during cron processing.");
+
+ // Create an entity and approve it for publishing, run cron for scheduler
+ // and check that the entity is published. This is a stronger test than
+ // simply approving the previously used entity above, as we do not know what
+ // publish state that may be in after the cron run above.
+ $entity = $this->createUnapprovedEntity($entityTypeId, $bundle, 'publish_on');
+ $this->approveEntity($entityTypeId, $entity->id(), 'field_approved_publishing');
+ $this->assertFalse($entity->isPublished(), "New approved '{$entity->label()}' should not be initially published.");
+ scheduler_cron();
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), "Approved '{$entity->label()}' should be published during cron processing.");
+
+ // Turn on immediate publishing when the date is in the past and repeat
+ // the tests. It is not needed to run cron jobs here.
+ $bundle_field_name = $entity->getEntityType()->get('entity_keys')['bundle'];
+ $entity->$bundle_field_name->entity->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
+
+ // Check that an entity can be approved and published programatically.
+ $entity = $this->createUnapprovedEntity($entityTypeId, $bundle, 'publish_on');
+ $this->assertFalse($entity->isPublished(), "New unapproved '{$entity->label()}' with a date in the past should not be published immediately after saving.");
+ $this->approveEntity($entityTypeId, $entity->id(), 'field_approved_publishing');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), "New approved '{$entity->label()}' with a date in the past should be published immediately when created programatically.");
+
+ // Check that an entity can be approved and published via edit form.
+ $entity = $this->createUnapprovedEntity($entityTypeId, $bundle, 'publish_on');
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(['field_approved_publishing[value]' => '1'], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), "Approved '{$entity->label()}' with a date in the past is published immediately after saving via edit form.");
+ }
+
+ /**
+ * Covers hook_scheduler_{type}_unpublishing_allowed()
+ *
+ * This hook is used to deny the unpublishing of individual entities. This
+ * test is simpler than the test sequence for allowed publishing, because the
+ * past date 'publish' option is not applicable.
+ *
+ * @dataProvider dataCustomEntityTypes()
+ */
+ public function testUnpublishingAllowed($entityTypeId, $bundle) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $this->drupalLogin($this->webUser);
+
+ // Check the 'approved for unpublishing' field is shown on the entity form.
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->assertSession()->fieldExists('edit-field-approved-unpublishing-value');
+
+ // Check that the message is shown when scheduling an entity for
+ // unpublishing which is not yet allowed to be unpublished.
+ $edit = [
+ "{$titleField}[0][value]" => "Red $entityTypeId - Set unpublish-on date without approval",
+ 'unpublish_on[0][value][date]' => date('Y-m-d', time() + 3),
+ 'unpublish_on[0][value][time]' => date('H:i:s', time() + 3),
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextMatches('/is scheduled for unpublishing.* but will not be unpublished until approved/');
+
+ // Create an entity that is scheduled but not approved for unpublishing, run
+ // cron for scheduler, and check that the entity is still published.
+ $entity = $this->createUnapprovedEntity($entityTypeId, $bundle, 'unpublish_on');
+ scheduler_cron();
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), "Unapproved '{$entity->label()}' should not be unpublished during cron processing.");
+
+ // Create an entity and approve it for unpublishing, run cron for scheduler
+ // and check that the entity is unpublished.
+ $entity = $this->createUnapprovedEntity($entityTypeId, $bundle, 'unpublish_on');
+ $this->approveEntity($entityTypeId, $entity->id(), 'field_approved_unpublishing');
+ $this->assertTrue($entity->isPublished(), "New approved '{$entity->label()}' should initially remain published.");
+ scheduler_cron();
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertFalse($entity->isPublished(), "Approved '{$entity->label()}' should be unpublished during cron processing.");
+ }
+
+ /**
+ * Creates a new entity that is not approved.
+ *
+ * The entity will have a publish/unpublish date in the past to make sure it
+ * will be included in the next cron run.
+ *
+ * @param string $entityTypeId
+ * The entity type to create, 'node' or 'media'.
+ * @param string $bundle
+ * The bundle to create, 'scheduler_api_test' or 'scheduler_api_media_test'.
+ * @param string $date_field
+ * The Scheduler date field to set, either 'publish_on' or 'unpublish_on'.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * The created entity object.
+ */
+ protected function createUnapprovedEntity($entityTypeId, $bundle, $date_field) {
+ $settings = [
+ 'title' => (($date_field == 'publish_on') ? 'Blue' : 'Red') . " $entityTypeId {$this->randomMachineName(10)}",
+ 'status' => ($date_field == 'unpublish_on'),
+ $date_field => strtotime('-1 day'),
+ 'field_approved_publishing' => 0,
+ 'field_approved_unpublishing' => 0,
+ ];
+ return $this->createEntity($entityTypeId, $bundle, $settings);
+ }
+
+ /**
+ * Approves an entity for publication or unpublication.
+ *
+ * @param string $entityTypeId
+ * The entity type to approve, 'node' or 'media'.
+ * @param int $id
+ * The id of the entity to approve.
+ * @param string $field_name
+ * The name of the field to set, either 'field_approved_publishing' or
+ * 'field_approved_unpublishing'.
+ */
+ protected function approveEntity($entityTypeId, $id, $field_name) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $storage->resetCache([$id]);
+ $entity = $storage->load($id);
+ $entity->set($field_name, TRUE);
+ $label_field = $entity->getEntityType()->get('entity_keys')['label'];
+ $entity->set($label_field, $entity->label() . " - approved for publishing: {$entity->field_approved_publishing->value}, for unpublishing: {$entity->field_approved_unpublishing->value}")->save();
+ }
+
+ /**
+ * Tests the hooks which allow hiding of scheduler input fields.
+ *
+ * This test covers:
+ * hook_scheduler_hide_publish_date()
+ * hook_scheduler_hide_unpublish_date()
+ * hook_scheduler_{type}_hide_publish_date()
+ * hook_scheduler_{type}_hide_unpublish_date()
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testHideDateField($entityTypeId, $bundle) {
+ $this->drupalLogin($this->schedulerUser);
+
+ // Create test entities.
+ $entity1 = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "Red $entityTypeId will have neither field hidden",
+ ]);
+ $entity2 = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "Orange $entityTypeId will have the publish-on field hidden",
+ ]);
+ $entity3 = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "Yellow $entityTypeId will have the unpublish-on field hidden",
+ ]);
+ $entity4 = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "Green $entityTypeId will have both Scheduler fields hidden",
+ ]);
+
+ // Set the scheduler fieldset to always expand, for ease during development.
+ $bundle_field_name = $entity1->getEntityType()->get('entity_keys')['bundle'];
+ $entity1->$bundle_field_name->entity->setThirdPartySetting('scheduler', 'expand_fieldset', 'always')->save();
+
+ /** @var \Drupal\Tests\WebAssert $assert */
+ $assert = $this->assertSession();
+
+ // Entity 1 'Red' should have both fields displayed.
+ $this->drupalGet($entity1->toUrl('edit-form'));
+ $assert->ElementExists('xpath', '//input[@id = "edit-publish-on-0-value-date"]');
+ $assert->ElementExists('xpath', '//input[@id = "edit-unpublish-on-0-value-date"]');
+
+ // Entity 2 'Orange' should have only the publish-on field hidden.
+ $this->drupalGet($entity2->toUrl('edit-form'));
+ $assert->ElementNotExists('xpath', '//input[@id = "edit-publish-on-0-value-date"]');
+ $assert->ElementExists('xpath', '//input[@id = "edit-unpublish-on-0-value-date"]');
+
+ // Entity 3 'Yellow' should have only the unpublish-on field hidden.
+ $this->drupalGet($entity3->toUrl('edit-form'));
+ $assert->ElementExists('xpath', '//input[@id = "edit-publish-on-0-value-date"]');
+ $assert->ElementNotExists('xpath', '//input[@id = "edit-unpublish-on-0-value-date"]');
+
+ // Entity 4 'Green' should have both publish-on and unpublish-on hidden.
+ $this->drupalGet($entity4->toUrl('edit-form'));
+ $assert->ElementNotExists('xpath', '//input[@id = "edit-publish-on-0-value-date"]');
+ $assert->ElementNotExists('xpath', '//input[@id = "edit-unpublish-on-0-value-date"]');
+ }
+
+ /**
+ * Tests when other modules execute the 'publish' and 'unpublish' processes.
+ *
+ * This test covers:
+ * hook_scheduler_publish_process()
+ * hook_scheduler_unpublish_process()
+ * hook_scheduler_{type}_publish_process()
+ * hook_scheduler_{type}_unpublish_process()
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testPublishUnpublishProcess($entityTypeId, $bundle) {
+ // $this->drupalLogin($this->schedulerUser);
+ $storage = $this->entityStorageObject($entityTypeId);
+
+ // Create test entities.
+ $entity1 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Red $entityTypeId will cause a failure on publishing",
+ 'publish_on' => strtotime('-1 day'),
+ ]);
+ $entity2 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Orange $entityTypeId will be unpublished by the API test module not Scheduler",
+ 'unpublish_on' => strtotime('-1 day'),
+ ]);
+ $entity3 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => FALSE,
+ 'title' => "Yellow $entityTypeId will be published by the API test module not Scheduler",
+ 'publish_on' => strtotime('-1 day'),
+ ]);
+ // 'Green' will have both fields hidden so is harder to test manually.
+ // Therefore introduce a different colour - Blue.
+ $entity4 = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'title' => "Blue $entityTypeId will cause a failure on unpublishing",
+ 'unpublish_on' => strtotime('-1 day'),
+ ]);
+
+ // Simulate a cron run.
+ scheduler_cron();
+
+ // Check red.
+ $storage->resetCache([$entity1->id()]);
+ $entity1 = $storage->load($entity1->id());
+ $this->assertFalse($entity1->isPublished(), 'Red should remain unpublished.');
+ $this->assertNotEmpty($entity1->publish_on->value, 'Red should still have a publish-on date.');
+
+ // Check orange.
+ $storage->resetCache([$entity2->id()]);
+ $entity2 = $storage->load($entity2->id());
+ $this->assertFalse($entity2->isPublished(), 'Orange should be unpublished.');
+ $this->assertStringContainsString('unpublishing processed by API test module', $entity2->label(), 'Orange should be processed by the API test module.');
+ $this->assertEmpty($entity2->unpublish_on->value, 'Orange should not have an unpublish-on date.');
+
+ // Check yellow.
+ $storage->resetCache([$entity3->id()]);
+ $entity3 = $storage->load($entity3->id());
+ $this->assertTrue($entity3->isPublished(), 'Yellow should be published.');
+ $this->assertStringContainsString('publishing processed by API test module', $entity3->label(), 'Yellow should be processed by the API test module.');
+ $this->assertEmpty($entity3->publish_on->value, 'Yellow should not have a publish-on date.');
+
+ // Check blue.
+ $storage->resetCache([$entity4->id()]);
+ $entity4 = $storage->load($entity4->id());
+ $this->assertTrue($entity4->isPublished(), 'Blue should remain published.');
+ $this->assertNotEmpty($entity4->unpublish_on->value, 'Blue should still have an unpublish-on date.');
+ }
+
+}
diff --git a/tests/src/Functional/SchedulerLightweightCronTest.php b/tests/src/Functional/SchedulerLightweightCronTest.php
index a182fd4..c79c6bc 100644
--- a/tests/src/Functional/SchedulerLightweightCronTest.php
+++ b/tests/src/Functional/SchedulerLightweightCronTest.php
@@ -59,7 +59,8 @@ class SchedulerLightweightCronTest extends SchedulerBrowserTestBase {
$this->assertEquals(20, strlen($key), 'The default lightweight cron key string length should be 20');
// Check that a new random key can be generated.
- $this->drupalPostForm($this->routeCronForm, [], 'Generate new random key');
+ $this->drupalGet($this->routeCronForm);
+ $this->submitForm([], 'Generate new random key');
$new_key_xpath = $this->xpath('//input[@id="edit-lightweight-access-key"]/@value');
$new_key = $new_key_xpath[0]->getText();
$this->assertNotEmpty($new_key, 'The lightweight cron key field should not be empty after generating a new key');
@@ -67,11 +68,13 @@ class SchedulerLightweightCronTest extends SchedulerBrowserTestBase {
$this->assertNotEquals($new_key, $key, 'The new lightweight cron key should be different from the previous key.');
// Check that the 'run lightweight cron' button works.
- $this->drupalPostForm($this->routeCronForm, [], "Run Scheduler's lightweight cron now");
+ $this->drupalGet($this->routeCronForm);
+ $this->submitForm([], "Run Scheduler's lightweight cron now");
$this->assertSession()->pageTextContains('Lightweight cron run completed.');
// Check that the form cannot be saved if the cron key is blank.
- $this->drupalPostForm($this->routeCronForm, ['lightweight_access_key' => ''], 'Save configuration');
+ $this->drupalGet($this->routeCronForm);
+ $this->submitForm(['lightweight_access_key' => ''], 'Save configuration');
$this->assertSession()->pageTextContains('Lightweight cron access key field is required.');
$this->assertSession()->pageTextNotContains('The configuration options have been saved.');
}
diff --git a/tests/src/Functional/SchedulerMessageTest.php b/tests/src/Functional/SchedulerMessageTest.php
index 8ca8415..61c2a57 100644
--- a/tests/src/Functional/SchedulerMessageTest.php
+++ b/tests/src/Functional/SchedulerMessageTest.php
@@ -3,18 +3,21 @@
namespace Drupal\Tests\scheduler\Functional;
/**
- * Tests the option to display or not display the confirmations message.
+ * Tests the 'show confirmation message' entity type setting.
*
* @group scheduler
*/
class SchedulerMessageTest extends SchedulerBrowserTestBase {
/**
- * Test the .
+ * Tests the option to display or not display the confirmation message.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testConfirmationMessage() {
+ public function testConfirmationMessage($entityTypeId, $bundle) {
// Log in.
$this->drupalLogin($this->schedulerUser);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
$publish_on = strtotime('+ 1 day 5 hours');
$unpublish_on = strtotime('+ 2 day 7 hours');
@@ -27,52 +30,62 @@ class SchedulerMessageTest extends SchedulerBrowserTestBase {
// Create the content and check that the messages are shown by default.
// First just a publish_on date.
$edit = [
- 'title[0][value]' => $title1,
+ "{$titleField}[0][value]" => $title1,
'publish_on[0][value][date]' => date('Y-m-d', $publish_on),
'publish_on[0][value][time]' => date('H:i:s', $publish_on),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node1 = $this->drupalGetNodeByTitle($title1);
+ $add_url = $this->entityAddUrl($entityTypeId, $bundle);
+ $this->drupalGet($add_url);
+ $this->submitForm($edit, 'Save');
+ $entity1 = $this->getEntityByTitle($entityTypeId, $title1);
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s', $title1, $publish_on_formatted));
// Second, just an unpublish_on date.
$edit = [
- 'title[0][value]' => $title2,
+ "{$titleField}[0][value]" => $title2,
'unpublish_on[0][value][date]' => date('Y-m-d', $unpublish_on),
'unpublish_on[0][value][time]' => date('H:i:s', $unpublish_on),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node2 = $this->drupalGetNodeByTitle($title2);
+ $this->drupalGet($add_url);
+ $this->submitForm($edit, 'Save');
+ $entity2 = $this->getEntityByTitle($entityTypeId, $title2);
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be unpublished %s', $title2, $unpublish_on_formatted));
- // Third, a node with both dates.
+ // Third, with both dates.
$edit = [
- 'title[0][value]' => $title3,
+ "{$titleField}[0][value]" => $title3,
'publish_on[0][value][date]' => date('Y-m-d', $publish_on),
'publish_on[0][value][time]' => date('H:i:s', $publish_on),
'unpublish_on[0][value][date]' => date('Y-m-d', $unpublish_on),
'unpublish_on[0][value][time]' => date('H:i:s', $unpublish_on),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node3 = $this->drupalGetNodeByTitle($title3);
+ $this->drupalGet($add_url);
+ $this->submitForm($edit, 'Save');
+ $entity3 = $this->getEntityByTitle($entityTypeId, $title3);
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s and unpublished %s', $title3, $publish_on_formatted, $unpublish_on_formatted));
// Change the option to not display the messages.
- $this->nodetype->setThirdPartySetting('scheduler', 'show_message_after_update', FALSE)->save();
- $this->drupalPostForm('node/' . $node1->id() . '/edit', [], 'Save');
+ $this->entityTypeObject($entityTypeId, $bundle)->setThirdPartySetting('scheduler', 'show_message_after_update', FALSE)->save();
+ $this->drupalGet($entity1->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$this->assertSession()->pageTextNotContains('is scheduled to be published');
- $this->drupalPostForm('node/' . $node2->id() . '/edit', [], 'Save');
+ $this->drupalGet($entity2->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$this->assertSession()->pageTextNotContains('is scheduled to be unpublished');
- $this->drupalPostForm('node/' . $node3->id() . '/edit', [], 'Save');
+ $this->drupalGet($entity3->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$this->assertSession()->pageTextNotContains('is scheduled to be published');
// Set back to display the messages, and check after edit.
- $this->nodetype->setThirdPartySetting('scheduler', 'show_message_after_update', TRUE)->save();
- $this->drupalPostForm('node/' . $node1->id() . '/edit', [], 'Save');
+ $this->entityTypeObject($entityTypeId, $bundle)->setThirdPartySetting('scheduler', 'show_message_after_update', TRUE)->save();
+ $this->drupalGet($entity1->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s', $title1, $publish_on_formatted));
- $this->drupalPostForm('node/' . $node2->id() . '/edit', [], 'Save');
+ $this->drupalGet($entity2->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be unpublished %s', $title2, $unpublish_on_formatted));
- $this->drupalPostForm('node/' . $node3->id() . '/edit', [], 'Save');
+ $this->drupalGet($entity3->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s and unpublished %s', $title3, $publish_on_formatted, $unpublish_on_formatted));
}
diff --git a/tests/src/Functional/SchedulerMetaInformationTest.php b/tests/src/Functional/SchedulerMetaInformationTest.php
index b4b8116..86b6a0c 100644
--- a/tests/src/Functional/SchedulerMetaInformationTest.php
+++ b/tests/src/Functional/SchedulerMetaInformationTest.php
@@ -10,39 +10,47 @@ namespace Drupal\Tests\scheduler\Functional;
class SchedulerMetaInformationTest extends SchedulerBrowserTestBase {
/**
- * Tests meta-information on scheduled nodes.
+ * Tests meta-information on scheduled entities.
*
- * When nodes are scheduled for unpublication, an X-Robots-Tag HTTP header is
- * sent, alerting crawlers about when an item expires and should be removed
- * from search results.
+ * When an entity is scheduled for unpublication, an X-Robots-Tag HTTP header
+ * is included, telling crawlers about when an item will expire and should be
+ * removed from search results.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testMetaInformation() {
+ public function testMetaInformation($entityTypeId, $bundle) {
// Log in.
$this->drupalLogin($this->schedulerUser);
- // Create a published node without scheduling.
- $published_node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => TRUE,
- ]);
- $this->drupalGet('node/' . $published_node->id());
+ // Create a published entity without scheduling dates.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => TRUE]);
// Since we did not set an unpublish date, there should be no X-Robots-Tag
// header on the response.
- $this->assertNull($this->drupalGetHeader('X-Robots-Tag'), 'X-Robots-Tag is not present when no unpublish date is set.');
+ $this->drupalGet($entity->toUrl());
+ $this->assertNull($this->getSession()->getResponseHeader('X-Robots-Tag'), 'X-Robots-Tag should not be present when no unpublish date is set.');
+ // Also check that there is no meta tag.
+ $this->assertSession()->responseNotContains('unavailable_after:');
- // Set a scheduler unpublish date on the node.
+ // Set an unpublish date on the entity.
$unpublish_date = strtotime('+1 day');
- $edit = [
- 'unpublish_on[0][value][date]' => $this->dateFormatter->format($unpublish_date, 'custom', 'Y-m-d'),
- 'unpublish_on[0][value][time]' => $this->dateFormatter->format($unpublish_date, 'custom', 'H:i:s'),
- ];
- $this->drupalPostForm('node/' . $published_node->id() . '/edit', $edit, 'Save');
+ $entity->set('unpublish_on', $unpublish_date)->save();
- // The node page should now have an X-Robots-Tag header with an
+ // The entity full page view should now have an X-Robots-Tag header with an
// unavailable_after-directive and RFC850 date- and time-value.
- $this->drupalGet('node/' . $published_node->id());
+ $this->drupalGet($entity->toUrl());
$this->assertSession()->responseHeaderEquals('X-Robots-Tag', 'unavailable_after: ' . date(DATE_RFC850, $unpublish_date));
+
+ // Check that the required meta tag is added to the html head section.
+ $this->assertSession()->responseMatches('~meta name=[\'"]robots[\'"] content=[\'"]unavailable_after: ' . date(DATE_RFC850, $unpublish_date) . '[\'"]~');
+
+ // If the entity type has a summary listing page, check that the entity is
+ // shown but the two tags are not present.
+ if ($this->drupalGet("$entityTypeId") && $this->getSession()->getStatusCode() == '200') {
+ $this->assertSession()->pageTextContains($entity->label());
+ $this->assertNull($this->getSession()->getResponseHeader('X-Robots-Tag'), 'X-Robots-Tag should not be added when entity is not in "full" view mode.');
+ $this->assertSession()->responseNotContains('unavailable_after:');
+ }
}
}
diff --git a/tests/src/Functional/SchedulerMultilingualTest.php b/tests/src/Functional/SchedulerMultilingualTest.php
index 79bd226..2a5cd40 100644
--- a/tests/src/Functional/SchedulerMultilingualTest.php
+++ b/tests/src/Functional/SchedulerMultilingualTest.php
@@ -38,7 +38,7 @@ class SchedulerMultilingualTest extends SchedulerBrowserTestBase {
protected function setUp(): void {
parent::setUp();
- // Create a user with the required translation permissions.
+ // Add four extra permissions for the adminUser -
// 'administer languages' for url admin/config/regional/content-language.
// 'administer content translation' to show the list of content fields at
// url admin/config/regional/content-language.
@@ -46,21 +46,13 @@ class SchedulerMultilingualTest extends SchedulerBrowserTestBase {
// url node/*/translations.
// 'translate any entity' for the 'add translation' link on the translations
// page, url node/*/translations/add/.
- $this->translatorUser = $this->drupalCreateUser([
+ $this->addPermissionsToUser($this->adminUser, [
'administer languages',
'administer content translation',
'create content translations',
'translate any entity',
]);
-
- // Get the additional role already assigned to the scheduler admin user
- // created in SchedulerBrowserTestBase and add this role to the translator
- // user, to avoid switching between users throughout this test.
- $admin_roles = $this->adminUser->getRoles();
- // Key 0 is 'authenticated' role. Key 1 is the first real role.
- $this->translatorUser->addRole($admin_roles[1]);
- $this->translatorUser->save();
- $this->drupalLogin($this->translatorUser);
+ $this->drupalLogin($this->adminUser);
// Allow scheduler dates in the past to be published on next cron run.
$this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
@@ -138,23 +130,23 @@ class SchedulerMultilingualTest extends SchedulerBrowserTestBase {
// The submit shows the updated values, so no need for second get.
$this->submitForm($settings, 'Save configuration');
- $early_return = FALSE;
if ($publish_on_translatable <> $status_translatable) {
// Check for validation form error on status and publish_on.
$this->assertSession()->elementExists('xpath', '//input[@id = "edit-settings-node-' . $this->type . '-fields-publish-on" and contains(@class, "error")]');
$this->assertSession()->elementExists('xpath', '//input[@id = "edit-settings-node-' . $this->type . '-fields-status" and contains(@class, "error")]');
- $early_return = TRUE;
}
if ($unpublish_on_translatable <> $status_translatable) {
// Check for validation form error on status and unpublish_on.
$this->assertSession()->elementExists('xpath', '//input[@id = "edit-settings-node-' . $this->type . '-fields-unpublish-on" and contains(@class, "error")]');
$this->assertSession()->elementExists('xpath', '//input[@id = "edit-settings-node-' . $this->type . '-fields-status" and contains(@class, "error")]');
- $early_return = TRUE;
}
- if ($early_return) {
+
+ if (empty($expected_status_values_before)) {
+ // The test data on this run was to verify the validation messages above.
// The rest of the test is meaningless so skip it and move to the next.
return;
}
+ $this->assertSession()->pageTextContains('Settings successfully updated.');
// Create a node in the 'original' language, before any translations. It is
// unpublished with no scheduled date.
@@ -257,9 +249,11 @@ class SchedulerMultilingualTest extends SchedulerBrowserTestBase {
/**
* Provides data for testPublishingTranslations().
*
- * Case 1 when the date is translatable and can differ between translations.
- * Case 2 when the date is not translatable and the behavior should be
+ * Case 1 when the dates are translatable and can differ between translations.
+ * Case 2 when the dates are not translatable and the behavior should be
* consistent over all translations.
+ * Case 3 - 8 when there are differences in the settings and the validation
+ * should prevent the form being saved.
*
* @return array
* The test data. Each array element has the format:
@@ -270,8 +264,8 @@ class SchedulerMultilingualTest extends SchedulerBrowserTestBase {
* Expected status of four translations after cron
*/
public function dataPublishingTranslations() {
- // The key text relates to which fields are translatable.
- return [
+ // The key text is just for info, and shows which fields are translatable.
+ $data = [
'all fields' => [TRUE, TRUE, TRUE,
[FALSE, TRUE, FALSE, FALSE],
[FALSE, TRUE, FALSE, TRUE],
@@ -288,6 +282,7 @@ class SchedulerMultilingualTest extends SchedulerBrowserTestBase {
'publish_on and status' => [TRUE, FALSE, TRUE, [], []],
'unpublish_on and status' => [FALSE, TRUE, TRUE, [], []],
];
+ return $data;
}
}
diff --git a/tests/src/Functional/SchedulerNodeAccessTest.php b/tests/src/Functional/SchedulerNodeAccessTest.php
deleted file mode 100644
index 0ca88be..0000000
--- a/tests/src/Functional/SchedulerNodeAccessTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-namespace Drupal\Tests\scheduler\Functional;
-
-/**
- * Tests that Scheduler cron has full access to the scheduled nodes.
- *
- * This test uses an additional test module 'scheduler_access_test' which uses
- * a custom node access definition to deny viewing of all nodes.
- *
- * @group scheduler
- */
-class SchedulerNodeAccessTest extends SchedulerBrowserTestBase {
-
- /**
- * Additional modules required.
- *
- * @var array
- */
- protected static $modules = ['scheduler_access_test'];
-
- /**
- * Tests Scheduler cron functionality when access to the nodes is denied.
- */
- public function testNodeAccess() {
-
- // scheduler_access_test_install() sets node_access_needs_rebuild(TRUE) and
- // this works when testing the module interactively, but during simpletest
- // the node access table is not rebuilt. Hence do that here explicitly here.
- node_access_rebuild();
-
- // Create data to test publishing then unpublishing via loop.
- // @todo Convert this test to use a @dataProvider function instead of this
- // array and the loop.
- $test_data = [
- 'publish_on' => [
- 'status' => FALSE,
- 'before' => 'unpublished',
- 'after' => 'published',
- ],
- 'unpublish_on' => [
- 'status' => TRUE,
- 'before' => 'published',
- 'after' => 'unpublished',
- ],
- ];
-
- foreach ($test_data as $field => $data) {
- // Create a node with the necessary scheduler date.
- $settings = [
- 'type' => $this->type,
- 'status' => $data['status'],
- 'title' => 'Test node to be ' . $data['after'],
- $field => $this->requestTime + 1,
- ];
- $node = $this->drupalCreateNode($settings);
- $this->drupalGet('node/' . $node->id());
- // Before running cron, viewing the node should give "403 Not Authorized".
- $this->assertSession()->statusCodeEquals(403);
-
- // Delay so that the date entered is now in the past, then run cron.
- sleep(2);
- $this->cronRun();
-
- // Reload the node.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- // Check that the node has been published or unpublished as required.
- $this->assertTrue($node->isPublished() === !$data['status'], 'Scheduler has ' . $data['after'] . ' the node via cron.');
-
- // Check the node is still not viewable.
- $this->drupalGet('node/' . $node->id());
- // After cron, viewing the node should still give "403 Not Authorized".
- $this->assertSession()->statusCodeEquals(403);
- }
-
- // Log in and assert that the two dblog messages are shown.
- $this->drupalLogin($this->adminUser);
- $this->drupalGet('admin/reports/dblog');
- $this->assertSession()->pageTextContains('scheduled publishing');
- $this->assertSession()->pageTextContains('scheduled unpublishing');
- }
-
-}
diff --git a/tests/src/Functional/SchedulerNonEnabledTypeTest.php b/tests/src/Functional/SchedulerNonEnabledTypeTest.php
index 69232e4..597be86 100644
--- a/tests/src/Functional/SchedulerNonEnabledTypeTest.php
+++ b/tests/src/Functional/SchedulerNonEnabledTypeTest.php
@@ -3,36 +3,44 @@
namespace Drupal\Tests\scheduler\Functional;
/**
- * Tests a content type which is not enabled for scheduling.
+ * Tests entity types which are not enabled for scheduling.
*
* @group scheduler
*/
class SchedulerNonEnabledTypeTest extends SchedulerBrowserTestBase {
/**
- * Tests the publish_enable and unpublish_enable node type settings.
+ * Tests the publish_enable and unpublish_enable entity type settings.
*
* @dataProvider dataNonEnabledType()
*/
- public function testNonEnabledType($id, $description, $publishing_enabled, $unpublishing_enabled) {
+ public function testNonEnabledType($id, $entityTypeId, $description, $publishing_enabled, $unpublishing_enabled) {
$this->drupalLogin($this->adminUser);
-
- // The first test case specifically checks the behavior of the default
- // unchanged settings, so only change these settings for later runs.
- if ($id > 0) {
- $this->nonSchedulerNodeType->setThirdPartySetting('scheduler', 'publish_enable', $publishing_enabled)
+ $entityType = $this->entityTypeObject($entityTypeId, 'non-enabled');
+ $bundle = $entityType->id();
+ $storage = $this->entityStorageObject($entityTypeId);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+
+ // The 'default' case specifically checks the behavior of the unchanged
+ // settings, so only change these when not running the default test.
+ if ($description != 'Default') {
+ $entityType->setThirdPartySetting('scheduler', 'publish_enable', $publishing_enabled)
->setThirdPartySetting('scheduler', 'unpublish_enable', $unpublishing_enabled)
->save();
}
- // Create info string to show what combinations are being tested.
- $info = 'Publishing ' . ($publishing_enabled ? 'enabled' : 'not enabled')
- . ', Unpublishing ' . ($unpublishing_enabled ? 'enabled' : 'not enabled')
- . ', ' . $description;
+ // When publishing and/or unpublishing are not enabled but the 'required'
+ // setting remains on, the entity must be able to be saved without a date.
+ $entityType->setThirdPartySetting('scheduler', 'publish_required', !$publishing_enabled)->save();
+ $entityType->setThirdPartySetting('scheduler', 'unpublish_required', !$unpublishing_enabled)->save();
+
+ // Allow dates in the past to be valid on saving the entity, to simplify the
+ // testing process.
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
- // Check that the field(s) are displayed only for the correct settings.
- $title = $id . 'a - ' . $info;
- $this->drupalGet('node/add/' . $this->nonSchedulerNodeType->id());
+ // Create a new entity via the add/bundle url, and check that the correct
+ // fields are displayed on the form depending on the enabled settings.
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
if ($publishing_enabled) {
$this->assertSession()->fieldExists('publish_on[0][value][date]');
}
@@ -47,71 +55,97 @@ class SchedulerNonEnabledTypeTest extends SchedulerBrowserTestBase {
$this->assertSession()->fieldNotExists('unpublish_on[0][value][date]');
}
- // When publishing and/or unpublishing are not enabled but the 'required'
- // setting remains on, the node must be able to be saved without a date.
- $this->nonSchedulerNodeType->setThirdPartySetting('scheduler', 'publish_required', !$publishing_enabled)->save();
- $this->nonSchedulerNodeType->setThirdPartySetting('scheduler', 'unpublish_required', !$unpublishing_enabled)->save();
- $this->drupalPostForm('node/add/' . $this->nonSchedulerNodeType->id(), ['title[0][value]' => $title], 'Save');
- // Check that the node has saved OK.
- $string = sprintf('%s %s has been created.', $this->nonSchedulerNodeType->get('name'), $title);
- $this->assertSession()->pageTextContains($string);
-
- // Create an unpublished node with a publishing date, which mimics what
- // could be done by a third-party module, or a by-product of the node type
+ // Fill in the title field and check that the entity can be saved OK.
+ $title = $id . 'a - ' . $description;
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (created|successfully saved)/');
+
+ // Create an unpublished entity with a publishing date, which mimics what
+ // could be done by a third-party module, or a by-product of the entity type
// being enabled for publishing then being disabled before it got published.
- $title = $id . 'b - ' . $info;
- $edit = [
- 'title' => $title,
- 'status' => 0,
- 'type' => $this->nonSchedulerNodeType->id(),
- 'publish_on' => $this->requestTime - 2,
+ $title = $id . 'b - ' . $description;
+ $values = [
+ "$titleField" => $title,
+ 'status' => FALSE,
+ 'publish_on' => $this->requestTime - 120,
];
- $node = $this->drupalCreateNode($edit);
+ $entity = $this->createEntity($entityTypeId, $bundle, $values);
+
+ // Check that the entity can be edited and saved OK.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (updated|successfully saved)/');
// Run cron and display the dblog.
$this->cronRun();
$this->drupalGet('admin/reports/dblog');
- // Reload the node.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- // Check if the node has been published or remains unpublished.
+ // Reload the entity.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ // Check if the entity has been published or remains unpublished.
if ($publishing_enabled) {
- $this->assertTrue($node->isPublished(), 'The unpublished node has been published - ' . $title);
+ $this->assertTrue($entity->isPublished(), "The unpublished entity '$title' should now be published");
}
else {
- $this->assertFalse($node->isPublished(), 'The unpublished node remains unpublished - ' . $title);
+ $this->assertFalse($entity->isPublished(), "The unpublished entity '$title' should remain unpublished");
}
- // Do the same for unpublishing.
- $title = $id . 'c - ' . $info;
- $edit = [
- 'title' => $title,
- 'status' => 1,
- 'type' => $this->nonSchedulerNodeType->id(),
- 'unpublish_on' => $this->requestTime - 1,
+ // Do the same for unpublishing - create a published entity with an
+ // unpublishing date in the future, to be valid for editing and saving.
+ $title = $id . 'c - ' . $description;
+ $values = [
+ "$titleField" => $title,
+ 'status' => TRUE,
+ 'unpublish_on' => $this->requestTime + 180,
];
- $node = $this->drupalCreateNode($edit);
-
- // Run cron and display the dblog.
+ $entity = $this->createEntity($entityTypeId, $bundle, $values);
+
+ // Check that the entity can be edited and saved.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (updated|successfully saved)/');
+
+ // Create a published entity with a date in the past, then run cron.
+ $title = $id . 'd - ' . $description;
+ $values = [
+ "$titleField" => $title,
+ 'status' => TRUE,
+ 'unpublish_on' => $this->requestTime - 120,
+ ];
+ $entity = $this->createEntity($entityTypeId, $bundle, $values);
$this->cronRun();
$this->drupalGet('admin/reports/dblog');
- // Reload the node.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- // Check if the node has been unpublished or remains published.
+ // Reload the entity.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ // Check if the entity has been unpublished or remains published.
if ($unpublishing_enabled) {
- $this->assertFalse($node->isPublished(), 'The published node has been unpublished - ' . $title);
+ $this->assertFalse($entity->isPublished(), "The published entity '$title' should now be unpublished");
}
else {
- $this->assertTrue($node->isPublished(), 'The published node remains published - ' . $title);
+ $this->assertTrue($entity->isPublished(), "The published entity '$title' should remain published");
}
// Display the full content list and the scheduled list. Calls to these
- // pages are for information and debug only. They could be removed.
- $this->drupalGet('admin/content');
- $this->drupalGet('admin/content/scheduled');
+ // pages are for information and debug only.
+ switch ($entityTypeId) {
+ case 'node':
+ $this->drupalGet('admin/content');
+ $this->drupalGet('admin/content/scheduled');
+ break;
+
+ case 'media':
+ $this->drupalGet('admin/content/media');
+ $this->drupalGet('admin/content/media/scheduled');
+ break;
+
+ case 'commerce_product':
+ $this->drupalGet('admin/commerce/products');
+ $this->drupalGet('admin/commerce/products/scheduled');
+ break;
+ }
}
/**
@@ -119,34 +153,32 @@ class SchedulerNonEnabledTypeTest extends SchedulerBrowserTestBase {
*
* @return array
* Each item in the test data array has the follow elements:
- * id - (in) a sequential id for use in node titles
- * description - (string) describing the scenario being checked
- * publishing_enabled - (bool) whether publishing is enabled
- * unpublishing_enabled - (bool) whether unpublishing is enabled
+ * id - (int) a sequential id for use in titles
+ * entityTypeId - (string) 'node', 'media' or 'commerce_product'
+ * description - (string) describing the scenario being checked
+ * publishing_enabled - (bool) whether publishing is enabled
+ * unpublishing_enabled - (bool) whether unpublishing is enabled
*/
public function dataNonEnabledType() {
- $data = [
+ $data = [];
+ foreach ($this->dataStandardEntityTypes() as $key => $values) {
+ $entityTypeId = $values[0];
// By default check that the scheduler date fields are not displayed.
- 0 => [0, 'Default', FALSE, FALSE],
+ $data["$key-1"] = [1, $entityTypeId, 'Default', FALSE, FALSE];
// Explicitly disable this content type for both settings.
- 1 => [1, 'Disabling both settings', FALSE, FALSE],
+ $data["$key-2"] = [2, $entityTypeId, 'Disabling both settings', FALSE, FALSE];
// Turn on scheduled publishing only.
- 2 => [2, 'Enabling publishing only', TRUE, FALSE],
+ $data["$key-3"] = [3, $entityTypeId, 'Enabling publishing only', TRUE, FALSE];
// Turn on scheduled unpublishing only.
- 3 => [3, 'Enabling unpublishing only', FALSE, TRUE],
+ $data["$key-4"] = [4, $entityTypeId, 'Enabling unpublishing only', FALSE, TRUE];
- // For completeness turn on bothbscheduled publishing and unpublishing.
- 4 => [4, 'Enabling both publishing and unpublishing', TRUE, TRUE],
- ];
-
- // Use unset($data[n]) to remove a temporarily unwanted item, use
- // return [$data[n]] to selectively test just one item, or have the
- // default return $data to test everything.
+ // For completeness turn on both scheduled publishing and unpublishing.
+ $data["$key-5"] = [5, $entityTypeId, 'Enabling both publishing and unpublishing', TRUE, TRUE];
+ }
return $data;
-
}
}
diff --git a/tests/src/Functional/SchedulerPastDatesTest.php b/tests/src/Functional/SchedulerPastDatesTest.php
index 6440ac7..2c6a742 100644
--- a/tests/src/Functional/SchedulerPastDatesTest.php
+++ b/tests/src/Functional/SchedulerPastDatesTest.php
@@ -11,112 +11,127 @@ class SchedulerPastDatesTest extends SchedulerBrowserTestBase {
/**
* Test the different options for past publication dates.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testSchedulerPastDates() {
+ public function testSchedulerPastDates($entityTypeId, $bundle) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
+
// Log in.
$this->drupalLogin($this->schedulerUser);
- // Create an unpublished page node.
- /** @var NodeInterface $node */
- $node = $this->drupalCreateNode(['type' => $this->type, 'status' => FALSE]);
- $created_time = $node->getCreatedTime();
-
- // Test the default behavior: an error message should be shown when the user
- // enters a publication date that is in the past.
+ // Create data for use in edits.
+ $title = 'Publish in the past ' . $this->randomString(10);
$edit = [
- 'title[0][value]' => 'Past ' . $this->randomString(10),
+ "{$titleField}[0][value]" => $title,
'publish_on[0][value][date]' => $this->dateFormatter->format(strtotime('-1 day', $this->requestTime), 'custom', 'Y-m-d'),
'publish_on[0][value][time]' => $this->dateFormatter->format(strtotime('-1 day', $this->requestTime), 'custom', 'H:i:s'),
];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+
+ // Create an unpublished entity.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => FALSE]);
+ $created_time = $entity->getCreatedTime();
+
+ // Test the default behavior: an error message should be shown when the user
+ // enters a publication date that is in the past.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The 'publish on' date must be in the future");
// Test the 'error' behavior explicitly.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'error')->save();
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', 'error')->save();
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The 'publish on' date must be in the future");
- // Test the 'publish' behavior: the node should be published immediately.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ // Test the 'publish' behavior: the entity should be published immediately.
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', 'publish')->save();
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
+
// Check that no error message is shown when the publication date is in the
// past and the "publish" behavior is chosen.
$this->assertSession()->pageTextNotContains("The 'publish on' date must be in the future");
- $this->assertSession()->pageTextContains(sprintf('%s %s has been updated.', $this->typeName, $edit['title[0][value]']));
-
- // Reload the node.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
-
- // Check that the node is published and has the expected timestamps.
- $this->assertTrue($node->isPublished(), 'The node has been published immediately when the publication date is in the past and the "publish" behavior is chosen.');
- $this->assertNull($node->publish_on->value, 'The node publish_on date has been removed after publishing when the "publish" behavior is chosen.');
- $this->assertEquals($node->getChangedTime(), strtotime('-1 day', $this->requestTime), 'The changed time of the node has been updated to the publish_on time when published immediately.');
- $this->assertEquals($node->getCreatedTime(), $created_time, 'The created time of the node has not been changed when the "publish" behavior is chosen.');
-
- // Test the 'schedule' behavior: the node should be unpublished and become
- // published on the next cron run. Use a new unpublished node.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
- $node = $this->drupalCreateNode(['type' => $this->type, 'status' => FALSE]);
- $created_time = $node->getCreatedTime();
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- // Check that no error is shown when the publish_on date is in the past.
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (updated|successfully saved)/');
+
+ // Reload the entity.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+
+ // Check that the entity is published and has the expected timestamps.
+ $this->assertTrue($entity->isPublished(), 'The entity has been published immediately when the publication date is in the past and the "publish" behavior is chosen.');
+ $this->assertNull($entity->publish_on->value, 'The entity publish_on date has been removed after publishing when the "publish" behavior is chosen.');
+ $this->assertEquals($entity->getChangedTime(), strtotime('-1 day', $this->requestTime), 'The changed time of the entity has been updated to the publish_on time when published immediately.');
+ $this->assertEquals($entity->getCreatedTime(), $created_time, 'The created time of the entity has not been changed when the "publish" behavior is chosen.');
+
+ // Test the 'schedule' behavior: the entity should be unpublished and become
+ // published on the next cron run. Use a new unpublished entity.
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => FALSE]);
+ $created_time = $entity->getCreatedTime();
+
+ // Edit, save and check that no error is shown when the publish_on date is
+ // in the past.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
$this->assertSession()->pageTextNotContains("The 'publish on' date must be in the future");
- $this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published', $edit['title[0][value]']));
- $this->assertSession()->pageTextContains(sprintf('%s %s has been updated.', $this->typeName, $edit['title[0][value]']));
+ $this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published', $title));
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (updated|successfully saved)/');
- // Reload the node.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ // Reload the entity.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
- // Check that the node is unpublished but scheduled correctly.
- $this->assertFalse($node->isPublished(), 'The node has been unpublished when the publication date is in the past and the "schedule" behavior is chosen.');
- $this->assertEquals(strtotime('-1 day', $this->requestTime), (int) $node->publish_on->value, 'The node has the correct publish_on date stored.');
+ // Check that the entity is unpublished but scheduled correctly.
+ $this->assertFalse($entity->isPublished(), 'The entity has been unpublished when the publication date is in the past and the "schedule" behavior is chosen.');
+ $this->assertEquals(strtotime('-1 day', $this->requestTime), (int) $entity->publish_on->value, 'The entity has the correct publish_on date stored.');
- // Simulate a cron run and check that the node is published.
+ // Simulate a cron run and check that the entity is published.
scheduler_cron();
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- $this->assertTrue($node->isPublished(), 'The node with publication date in the past and the "schedule" behavior has now been published by cron.');
- $this->assertEquals($node->getChangedTime(), strtotime('-1 day', $this->requestTime), 'The changed time of the node has been updated to the publish_on time when published via cron.');
- $this->assertEquals($node->getCreatedTime(), $created_time, 'The created time of the node has not been changed when the "schedule" behavior is chosen.');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $this->assertTrue($entity->isPublished(), 'The entity with publication date in the past and the "schedule" behavior has now been published by cron.');
+ $this->assertEquals($entity->getChangedTime(), strtotime('-1 day', $this->requestTime), 'The changed time of the entity has been updated to the publish_on time when published via cron.');
+ $this->assertEquals($entity->getCreatedTime(), $created_time, 'The created time of the entity has not been changed when the "schedule" behavior is chosen.');
// Test the option to alter the creation time if the publishing time is
- // earlier than the node created time.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date_created', TRUE)->save();
-
+ // earlier than the entity created time.
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date_created', TRUE)->save();
$past_date_options = [
'publish' => 'publish',
'schedule' => 'schedule',
];
-
foreach ($past_date_options as $key => $option) {
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', $key)->save();
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', $key)->save();
- // Create a new node, edit and save.
- $node = $this->drupalCreateNode(['type' => $this->type, 'status' => FALSE]);
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ // Create a new unpublished entity, edit and save.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => FALSE]);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
if ($option == 'schedule') {
scheduler_cron();
}
- // Reload the node.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ // Reload the entity.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that the created time has been altered to match publishing time.
- $this->assertEquals($node->getCreatedTime(), strtotime('-1 day', $this->requestTime), sprintf('The created time of the node has not been changed when the %s option is chosen.', $option));
+ $this->assertEquals($entity->getCreatedTime(), strtotime('-1 day', $this->requestTime), sprintf('The created time of the entity has not been changed when the %s option is chosen.', $option));
}
// Check that an Unpublish date in the past fails validation.
$edit = [
- 'title[0][value]' => 'Unpublish in the past ' . $this->randomString(10),
+ "{$titleField}[0][value]" => 'Unpublish in the past ' . $this->randomString(10),
'unpublish_on[0][value][date]' => $this->dateFormatter->format($this->requestTime - 3600, 'custom', 'Y-m-d'),
'unpublish_on[0][value][time]' => $this->dateFormatter->format($this->requestTime - 3600, 'custom', 'H:i:s'),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The 'unpublish on' date must be in the future");
}
diff --git a/tests/src/Functional/SchedulerPermissionsTest.php b/tests/src/Functional/SchedulerPermissionsTest.php
index d2de79f..c71ef0d 100644
--- a/tests/src/Functional/SchedulerPermissionsTest.php
+++ b/tests/src/Functional/SchedulerPermissionsTest.php
@@ -3,57 +3,107 @@
namespace Drupal\Tests\scheduler\Functional;
/**
- * Tests the permissions of the Scheduler module.
+ * Tests some permissions of the Scheduler module.
+ *
+ * These tests check the permissions when adding and editing a scheduler-enabled
+ * node or media entity type. The permission to access the scheduled content
+ * overview and user tab views is covered in SchedulerViewsAccessTest.
*
* @group scheduler
*/
class SchedulerPermissionsTest extends SchedulerBrowserTestBase {
/**
- * Tests that users without permission do not see the scheduler date fields.
+ * {@inheritdoc}
*/
- public function testUserPermissionsAdd() {
- // Create a user who can add the content type but who does not have the
- // permission to use the scheduler functionality.
- $this->webUser = $this->drupalCreateUser([
- 'access content',
- 'administer nodes',
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Define a set of permissions which all users get. Then in addition, each
+ // user gets the specific permission to schedule their own entity type.
+ // The permission 'administer nodes' is needed when setting the node status
+ // field on edit. There is no corresponding separate permission for media or
+ // product entity types.
+ $permissions = [
'create ' . $this->type . ' content',
'edit own ' . $this->type . ' content',
- 'delete own ' . $this->type . ' content',
- 'view own unpublished content',
- ]);
- $this->drupalLogin($this->webUser);
+ 'administer nodes',
+ 'create ' . $this->mediaTypeName . ' media',
+ 'edit own ' . $this->mediaTypeName . ' media',
+ 'view own unpublished media',
+ 'create ' . $this->productTypeName . ' commerce_product',
+ 'update own ' . $this->productTypeName . ' commerce_product',
+ 'view own unpublished commerce_product',
+ // 'administer commerce_store' is needed to see and use any store, i.e
+ // cannot add a product without this. Is it a bug?
+ 'administer commerce_store',
+ ];
+
+ // Create a user who can add and edit the standard scheduler-enabled node,
+ // media and product entity types, but only schedule nodes.
+ $this->nodeUser = $this->drupalCreateUser(array_merge($permissions, ['schedule publishing of nodes']));
+ $this->nodeUser->set('name', 'Noddy the Node Editor')->save();
+
+ // Create a user who can add and edit the standard scheduler-enabled node,
+ // media and product entity types, but only schedule media.
+ $this->mediaUser = $this->drupalCreateUser(array_merge($permissions, ['schedule publishing of media']));
+ $this->mediaUser->set('name', 'Medina the Media Editor')->save();
+
+ // Create a user who can add and edit the standard scheduler-enabled node,
+ // media and product entity types, but only schedule products.
+ $this->commerce_productUser = $this->drupalCreateUser(array_merge($permissions, ['schedule publishing of commerce_product']));
+ $this->commerce_productUser->set('name', 'Proctor the Product Editor')->save();
+ }
+
+ /**
+ * Tests that users without permission do not see the scheduler date fields.
+ *
+ * @dataProvider dataPermissionsTest()
+ */
+ public function testUserPermissionsAdd($entityTypeId, $bundle, $user) {
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
- // Check that neither of the fields are displayed when creating a node.
- $this->drupalGet('node/add/' . $this->type);
- $this->assertSession()->fieldNotExists('publish_on[0][value][date]');
- $this->assertSession()->fieldNotExists('unpublish_on[0][value][date]');
+ // Log in with the required user, as specified by the parameter.
+ $this->drupalLogin($this->$user);
// Initially run tests when publishing and unpublishing are not required.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_required', FALSE)
+ $this->entityTypeObject($entityTypeId)->setThirdPartySetting('scheduler', 'publish_required', FALSE)
->setThirdPartySetting('scheduler', 'unpublish_required', FALSE)
->save();
- // Check that a new node can be saved and published.
- $title = $this->randomString(15);
- $edit = ['title[0][value]' => $title, 'status[value]' => TRUE];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $this->assertSession()->pageTextContains(sprintf('%s %s has been created.', $this->typeName, $title));
- $this->assertTrue($this->drupalGetNodeByTitle($title)->isPublished(), 'The new node is published');
-
- // Check that a new node can be saved as unpublished.
- $title = $this->randomString(15);
- $edit = ['title[0][value]' => $title, 'status[value]' => FALSE];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $this->assertSession()->pageTextContains(sprintf('%s %s has been created.', $this->typeName, $title));
- $this->assertFalse($this->drupalGetNodeByTitle($title)->isPublished(), 'The new node is unpublished');
+ // Check that the fields are displayed as expected when creating an entity.
+ // If the user variable matches the entity type id then that user has
+ // scheduling permission on this type, so the fields should be shown.
+ // Otherwise the fields should not be shown.
+ $add_url = $this->entityAddUrl($entityTypeId, $bundle);
+ $this->drupalGet($add_url);
+ if (strpos($user, $entityTypeId) !== FALSE) {
+ $this->assertSession()->fieldExists('publish_on[0][value][date]');
+ $this->assertSession()->fieldExists('unpublish_on[0][value][date]');
+ }
+ else {
+ $this->assertSession()->fieldNotExists('publish_on[0][value][date]');
+ $this->assertSession()->fieldNotExists('unpublish_on[0][value][date]');
+ }
+
+ // Check that the new entity can be saved and published.
+ $title = 'Published - ' . $this->randomString(15);
+ $edit = ["{$titleField}[0][value]" => $title, 'status[value]' => TRUE];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (created|successfully saved)/');
+ $this->assertNotEmpty($entity = $this->getEntityByTitle($entityTypeId, $title), sprintf('The new %s with title "%s" was created sucessfully.', $entityTypeId, $title));
+ $this->assertTrue($entity->isPublished(), 'The new entity is published');
+
+ // Check that a new entity can be saved as unpublished.
+ $title = 'Unpublished - ' . $this->randomString(15);
+ $edit = ["{$titleField}[0][value]" => $title, 'status[value]' => FALSE];
+ $this->drupalGet($add_url);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (created|successfully saved)/');
+ $this->assertNotEmpty($entity = $this->getEntityByTitle($entityTypeId, $title), sprintf('The new %s with title "%s" was created sucessfully.', $entityTypeId, $title));
+ $this->assertFalse($entity->isPublished(), 'The new entity is unpublished');
// Set publishing and unpublishing to required, to make it a stronger test.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_required', TRUE)
- ->setThirdPartySetting('scheduler', 'unpublish_required', TRUE)
- ->save();
-
// @todo Add tests when scheduled publishing and unpublishing are required.
// Cannot be done until we make a decision on what 'required' means.
// @see https://www.drupal.org/node/2707411
@@ -63,64 +113,89 @@ class SchedulerPermissionsTest extends SchedulerBrowserTestBase {
/**
* Tests that users without permission can edit existing scheduled content.
+ *
+ * @dataProvider dataPermissionsTest()
*/
- public function testUserPermissionsEdit() {
- // Create a user who can add the content type but who does not have the
- // permission to use the scheduler functionality.
- $this->webUser = $this->drupalCreateUser([
- 'access content',
- 'administer nodes',
- 'create ' . $this->type . ' content',
- 'edit own ' . $this->type . ' content',
- 'delete own ' . $this->type . ' content',
- 'view own unpublished content',
- ]);
- $this->drupalLogin($this->webUser);
+ public function testUserPermissionsEdit($entityTypeId, $bundle, $user) {
+ $storage = $this->entityStorageObject($entityTypeId);
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+
+ // Log in with the required user, as specified by the parameter.
+ $this->drupalLogin($this->$user);
$publish_time = strtotime('+ 6 hours', $this->requestTime);
$unpublish_time = strtotime('+ 10 hours', $this->requestTime);
- // Create nodes with publish_on and unpublish_on dates.
- $unpublished_node = $this->drupalCreateNode([
- 'type' => $this->type,
+ // Create an unpublished entity with a publish_on date.
+ $unpublished_entity = $this->createEntity($entityTypeId, $bundle, [
'status' => FALSE,
'publish_on' => $publish_time,
]);
- $published_node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => TRUE,
- 'unpublish_on' => $unpublish_time,
- ]);
// Verify that the publish_on date is stored as expected before editing.
- $this->assertEquals($publish_time, $unpublished_node->publish_on->value, 'The publish_on value is stored correctly before edit.');
-
- // Edit the unpublished node and save.
+ $this->assertEquals($publish_time, $unpublished_entity->publish_on->value, 'The publish_on value is stored correctly before edit.');
+
+ // Edit the unpublished entity and check that the fields are displayed as
+ // expected, depending on the user.
+ $this->drupalGet($unpublished_entity->toUrl('edit-form'));
+ if (strpos($user, $entityTypeId) !== FALSE) {
+ $this->assertSession()->fieldExists('publish_on[0][value][date]');
+ $this->assertSession()->fieldExists('unpublish_on[0][value][date]');
+ }
+ else {
+ $this->assertSession()->fieldNotExists('publish_on[0][value][date]');
+ $this->assertSession()->fieldNotExists('unpublish_on[0][value][date]');
+ }
+
+ // Save the entity and check the title is updated as expected.
$title = 'For Publishing ' . $this->randomString(10);
- $this->drupalPostForm('node/' . $unpublished_node->id() . '/edit', ['title[0][value]' => $title], 'Save');
-
- // Check the updated title, to verify that edit and save was sucessful.
- $unpublished_node = $this->nodeStorage->load($unpublished_node->id());
- $this->assertEquals($title, $unpublished_node->title->value, 'The unpublished node title has been updated correctly after edit.');
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $unpublished_entity = $storage->load($unpublished_entity->id());
+ $this->assertEquals($title, $unpublished_entity->label(), 'The unpublished entity title has been updated correctly after edit.');
// Test that the publish_on date is still stored and is unchanged.
- $this->assertEquals($publish_time, $unpublished_node->publish_on->value, 'The node publish_on value is still stored correctly after edit.');
+ $this->assertEquals($publish_time, $unpublished_entity->publish_on->value, 'The publish_on value is still stored correctly after edit.');
+
+ // Repeat for unpublishing. Create an entity scheduled for unpublishing.
+ $published_entity = $this->createEntity($entityTypeId, $bundle, [
+ 'status' => TRUE,
+ 'unpublish_on' => $unpublish_time,
+ ]);
- // Do the same for unpublishing.
// Verify that the unpublish_on date is stored as expected before editing.
- $this->assertEquals($unpublish_time, $published_node->unpublish_on->value, 'The unpublish_on value is stored correctly before edit.');
+ $this->assertEquals($unpublish_time, $published_entity->unpublish_on->value, 'The unpublish_on value is stored correctly before edit.');
- // Edit the published node and save.
+ // Edit the published entity and save.
$title = 'For Unpublishing ' . $this->randomString(10);
- $this->drupalPostForm('node/' . $published_node->id() . '/edit', ['title[0][value]' => $title], 'Save');
+ $this->drupalGet($published_entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
// Check the updated title, to verify that edit and save was sucessful.
- $published_node = $this->nodeStorage->load($published_node->id());
- $this->assertEquals($title, $published_node->title->value, 'The published node title has been updated correctly after edit.');
+ $published_entity = $storage->load($published_entity->id());
+ $this->assertEquals($title, $published_entity->label(), 'The published entity title has been updated correctly after edit.');
// Test that the unpublish_on date is still stored and is unchanged.
- $this->assertEquals($unpublish_time, $published_node->unpublish_on->value, 'The node unpublish_on value is still stored correctly after edit.');
+ $this->assertEquals($unpublish_time, $published_entity->unpublish_on->value, 'The unpublish_on value is still stored correctly after edit.');
+ }
+ /**
+ * Provides data for testUserPermissionsAdd() and testUserPermissionsEdit()
+ *
+ * The data in dataStandardEntityTypes() is expanded to test each entity type
+ * with users who only have scheduler permission on one entity type and no
+ * permission for the other entity types.
+ *
+ * @return array
+ * Each array item has the values: [entity type id, bundle id, user name].
+ */
+ public function dataPermissionsTest() {
+ $data = [];
+ foreach ($this->dataStandardEntityTypes() as $key => $values) {
+ $data["$key-1"] = array_merge($values, ['nodeUser']);
+ $data["$key-2"] = array_merge($values, ['mediaUser']);
+ $data["$key-3"] = array_merge($values, ['commerce_productUser']);
+ }
+ return $data;
}
}
diff --git a/tests/src/Functional/SchedulerRequiredTest.php b/tests/src/Functional/SchedulerRequiredTest.php
index f780136..dc034e4 100644
--- a/tests/src/Functional/SchedulerRequiredTest.php
+++ b/tests/src/Functional/SchedulerRequiredTest.php
@@ -73,7 +73,8 @@ class SchedulerRequiredTest extends SchedulerBrowserTestBase {
'unpublish_on[0][value][time]' => '',
];
// Add or edit the node.
- $this->drupalPostForm($path, $values, 'Save');
+ $this->drupalGet($path);
+ $this->submitForm($values, 'Save');
// Check for the expected result.
if ($publish_expected) {
diff --git a/tests/src/Functional/SchedulerRevisioningTest.php b/tests/src/Functional/SchedulerRevisioningTest.php
index 56e0a82..ece8a1e 100644
--- a/tests/src/Functional/SchedulerRevisioningTest.php
+++ b/tests/src/Functional/SchedulerRevisioningTest.php
@@ -2,7 +2,7 @@
namespace Drupal\Tests\scheduler\Functional;
-use Drupal\node\NodeInterface;
+use Drupal\Core\Entity\EntityInterface;
/**
* Tests revision options when Scheduler publishes or unpublishes content.
@@ -12,147 +12,139 @@ use Drupal\node\NodeInterface;
class SchedulerRevisioningTest extends SchedulerBrowserTestBase {
/**
- * Simulates the scheduled (un)publication of a node.
+ * Simulates the scheduled (un)publication of an entity.
*
- * @param \Drupal\node\NodeInterface $node
- * The node to schedule.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to schedule.
* @param string $action
- * The action to perform: either 'publish' or 'unpublish'. Defaults to
- * 'publish'.
+ * The action to perform: either 'publish' or 'unpublish'.
*
- * @return \Drupal\node\NodeInterface
- * The updated node, after scheduled (un)publication via a cron run.
+ * @return \Drupal\Core\Entity\EntityInterface
+ * The updated entity, after scheduled (un)publication via a cron run.
*/
- protected function schedule(NodeInterface $node, $action = 'publish') {
+ protected function scheduleAndRunCron(EntityInterface $entity, string $action) {
// Simulate scheduling by setting the (un)publication date in the past and
// running cron.
- $node->{$action . '_on'} = strtotime('-5 hour', $this->requestTime);
- $node->save();
+ $entity->{$action . '_on'} = strtotime('-5 hour', $this->requestTime);
+ $entity->save();
scheduler_cron();
- $this->nodeStorage->resetCache([$node->id()]);
- return $this->nodeStorage->load($node->id());
+ $storage = $this->entityStorageObject($entity->getEntityTypeId());
+ $storage->resetCache([$entity->id()]);
+ return $storage->load($entity->id());
}
/**
- * Check if the number of revisions for a node matches a given value.
+ * Check if the number of revisions for an entity matches a given value.
*
- * @param int $nid
- * The node id of the node to check.
- * @param string $value
- * The value with which the number of revisions will be compared.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to check.
+ * @param int $expected
+ * The expected number of revisions.
* @param string $message
* The message to display along with the assertion.
*/
- protected function assertRevisionCount($nid, $value, $message = '') {
+ protected function assertRevisionCount(EntityInterface $entity, int $expected, string $message = '') {
+ if (!$entity->getEntityType()->isRevisionable()) {
+ return;
+ }
// Because we are not deleting any revisions we can take a short cut and use
// getLatestRevisionId() which will effectively be the number of revisions.
- $count = $this->nodeStorage->getLatestRevisionId($nid);
- $this->assertEquals($value, (int) $count, $message);
+ $storage = $this->entityStorageObject($entity->getEntityTypeId());
+ $count = $storage->getLatestRevisionId($entity->id());
+ $this->assertEquals($expected, (int) $count, $message);
}
/**
- * Check if the latest revision log message of a node matches a given string.
+ * Tests the creation of new revisions on scheduling.
*
- * @param int $nid
- * The node id of the node to check.
- * @param string $value
- * The value with which the log message will be compared.
- * @param string $message
- * The message to display along with the assertion.
+ * This test is still useful for Commerce Products which are not revisionable
+ * because it shows that this entity type can be processed correctly even if
+ * the scheduler revision option is incorrectly set on.
*
- * @return bool
- * TRUE if the assertion succeeded, FALSE otherwise.
+ * @dataProvider dataStandardEntityTypes()
*/
- protected function assertRevisionLogMessage($nid, $value, $message = '') {
- // Retrieve the latest revision log message for this node.
- $log_message = $this->database->select('node_revision', 'r')
- ->fields('r', ['revision_log'])
- ->condition('nid', $nid)
- ->orderBy('vid', 'DESC')
- ->range(0, 1)
- ->execute()
- ->fetchField();
-
- return $this->assertEquals($value, $log_message, $message);
- }
+ public function testNewRevision($entityTypeId, $bundle) {
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
- /**
- * Tests the creation of new revisions on scheduling.
- */
- public function testRevisioning() {
- // Create a scheduled node that is not automatically revisioned.
- $created = strtotime('-2 day', $this->requestTime);
- $settings = [
- 'type' => $this->type,
- 'revision' => 0,
- 'created' => $created,
- ];
- $node = $this->drupalCreateNode($settings);
+ // Create a scheduled entity that is not automatically revisioned.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['revision' => 0]);
+ $this->assertRevisionCount($entity, 1, 'The initial revision count is 1 when the entity is created.');
- // Ensure nodes with past dates will be scheduled not published immediately.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
+ // Ensure entities with past dates are scheduled not published immediately.
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
// First test scheduled publication with revisioning disabled by default.
- $node = $this->schedule($node);
- $this->assertRevisionCount($node->id(), 1, 'No new revision is created by default when a node is published.');
+ $entity = $this->scheduleAndRunCron($entity, 'publish');
+ $this->assertRevisionCount($entity, 1, 'No new revision is created by default when entity is published. Revision count remains at 1.');
// Test scheduled unpublication.
- $node = $this->schedule($node, 'unpublish');
- $this->assertRevisionCount($node->id(), 1, 'No new revision is created by default when a node is unpublished.');
+ $entity = $this->scheduleAndRunCron($entity, 'unpublish');
+ $this->assertRevisionCount($entity, 1, 'No new revision is created by default when entity is unpublished. Revision count remains at 1.');
// Enable revisioning.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_revision', TRUE)
+ $entityType->setThirdPartySetting('scheduler', 'publish_revision', TRUE)
->setThirdPartySetting('scheduler', 'unpublish_revision', TRUE)
->save();
// Test scheduled publication with revisioning enabled.
- $node = $this->schedule($node);
- $this->assertRevisionCount($node->id(), 2, 'A new revision was created when revisioning is enabled.');
- $expected_message = sprintf('Published by Scheduler. The scheduled publishing date was %s.',
- $this->dateFormatter->format(strtotime('-5 hour', $this->requestTime), 'short'));
- $this->assertRevisionLogMessage($node->id(), $expected_message, 'The correct message was found in the node revision log after scheduled publishing.');
+ $entity = $this->scheduleAndRunCron($entity, 'publish');
+ $this->assertTrue($entity->isPublished(), 'Entity is published after cron.');
+
+ if ($entity->getEntityType()->isRevisionable()) {
+ $this->assertRevisionCount($entity, 2, 'A new revision was created when the entity was published with revisioning enabled.');
+ $expected_message = sprintf('Published by Scheduler. The scheduled publishing date was %s.',
+ $this->dateFormatter->format(strtotime('-5 hour', $this->requestTime), 'short'));
+ $this->assertEquals($entity->getRevisionLogMessage(), $expected_message, 'The correct message was found in the entity revision log after scheduled publishing.');
+ }
// Test scheduled unpublication with revisioning enabled.
- $node = $this->schedule($node, 'unpublish');
- $this->assertRevisionCount($node->id(), 3, 'A new revision was created when a node was unpublished with revisioning enabled.');
- $expected_message = sprintf('Unpublished by Scheduler. The scheduled unpublishing date was %s.',
- $this->dateFormatter->format(strtotime('-5 hour', $this->requestTime), 'short'));
- $this->assertRevisionLogMessage($node->id(), $expected_message, 'The correct message was found in the node revision log after scheduled unpublishing.');
+ $entity = $this->scheduleAndRunCron($entity, 'unpublish');
+ $this->assertFalse($entity->isPublished(), 'Entity is unpublished after cron.');
+
+ if ($entity->getEntityType()->isRevisionable()) {
+ $this->assertRevisionCount($entity, 3, 'A new revision was created when the entity was unpublished with revisioning enabled.');
+ $expected_message = sprintf('Unpublished by Scheduler. The scheduled unpublishing date was %s.',
+ $this->dateFormatter->format(strtotime('-5 hour', $this->requestTime), 'short'));
+ $this->assertEquals($entity->getRevisionLogMessage(), $expected_message, 'The correct message was found in the entity revision log after scheduled unpublishing.');
+ }
}
/**
- * Tests the 'touch' option to alter the node created date during publishing.
+ * Tests the 'touch' option to alter the created date during publishing.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testAlterCreationDate() {
- // Ensure nodes with past dates will be scheduled not published immediately.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
+ public function testAlterCreationDate($entityTypeId, $bundle) {
+ // Ensure entities with past dates are scheduled not published immediately.
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
+ $entityType->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
- // Create a node with a 'created' date two days in the past.
+ // Create an entity with a 'created' date two days in the past.
$created = strtotime('-2 day', $this->requestTime);
$settings = [
- 'type' => $this->type,
'created' => $created,
'status' => FALSE,
];
- $node = $this->drupalCreateNode($settings);
- // Show that the node is not published.
- $this->assertFalse($node->isPublished(), 'The node is not published.');
+ $entity = $this->createEntity($entityTypeId, $bundle, $settings);
+
+ // Show that the entity is not published.
+ $this->assertFalse($entity->isPublished(), 'The entity is not published.');
- // Schedule the node for publishing and run cron.
- $node = $this->schedule($node, 'publish');
- // Get the created date from the node and check that it has not changed.
- $created_after_cron = $node->created->value;
- $this->assertTrue($node->isPublished(), 'The node has been published.');
- $this->assertEquals($created, $created_after_cron, 'The node creation date is not changed by default.');
+ // Schedule the entity for publishing and run cron.
+ $entity = $this->scheduleAndRunCron($entity, 'publish');
+ // Get the created date from the entity and check that it has not changed.
+ $created_after_cron = $entity->created->value;
+ $this->assertTrue($entity->isPublished(), 'The entity has been published.');
+ $this->assertEquals($created, $created_after_cron, 'The entity creation date is not changed by default.');
// Set option to change the created date to match the publish_on date.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_touch', TRUE)->save();
+ $entityType->setThirdPartySetting('scheduler', 'publish_touch', TRUE)->save();
- // Schedule the node again and run cron.
- $node = $this->schedule($node, 'publish');
+ // Schedule the entity again and run cron.
+ $entity = $this->scheduleAndRunCron($entity, 'publish');
// Check that the created date has changed to match the publish_on date.
- $created_after_cron = $node->created->value;
- $this->assertEquals(strtotime('-5 hour', $this->requestTime), $created_after_cron, "With 'touch' option set, the node creation date is changed to match the publishing date.");
+ $created_after_cron = $entity->created->value;
+ $this->assertEquals(strtotime('-5 hour', $this->requestTime), $created_after_cron, "With 'touch' option set, the entity creation date is changed to match the publishing date.");
}
diff --git a/tests/src/Functional/SchedulerRulesActionsTest.php b/tests/src/Functional/SchedulerRulesActionsTest.php
index 94b3593..c3a3684 100644
--- a/tests/src/Functional/SchedulerRulesActionsTest.php
+++ b/tests/src/Functional/SchedulerRulesActionsTest.php
@@ -29,43 +29,56 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
$this->expressionManager = $this->container->get('plugin.manager.rules_expression');
$this->drupalLogin($this->adminUser);
- // Create node A which is published and enabled for Scheduling.
- $this->node_a = $this->drupalCreateNode([
- 'title' => 'Initial Test Node',
- 'type' => $this->type,
- 'uid' => $this->adminUser->id(),
- 'status' => TRUE,
- ]);
+ }
- // Create node B which is published but not enabled for Scheduling.
- $this->node_b = $this->drupalCreateNode([
- 'title' => 'Something Else',
- 'type' => $this->nonSchedulerNodeType->id(),
- 'uid' => $this->adminUser->id(),
- 'status' => TRUE,
- ]);
+ /**
+ * Provides test data.
+ *
+ * @return array
+ * Each array item has the values:
+ * [entity type id, enabled bundle id, non-enabled bundle id].
+ */
+ public function dataRulesActions() {
+ $data = [
+ '#node' => ['node', $this->type, $this->nonSchedulerType],
+ '#media' => ['media', $this->mediaTypeName, $this->nonSchedulerMediaTypeName],
+ '#commerce_product' => ['commerce_product', $this->productTypeName, $this->nonSchedulerProductTypeName],
+ ];
+ return $data;
}
/**
* Tests the actions which set and remove the 'Publish On' date.
+ *
+ * @dataProvider dataRulesActions()
*/
- public function testPublishOnActions() {
-
+ public function testPublishOnActions($entityTypeId, $enabled, $nonEnabled) {
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
$publish_on = $this->requestTime + 1800;
$publish_on_formatted = $this->dateFormatter->format($publish_on, 'long');
+ // The legacy rules action ids for nodes remain as:
+ // - scheduler_set_publishing_date_action
+ // - scheduler_publish_now_action
+ // For all other entity types the new derived action ids are of the form:
+ // - scheduler_set_publishing_date:{type}
+ // - scheduler_publish_now:{type}
+ // .
+ $action_suffix = ($entityTypeId == 'node') ? '_action' : ":$entityTypeId";
+ $storage = $this->entityStorageObject($entityTypeId);
+
// Create rule 1 to set the publishing date.
$rule1 = $this->expressionManager->createRule();
$rule1->addCondition('rules_data_comparison',
ContextConfig::create()
- ->map('data', 'node.title.value')
+ ->map('data', "$entityTypeId.$titleField.value")
->setValue('operation', 'contains')
- ->setValue('value', 'Trigger Action Rule 1')
+ ->setValue('value', 'Trigger Rule 1')
);
$message1 = 'RULES message 1. Action to set Publish-on date.';
- $rule1->addAction('scheduler_set_publishing_date_action',
+ $rule1->addAction("scheduler_set_publishing_date$action_suffix",
ContextConfig::create()
- ->map('node', 'node')
+ ->map("$entityTypeId", "$entityTypeId")
->setValue('date', $publish_on)
)
->addAction('rules_system_message',
@@ -73,31 +86,31 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
->setValue('message', $message1)
->setValue('type', 'status')
);
- // The event needs to be rules_entity_presave:node 'before saving' because
- // rules_entity_update:node 'after save' is too late to set the date.
+ // The event needs to be rules_entity_presave:{type} 'before saving' because
+ // rules_entity_update:{type} 'after save' is too late to set the date.
$config_entity = $this->rulesStorage->create([
'id' => 'rule1',
- 'events' => [['event_name' => 'rules_entity_presave:node']],
+ 'events' => [['event_name' => "rules_entity_presave:$entityTypeId"]],
'expression' => $rule1->getConfiguration(),
]);
$config_entity->save();
- // Create rule 2 to remove the publishing date and publish the node.
+ // Create rule 2 to remove the publishing date and publish the entity.
$rule2 = $this->expressionManager->createRule();
$rule2->addCondition('rules_data_comparison',
ContextConfig::create()
- ->map('data', 'node.title.value')
+ ->map('data', "$entityTypeId.$titleField.value")
->setValue('operation', 'contains')
- ->setValue('value', 'Trigger Action Rule 2')
+ ->setValue('value', 'Trigger Rule 2')
);
- $message2 = 'RULES message 2. Action to remove Publish-on date and publish the node immediately.';
- $rule2->addAction('scheduler_remove_publishing_date_action',
+ $message2 = 'RULES message 2. Action to remove Publish-on date and publish immediately.';
+ $rule2->addAction("scheduler_remove_publishing_date$action_suffix",
ContextConfig::create()
- ->map('node', 'node')
+ ->map("$entityTypeId", "$entityTypeId")
)
- ->addAction('scheduler_publish_now_action',
+ ->addAction("scheduler_publish_now$action_suffix",
ContextConfig::create()
- ->map('node', 'node')
+ ->map("$entityTypeId", "$entityTypeId")
)
->addAction('rules_system_message',
ContextConfig::create()
@@ -106,130 +119,114 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule2',
- 'events' => [['event_name' => 'rules_entity_presave:node']],
+ 'events' => [['event_name' => "rules_entity_presave:$entityTypeId"]],
'expression' => $rule2->getConfiguration(),
]);
$config_entity->save();
$assert = $this->assertSession();
- // First, create a new scheduler-enabled node, triggering rule 1.
- $edit = [
- 'title[0][value]' => 'New node - Trigger Action Rule 1',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node = $this->drupalGetNodeByTitle('New node - Trigger Action Rule 1');
- $this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s', 'New node - Trigger Action Rule 1', $publish_on_formatted));
+ // First, create a new scheduler-enabled entity, triggering rule 1.
+ $title = "First - new enabled $enabled - Trigger Rule 1";
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $enabled));
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->assertSession()->pageTextContains(sprintf('%s is scheduled to be published %s', $title, $publish_on_formatted));
// Check that rule 1 is triggered and rule 2 is not. Check that a publishing
// date has been set and the status is now unpublished.
$assert->pageTextContains($message1);
$assert->pageTextNotContains($message2);
- $this->assertEquals($node->publish_on->value, $publish_on, 'Node is scheduled for publishing at the correct time.');
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
- $this->assertFalse($node->isPublished(), 'Node is now unpublished for title: "' . $node->title->value . '".');
+ $this->assertEquals($entity->publish_on->value, $publish_on, 'Entity should be scheduled for publishing at the correct time');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing.');
+ $this->assertFalse($entity->isPublished(), 'Entity should be unpublished');
- // Second, edit a pre-existing Scheduler-enabled node, without triggering
+ // Second, edit a pre-existing Scheduler-enabled entity, without triggering
// either of the rules.
- $node = $this->node_a;
- $edit = [
- 'title[0][value]' => 'Edit node - but no rules will be triggered',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ $entity = $this->createEntity($entityTypeId, $enabled, [
+ "$titleField" => "Second - existing enabled $enabled",
+ ]);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit enabled $enabled - but no rules will be triggered"], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that neither of the rules are triggered, no publish and unpublish
// dates are set and the status is still published.
$assert->pageTextNotContains($message1);
$assert->pageTextNotContains($message2);
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
- $this->assertTrue($node->isPublished(), 'Node remains published for title: "' . $node->title->value . '".');
-
- // Edit the node, triggering rule 1.
- $edit = [
- 'title[0][value]' => 'Edit node - Trigger Action Rule 1',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing');
+ $this->assertTrue($entity->isPublished(), 'Entity should remain published');
+
+ // Edit the entity, triggering rule 1.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit enabled $enabled - Trigger Rule 1"], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that rule 1 is triggered and rule 2 is not. Check that a publishing
// date has been set and the status is now unpublished.
$assert->pageTextContains($message1);
$assert->pageTextNotContains($message2);
- $this->assertNotEmpty($node->publish_on->value, 'Node is scheduled for publishing.');
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
- $this->assertFalse($node->isPublished(), 'Node is now unpublished for title: "' . $node->title->value . '".');
-
- // Edit the node, triggering rule 2.
- $edit = [
- 'title[0][value]' => 'Edit node - Trigger Action Rule 2',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ $this->assertEquals($entity->publish_on->value, $publish_on, 'Entity should be scheduled for publishing at the correct time');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing');
+ $this->assertFalse($entity->isPublished(), 'Entity should be unpublished');
+
+ // Edit the entity, triggering rule 2.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit enabled $enabled - Trigger Rule 2"], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that rule 2 is triggered and rule 1 is not. Check that the
// publishing date has been removed and the status is now published.
$assert->pageTextNotContains($message1);
$assert->pageTextContains($message2);
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
- $this->assertTrue($node->isPublished(), 'Node is now published for title: "' . $node->title->value . '".');
-
- // Third, create a new node which is not scheduler-enabled.
- $edit = [
- 'title[0][value]' => 'New non-enabled node - Trigger Action Rule 1',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/add/' . $this->nonSchedulerNodeType->id(), $edit, 'Save');
- $node = $this->drupalGetNodeByTitle('New non-enabled node - Trigger Action Rule 1');
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing.');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing.');
+ $this->assertTrue($entity->isPublished(), 'Entity should be published.');
+
+ // Third, create a new entity which is not scheduler-enabled.
+ $title = "Third - new non-enabled $nonEnabled - Trigger Rule 1";
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $nonEnabled));
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
// Check that rule 1 issued a warning message.
$assert->pageTextContains('warning message');
$assert->elementExists('xpath', '//div[@aria-label="Warning message" and contains(string(), "Action")]');
// Check that no publishing date is set.
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing');
// Check that a log message has been recorded.
$log = \Drupal::database()->select('watchdog', 'w')
->condition('type', 'scheduler')
->condition('severity', RfcLogLevel::WARNING)
->countQuery()
->execute()
- ->fetchColumn();
+ ->fetchField();
$this->assertEquals(1, $log, 'There is 1 watchdog warning message from Scheduler');
- // Fourthly, edit a pre-existing node which is not enabled for Scheduler.
- $node = $this->node_b;
-
- // Edit the node, triggering rule 1.
- $edit = [
- 'title[0][value]' => 'Edit non-enabled node - Trigger Action Rule 1',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ // Fourthly, edit a pre-existing entity which is not enabled for Scheduler,
+ // triggering rule 1.
+ $entity = $this->createEntity($entityTypeId, $nonEnabled, [
+ "$titleField" => "Fourth - existing non-enabled $nonEnabled",
+ ]);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit non-enabled $nonEnabled - Trigger Rule 1"], 'Save');
// Check that rule 1 issued a warning message.
$assert->pageTextContains('warning message');
$assert->elementExists('xpath', '//div[@aria-label="Warning message" and contains(string(), "Action")]');
// Check that no publishing date is set.
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing.');
// Check that a log message has been recorded.
$log = \Drupal::database()->select('watchdog', 'w')
->condition('type', 'scheduler')
->condition('severity', RfcLogLevel::WARNING)
->countQuery()
->execute()
- ->fetchColumn();
+ ->fetchField();
$this->assertEquals(2, $log, 'There are now 2 watchdog warning messages from Scheduler');
- // Edit the node, triggering rule 2.
- $edit = [
- 'title[0][value]' => 'Edit non-enabled node - Trigger Action Rule 2',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ // Edit the entity again, triggering rule 2.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit non-enabled $nonEnabled - Trigger Rule 2"], 'Save');
// Check that rule 2 issued a warning message.
$assert->pageTextContains('warning message');
$assert->elementExists('xpath', '//div[@aria-label="Warning message" and contains(string(), "Action")]');
@@ -239,30 +236,43 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
->condition('severity', RfcLogLevel::WARNING)
->countQuery()
->execute()
- ->fetchColumn();
+ ->fetchField();
$this->assertEquals(3, $log, 'There are now 3 watchdog warning messages from Scheduler');
+ $this->drupalGet('admin/reports/dblog');
}
/**
* Tests the actions which set and remove the 'Unpublish On' date.
+ *
+ * @dataProvider dataRulesActions()
*/
- public function testUnpublishOnActions() {
-
+ public function testUnpublishOnActions($entityTypeId, $enabled, $nonEnabled) {
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
$unpublish_on = $this->requestTime + 2400;
$unpublish_on_formatted = $this->dateFormatter->format($unpublish_on, 'long');
+ // The legacy rules action ids for nodes remain as:
+ // - scheduler_set_unpublishing_date_action
+ // - scheduler_unpublish_now_action
+ // For all other entity types the new derived action ids are of the form:
+ // - scheduler_set_unpublishing_date:{type}
+ // - scheduler_unpublish_now:{type}
+ // .
+ $action_suffix = ($entityTypeId == 'node') ? '_action' : ":$entityTypeId";
+ $storage = $this->entityStorageObject($entityTypeId);
+
// Create rule 3 to set the unpublishing date.
$rule3 = $this->expressionManager->createRule();
$rule3->addCondition('rules_data_comparison',
ContextConfig::create()
- ->map('data', 'node.title.value')
+ ->map('data', "$entityTypeId.$titleField.value")
->setValue('operation', 'contains')
- ->setValue('value', 'Trigger Action Rule 3')
+ ->setValue('value', 'Trigger Rule 3')
);
$message3 = 'RULES message 3. Action to set Unpublish-on date.';
- $rule3->addAction('scheduler_set_unpublishing_date_action',
+ $rule3->addAction("scheduler_set_unpublishing_date$action_suffix",
ContextConfig::create()
- ->map('node', 'node')
+ ->map("$entityTypeId", "$entityTypeId")
->setValue('date', $unpublish_on)
)
->addAction('rules_system_message',
@@ -270,31 +280,31 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
->setValue('message', $message3)
->setValue('type', 'status')
);
- // The event needs to be rules_entity_presave:node 'before saving' because
- // rules_entity_update:node 'after save' is too late to set the date.
+ // The event needs to be rules_entity_presave:{type} 'before saving' because
+ // rules_entity_update:{type} 'after save' is too late to set the date.
$config_entity = $this->rulesStorage->create([
'id' => 'rule3',
- 'events' => [['event_name' => 'rules_entity_presave:node']],
+ 'events' => [['event_name' => "rules_entity_presave:$entityTypeId"]],
'expression' => $rule3->getConfiguration(),
]);
$config_entity->save();
- // Create rule 4 to remove the unpublishing date and unpublish the node.
+ // Create rule 4 to remove the unpublishing date and unpublish the entity.
$rule4 = $this->expressionManager->createRule();
$rule4->addCondition('rules_data_comparison',
ContextConfig::create()
- ->map('data', 'node.title.value')
+ ->map('data', "$entityTypeId.$titleField.value")
->setValue('operation', 'contains')
- ->setValue('value', 'Trigger Action Rule 4')
+ ->setValue('value', 'Trigger Rule 4')
);
$message4 = 'RULES message 4. Action to remove Unpublish-on date and unpublish the node immediately.';
- $rule4->addAction('scheduler_remove_unpublishing_date_action',
+ $rule4->addAction("scheduler_remove_unpublishing_date$action_suffix",
ContextConfig::create()
- ->map('node', 'node')
+ ->map("$entityTypeId", "$entityTypeId")
)
- ->addAction('scheduler_unpublish_now_action',
+ ->addAction("scheduler_unpublish_now$action_suffix",
ContextConfig::create()
- ->map('node', 'node')
+ ->map("$entityTypeId", "$entityTypeId")
)
->addAction('rules_system_message',
ContextConfig::create()
@@ -303,130 +313,114 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule4',
- 'events' => [['event_name' => 'rules_entity_presave:node']],
+ 'events' => [['event_name' => "rules_entity_presave:$entityTypeId"]],
'expression' => $rule4->getConfiguration(),
]);
$config_entity->save();
$assert = $this->assertSession();
- // First, create a new scheduler-enabled node, triggering rule 3.
- $edit = [
- 'title[0][value]' => 'New node - Trigger Action Rule 3',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node = $this->drupalGetNodeByTitle('New node - Trigger Action Rule 3');
- $this->assertSession()->pageTextContains(sprintf('%s is scheduled to be unpublished %s', 'New node - Trigger Action Rule 3', $unpublish_on_formatted));
+ // First, create a new scheduler-enabled entity, triggering rule 3.
+ $title = "First - new enabled $enabled - Trigger Rule 3";
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $enabled));
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->assertSession()->pageTextContains(sprintf('%s is scheduled to be unpublished %s', $title, $unpublish_on_formatted));
// Check that rule 3 is triggered and rule 4 is not. Check that a publishing
// date has been set and the status is now unpublished.
$assert->pageTextContains($message3);
$assert->pageTextNotContains($message4);
- $this->assertEquals($node->unpublish_on->value, $unpublish_on, 'Node is scheduled for unpublishing at the correct time.');
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
- $this->assertTrue($node->isPublished(), 'Node is published for title: "' . $node->title->value . '".');
+ $this->assertEquals($entity->unpublish_on->value, $unpublish_on, 'Entity should be scheduled for unpublishing at the correct time');
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing.');
+ $this->assertTrue($entity->isPublished(), 'Entity should be published');
- // Second, edit a pre-existing Scheduler-enabled node, without triggering
+ // Second, edit a pre-existing Scheduler-enabled entity, without triggering
// either of the rules.
- $node = $this->node_a;
- $edit = [
- 'title[0][value]' => 'Edit node - but no rules will be triggered',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ $entity = $this->createEntity($entityTypeId, $enabled, [
+ "$titleField" => "Second - existing enabled $enabled",
+ ]);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit enabled $enabled - but no rules will be triggered"], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that neither of the rules are triggered, no publish and unpublish
// dates are set and the status is still published.
$assert->pageTextNotContains($message3);
$assert->pageTextNotContains($message4);
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
- $this->assertTrue($node->isPublished(), 'Node remains published for title: "' . $node->title->value . '".');
-
- // Edit the node, triggering rule 3.
- $edit = [
- 'title[0][value]' => 'Edit node - Trigger Action Rule 3',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing');
+ $this->assertTrue($entity->isPublished(), 'Entity should remain published');
+
+ // Edit the entity, triggering rule 3.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit enabled $enabled - Trigger Rule 3"], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that rule 3 is triggered and rule 4 is not. Check that an
// unpublishing date has been set and the status is still published.
$assert->pageTextContains($message3);
$assert->pageTextNotContains($message4);
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
- $this->assertNotEmpty($node->unpublish_on->value, 'Node is scheduled for unpublishing.');
- $this->assertTrue($node->isPublished(), 'Node is still published for title: "' . $node->title->value . '".');
-
- // Edit the node, triggering rule 4.
- $edit = [
- 'title[0][value]' => 'Edit node - Trigger Action Rule 4',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing');
+ $this->assertEquals($entity->unpublish_on->value, $unpublish_on, 'Entity should be scheduled for unpublishing at the correct time');
+ $this->assertTrue($entity->isPublished(), 'Entity is still published');
+
+ // Edit the entity, triggering rule 4.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit enabled $enabled - Trigger Rule 4"], 'Save');
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
// Check that rule 4 is triggered and rule 3 is not. Check that the
// unpublishing date has been removed and the status is now unpublished.
$assert->pageTextNotContains($message3);
$assert->pageTextContains($message4);
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
- $this->assertFalse($node->isPublished(), 'Node is now unpublished for title: "' . $node->title->value . '".');
-
- // Third, create a new node which is not scheduler-enabled.
- $edit = [
- 'title[0][value]' => 'New non-enabled node - Trigger Action Rule 3',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/add/' . $this->nonSchedulerNodeType->id(), $edit, 'Save');
- $node = $this->drupalGetNodeByTitle('New non-enabled node - Trigger Action Rule 3');
+ $this->assertEmpty($entity->publish_on->value, 'Entity should not be scheduled for publishing.');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing.');
+ $this->assertFalse($entity->isPublished(), 'Entity should be unpublished.');
+
+ // Third, create a new entity which is not scheduler-enabled.
+ $title = "Third - new non-enabled $nonEnabled - Trigger Rule 3";
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $nonEnabled));
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
// Check that rule 3 issued a warning message.
$assert->pageTextContains('warning message');
$assert->elementExists('xpath', '//div[@aria-label="Warning message" and contains(string(), "Action")]');
// Check that no publishing date is set.
- $this->assertEmpty($node->publish_on->value, 'Node is not scheduled for publishing.');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing');
// Check that a log message has been recorded.
$log = \Drupal::database()->select('watchdog', 'w')
->condition('type', 'scheduler')
->condition('severity', RfcLogLevel::WARNING)
->countQuery()
->execute()
- ->fetchColumn();
+ ->fetchField();
$this->assertEquals(1, $log, 'There is 1 watchdog warning message from Scheduler');
- // Fourthly, edit a pre-existing node which is not enabled for Scheduler.
- $node = $this->node_b;
-
- // Edit the node, triggering rule 3.
- $edit = [
- 'title[0][value]' => 'Edit non-enabled node - Trigger Action Rule 3',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ // Fourthly, edit a pre-existing entity which is not enabled for Scheduler,
+ // triggering rule 3.
+ $entity = $this->createEntity($entityTypeId, $nonEnabled, [
+ "$titleField" => "Fourth - existing non-enabled $nonEnabled",
+ ]);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit non-enabled $nonEnabled - Trigger Rule 3"], 'Save');
// Check that rule 3 issued a warning message.
$assert->pageTextContains('warning message');
$assert->elementExists('xpath', '//div[@aria-label="Warning message" and contains(string(), "Action")]');
// Check that no unpublishing date is set.
- $this->assertEmpty($node->unpublish_on->value, 'Node is not scheduled for unpublishing.');
+ $this->assertEmpty($entity->unpublish_on->value, 'Entity should not be scheduled for unpublishing.');
// Check that a log message has been recorded.
$log = \Drupal::database()->select('watchdog', 'w')
->condition('type', 'scheduler')
->condition('severity', RfcLogLevel::WARNING)
->countQuery()
->execute()
- ->fetchColumn();
+ ->fetchField();
$this->assertEquals(2, $log, 'There are now 2 watchdog warning messages from Scheduler');
- // Edit the node, triggering rule 4.
- $edit = [
- 'title[0][value]' => 'Edit non-enabled node - Trigger Action Rule 4',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ // Edit the entity again, triggering rule 4.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => "Edit non-enabled $nonEnabled - Trigger Rule 4"], 'Save');
// Check that rule 4 issued a warning message.
$assert->pageTextContains('warning message');
$assert->elementExists('xpath', '//div[@aria-label="Warning message" and contains(string(), "Action")]');
@@ -436,8 +430,9 @@ class SchedulerRulesActionsTest extends SchedulerBrowserTestBase {
->condition('severity', RfcLogLevel::WARNING)
->countQuery()
->execute()
- ->fetchColumn();
+ ->fetchField();
$this->assertEquals(3, $log, 'There are now 3 watchdog warning messages from Scheduler');
+ $this->drupalGet('admin/reports/dblog');
}
}
diff --git a/tests/src/Functional/SchedulerRulesConditionsTest.php b/tests/src/Functional/SchedulerRulesConditionsTest.php
index 972d958..cbdfa3e 100644
--- a/tests/src/Functional/SchedulerRulesConditionsTest.php
+++ b/tests/src/Functional/SchedulerRulesConditionsTest.php
@@ -23,121 +23,129 @@ class SchedulerRulesConditionsTest extends SchedulerBrowserTestBase {
*/
protected function setUp(): void {
parent::setUp();
-
$this->rulesStorage = $this->container->get('entity_type.manager')->getStorage('rules_reaction_rule');
$this->expressionManager = $this->container->get('plugin.manager.rules_expression');
-
- // Create a published node.
- $this->node = $this->drupalCreateNode([
- 'title' => 'Rules Test Node',
- 'type' => $this->type,
- 'uid' => $this->schedulerUser->id(),
- 'status' => TRUE,
- ]);
}
/**
- * Tests the conditions for whether a nodetype is enabled for Scheduler.
+ * Tests the conditions for whether an entity type is enabled for Scheduler.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testNodeTypeEnabledConditions() {
- // Create a reaction rule to display a message when viewing a node of a type
- // that is enabled for scheduled publishing.
+ public function testEntityTypeEnabledConditions($entityTypeId, $bundle) {
+
+ // The legacy rules condition ids for nodes remain as:
+ // - scheduler_condition_publishing_is_enabled
+ // - scheduler_condition_unpublishing_is_enabled
+ // For all other entity types the new derived condition ids are of the form:
+ // - scheduler_publishing_is_enabled:{type}
+ // - scheduler_unpublishing_is_enabled:{type}
+ // .
+ $condition_prefix = ($entityTypeId == 'node') ? 'scheduler_condition_' : 'scheduler_';
+ $condition_suffix = ($entityTypeId == 'node') ? '' : ":$entityTypeId";
+ $entityType = $this->entityTypeObject($entityTypeId, $bundle);
+ $assert = $this->assertSession();
+
+ // Create a reaction rule to display a message when viewing an entity of a
+ // type that is enabled for scheduled publishing.
// "viewing content" actually means "viewing PUBLISHED content".
$rule1 = $this->expressionManager->createRule();
- $rule1->addCondition('scheduler_condition_publishing_is_enabled',
- ContextConfig::create()->map('node', 'node')
+ $rule1->addCondition("{$condition_prefix}publishing_is_enabled{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")
);
- $message1 = 'RULES message 1. This node type is enabled for scheduled publishing.';
+ $message1 = 'RULES message 1. This entity type is enabled for scheduled publishing.';
$rule1->addAction('rules_system_message', ContextConfig::create()
->setValue('message', $message1)
->setValue('type', 'status')
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule1',
- 'events' => [['event_name' => 'rules_entity_view:node']],
+ 'events' => [['event_name' => "rules_entity_view:$entityTypeId"]],
'expression' => $rule1->getConfiguration(),
]);
$config_entity->save();
- // Create a reaction rule to display a message when viewing a node of a type
- // that is enabled for scheduled unpublishing.
+ // Create a reaction rule to display a message when viewing an entity of a
+ // type that is enabled for scheduled unpublishing.
$rule2 = $this->expressionManager->createRule();
- $rule2->addCondition('scheduler_condition_unpublishing_is_enabled',
- ContextConfig::create()->map('node', 'node')
+ $rule2->addCondition("{$condition_prefix}unpublishing_is_enabled{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")
);
- $message2 = 'RULES message 2. This node type is enabled for scheduled unpublishing.';
+ $message2 = 'RULES message 2. This entity type is enabled for scheduled unpublishing.';
$rule2->addAction('rules_system_message', ContextConfig::create()
->setValue('message', $message2)
->setValue('type', 'status')
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule2',
- 'events' => [['event_name' => 'rules_entity_view:node']],
+ 'events' => [['event_name' => "rules_entity_view:$entityTypeId"]],
'expression' => $rule2->getConfiguration(),
]);
$config_entity->save();
- // Create a reaction rule to display a message when viewing a node of a type
- // that is NOT enabled for scheduled publishing.
+ // Create a reaction rule to display a message when viewing an entity of a
+ // type that is NOT enabled for scheduled publishing.
$rule3 = $this->expressionManager->createRule();
- $rule3->addCondition('scheduler_condition_publishing_is_enabled',
- ContextConfig::create()->map('node', 'node')->negateResult()
+ $rule3->addCondition("{$condition_prefix}publishing_is_enabled{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")->negateResult()
);
- $message3 = 'RULES message 3. This node type is not enabled for scheduled publishing.';
+ $message3 = 'RULES message 3. This entity type is not enabled for scheduled publishing.';
$rule3->addAction('rules_system_message', ContextConfig::create()
->setValue('message', $message3)
->setValue('type', 'status')
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule3',
- 'events' => [['event_name' => 'rules_entity_view:node']],
+ 'events' => [['event_name' => "rules_entity_view:$entityTypeId"]],
'expression' => $rule3->getConfiguration(),
]);
$config_entity->save();
- // Create a reaction rule to display a message when viewing a node of a type
- // that is NOT enabled for scheduled unpublishing.
+ // Create a reaction rule to display a message when viewing an entity of a
+ // type that is NOT enabled for scheduled unpublishing.
$rule4 = $this->expressionManager->createRule();
- $rule4->addCondition('scheduler_condition_unpublishing_is_enabled',
- ContextConfig::create()->map('node', 'node')->negateResult()
+ $rule4->addCondition("{$condition_prefix}unpublishing_is_enabled{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")->negateResult()
);
- $message4 = 'RULES message 4. This node type is not enabled for scheduled unpublishing.';
+ $message4 = 'RULES message 4. This entity type is not enabled for scheduled unpublishing.';
$rule4->addAction('rules_system_message', ContextConfig::create()
->setValue('message', $message4)
->setValue('type', 'status')
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule4',
- 'events' => [['event_name' => 'rules_entity_view:node']],
+ 'events' => [['event_name' => "rules_entity_view:$entityTypeId"]],
'expression' => $rule4->getConfiguration(),
]);
$config_entity->save();
- $assert = $this->assertSession();
+ // Create a published entity.
+ $entity = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "Enabled Conditions - $entityTypeId $bundle",
+ 'status' => TRUE,
+ ]);
- // View the node and check the default position - that the node type is
+ // View the entity and check the default position - that the entity type is
// enabled for both publishing and unpublishing.
- $this->drupalGet('node/' . $this->node->id());
+ $this->drupalGet($entity->toUrl());
$assert->pageTextContains($message1);
$assert->pageTextContains($message2);
$assert->pageTextNotContains($message3);
$assert->pageTextNotContains($message4);
- // Turn off scheduled publishing for the node type and check the rules.
- $this->nodetype->setThirdPartySetting('scheduler', 'publish_enable', FALSE)->save();
- // Flushing the caches was not required when using WebTestBase but is needed
- // after converting to BrowserTestBase.
+ // Turn off scheduled publishing for the entity type and check the rules.
+ $entityType->setThirdPartySetting('scheduler', 'publish_enable', FALSE)->save();
drupal_flush_all_caches();
- $this->drupalGet('node/' . $this->node->id());
+ $this->drupalGet($entity->toUrl());
$assert->pageTextNotContains($message1);
$assert->pageTextContains($message2);
$assert->pageTextContains($message3);
$assert->pageTextNotContains($message4);
- // Turn off scheduled unpublishing for the node type and the check again.
- $this->nodetype->setThirdPartySetting('scheduler', 'unpublish_enable', FALSE)->save();
+ // Turn off scheduled unpublishing for the entity type and the check again.
+ $entityType->setThirdPartySetting('scheduler', 'unpublish_enable', FALSE)->save();
drupal_flush_all_caches();
- $this->drupalGet('node/' . $this->node->id());
+ $this->drupalGet($entity->toUrl());
$assert->pageTextNotContains($message1);
$assert->pageTextNotContains($message2);
$assert->pageTextContains($message3);
@@ -146,14 +154,27 @@ class SchedulerRulesConditionsTest extends SchedulerBrowserTestBase {
}
/**
- * Tests the conditions for whether a node is scheduled.
+ * Tests the conditions for whether an entity is scheduled.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testNodeIsScheduledConditions() {
- // Create a reaction rule to display a message when a node is updated and
+ public function testEntityIsScheduledConditions($entityTypeId, $bundle) {
+ // The legacy rules condition ids for nodes remain as:
+ // - scheduler_condition_node_scheduled_for_publishing
+ // - scheduler_condition_node_scheduled_for_unpublishing
+ // For all other entity types the new derived condition ids are of the form:
+ // - scheduler_entity_is_scheduled_for_publishing:{type}
+ // - scheduler_entity_is_scheduled_for_unpublishing:{type}
+ // .
+ $condition_prefix = ($entityTypeId == 'node') ? 'scheduler_condition_node_' : 'scheduler_entity_is_';
+ $condition_suffix = ($entityTypeId == 'node') ? '' : ":$entityTypeId";
+ $assert = $this->assertSession();
+
+ // Create a reaction rule to display a message when an entity is updated and
// is not scheduled for publishing.
$rule5 = $this->expressionManager->createRule();
- $rule5->addCondition('scheduler_condition_node_scheduled_for_publishing',
- ContextConfig::create()->map('node', 'node')->negateResult()
+ $rule5->addCondition("{$condition_prefix}scheduled_for_publishing{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")->negateResult()
);
$message5 = 'RULES message 5. This content is not scheduled for publishing.';
$rule5->addAction('rules_system_message', ContextConfig::create()
@@ -162,16 +183,16 @@ class SchedulerRulesConditionsTest extends SchedulerBrowserTestBase {
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule5',
- 'events' => [['event_name' => 'rules_entity_update:node']],
+ 'events' => [['event_name' => "rules_entity_update:$entityTypeId"]],
'expression' => $rule5->getConfiguration(),
]);
$config_entity->save();
- // Create a reaction rule to display a message when a node is updated and
+ // Create a reaction rule to display a message when an entity is updated and
// is not scheduled for unpublishing.
$rule6 = $this->expressionManager->createRule();
- $rule6->addCondition('scheduler_condition_node_scheduled_for_unpublishing',
- ContextConfig::create()->map('node', 'node')->negateResult()
+ $rule6->addCondition("{$condition_prefix}scheduled_for_unpublishing{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")->negateResult()
);
$message6 = 'RULES message 6. This content is not scheduled for unpublishing.';
$rule6->addAction('rules_system_message', ContextConfig::create()
@@ -180,16 +201,16 @@ class SchedulerRulesConditionsTest extends SchedulerBrowserTestBase {
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule6',
- 'events' => [['event_name' => 'rules_entity_update:node']],
+ 'events' => [['event_name' => "rules_entity_update:$entityTypeId"]],
'expression' => $rule6->getConfiguration(),
]);
$config_entity->save();
- // Create a reaction rule to display a message when a node is updated and
+ // Create a reaction rule to display a message when an entity is updated and
// is scheduled for publishing.
$rule7 = $this->expressionManager->createRule();
- $rule7->addCondition('scheduler_condition_node_scheduled_for_publishing',
- ContextConfig::create()->map('node', 'node')
+ $rule7->addCondition("{$condition_prefix}scheduled_for_publishing{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")
);
$message7 = 'RULES message 7. This content is scheduled for publishing.';
$rule7->addAction('rules_system_message', ContextConfig::create()
@@ -198,18 +219,16 @@ class SchedulerRulesConditionsTest extends SchedulerBrowserTestBase {
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule7',
- 'events' => [['event_name' => 'rules_entity_update:node']],
+ 'events' => [['event_name' => "rules_entity_update:$entityTypeId"]],
'expression' => $rule7->getConfiguration(),
]);
$config_entity->save();
- $assert = $this->assertSession();
-
- // Create a reaction rule to display a message when a node is updated and
+ // Create a reaction rule to display a message when an entity is updated and
// is scheduled for unpublishing.
$rule8 = $this->expressionManager->createRule();
- $rule8->addCondition('scheduler_condition_node_scheduled_for_unpublishing',
- ContextConfig::create()->map('node', 'node')
+ $rule8->addCondition("{$condition_prefix}scheduled_for_unpublishing{$condition_suffix}",
+ ContextConfig::create()->map("$entityTypeId", "$entityTypeId")
);
$message8 = 'RULES message 8. This content is scheduled for unpublishing.';
$rule8->addAction('rules_system_message', ContextConfig::create()
@@ -218,43 +237,50 @@ class SchedulerRulesConditionsTest extends SchedulerBrowserTestBase {
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule8',
- 'events' => [['event_name' => 'rules_entity_update:node']],
+ 'events' => [['event_name' => "rules_entity_update:$entityTypeId"]],
'expression' => $rule8->getConfiguration(),
]);
$config_entity->save();
$this->drupalLogin($this->schedulerUser);
- // Edit the node but do not enter any scheduling dates.
- $edit = [
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $this->node->id() . '/edit', $edit, 'Save');
+ // Create a published entity.
+ $entity = $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "Scheduled Conditions - $entityTypeId $bundle",
+ 'uid' => $this->schedulerUser->id(),
+ 'status' => TRUE,
+ ]);
+ // Edit the entity but do not enter any scheduling dates, and check that
+ // only messages 5 and 6 are shown.
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm([], 'Save');
$assert->pageTextContains($message5);
$assert->pageTextContains($message6);
$assert->pageTextNotContains($message7);
$assert->pageTextNotContains($message8);
- // Edit the node and set a publish_on date.
+ // Edit the entity, set a publish_on date, and check that message 5 is now
+ // not shown and we get message 7 instead.
$edit = [
'publish_on[0][value][date]' => date('Y-m-d', strtotime('+1 day', $this->requestTime)),
'publish_on[0][value][time]' => date('H:i:s', strtotime('+1 day', $this->requestTime)),
];
- $this->drupalPostForm('node/' . $this->node->id() . '/edit', $edit, 'Save');
-
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
$assert->pageTextNotContains($message5);
$assert->pageTextContains($message6);
$assert->pageTextContains($message7);
$assert->pageTextNotContains($message8);
- // Edit the node and set an unpublish_on date.
+ // Edit the entity again, set an unpublish_on date, and check that message 6
+ // is now not shown and we get message 8 instead.
$edit = [
'unpublish_on[0][value][date]' => date('Y-m-d', strtotime('+2 day', $this->requestTime)),
'unpublish_on[0][value][time]' => date('H:i:s', strtotime('+2 day', $this->requestTime)),
];
- $this->drupalPostForm('node/' . $this->node->id() . '/edit', $edit, 'Save');
-
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
$assert->pageTextNotContains($message5);
$assert->pageTextNotContains($message6);
$assert->pageTextContains($message7);
diff --git a/tests/src/Functional/SchedulerRulesEventsTest.php b/tests/src/Functional/SchedulerRulesEventsTest.php
index 803a16c..3e91a44 100644
--- a/tests/src/Functional/SchedulerRulesEventsTest.php
+++ b/tests/src/Functional/SchedulerRulesEventsTest.php
@@ -29,226 +29,221 @@ class SchedulerRulesEventsTest extends SchedulerBrowserTestBase {
$this->rulesStorage = $this->container->get('entity_type.manager')->getStorage('rules_reaction_rule');
$this->expressionManager = $this->container->get('plugin.manager.rules_expression');
- // Create six reaction rules, one for each event that Scheduler triggers.
+ // Create a reaction rule to display a system message for each of the six
+ // events that Scheduler triggers, for each entity type. The array of data
+ // contains the event name and the text to display.
// These rules are all active throughout all of the tests, which makes the
// tests stronger, because it will show not only that the correct events are
// triggered in the right places, but also that they are not triggered in
// the wrong places.
$rule_data = [
+ // The first six events are the originals, only dispatched for Nodes.
1 => ['scheduler_new_node_is_scheduled_for_publishing_event', 'A new node is created and is scheduled for publishing.'],
2 => ['scheduler_existing_node_is_scheduled_for_publishing_event', 'An existing node is saved and is scheduled for publishing.'],
3 => ['scheduler_has_published_this_node_event', 'Scheduler has published this node during cron.'],
4 => ['scheduler_new_node_is_scheduled_for_unpublishing_event', 'A new node is created and is scheduled for unpublishing.'],
5 => ['scheduler_existing_node_is_scheduled_for_unpublishing_event', 'An existing node is saved and is scheduled for unpublishing.'],
6 => ['scheduler_has_unpublished_this_node_event', 'Scheduler has unpublished this node during cron.'],
+ // These six events are dispatched only for Media entities.
+ 7 => ['scheduler:new_media_is_scheduled_for_publishing', 'A new media item is created and scheduled for publishing.'],
+ 8 => ['scheduler:existing_media_is_scheduled_for_publishing', 'An existing media item is saved and scheduled for publishing.'],
+ 9 => ['scheduler:media_has_been_published_via_cron', 'Scheduler has published this media item during cron.'],
+ 10 => ['scheduler:new_media_is_scheduled_for_unpublishing', 'A new media item is created and scheduled for unpublishing.'],
+ 11 => ['scheduler:existing_media_is_scheduled_for_unpublishing', 'An existing media item is saved and scheduled for unpublishing.'],
+ 12 => ['scheduler:media_has_been_unpublished_via_cron', 'Scheduler has unpublished this media item during cron.'],
+ // These six events are dispatched only for Commerce Product entities.
+ 13 => ['scheduler:new_commerce_product_is_scheduled_for_publishing', 'A new product is created and scheduled for publishing.'],
+ 14 => ['scheduler:existing_commerce_product_is_scheduled_for_publishing', 'An existing product is scheduled for publishing.'],
+ 15 => ['scheduler:commerce_product_has_been_published_via_cron', 'Scheduler has published this product during cron.'],
+ 16 => ['scheduler:new_commerce_product_is_scheduled_for_unpublishing', 'A new product is created and scheduled for unpublishing.'],
+ 17 => ['scheduler:existing_commerce_product_is_scheduled_for_unpublishing', 'An existing product is scheduled for unpublishing.'],
+ 18 => ['scheduler:commerce_product_has_been_unpublished_via_cron', 'Scheduler has unpublished this product during cron.'],
];
+
// PHPCS throws a false-positive 'variable $var is undefined' message when
// the variable is defined by list( ) syntax. To avoid the unwanted warnings
- // we can wrap the section with codingStandardsIgnoreStart and IgnoreEnd.
+ // we can put phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ // before each line that produces a warning of this type.
+ // This has been fixed in Coder 8.3.10 which is used in Core 9.1.
// @see https://www.drupal.org/project/coder/issues/2876245
- // @codingStandardsIgnoreStart
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
foreach ($rule_data as $i => list($event_name, $description)) {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
$rule[$i] = $this->expressionManager->createRule();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
$this->message[$i] = 'RULES message ' . $i . '. ' . $description;
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
$rule[$i]->addAction('rules_system_message', ContextConfig::create()
->setValue('message', $this->message[$i])
->setValue('type', 'status')
);
$config_entity = $this->rulesStorage->create([
'id' => 'rule' . $i,
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
'events' => [['event_name' => $event_name]],
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
'expression' => $rule[$i]->getConfiguration(),
]);
$config_entity->save();
}
- // @codingStandardsIgnoreEnd
$this->drupalLogin($this->schedulerUser);
}
/**
- * Tests that no events are triggered when there are no scheduling dates.
+ * Check the presence or absence of expected message texts on the page.
+ *
+ * @param string $entityTypeId
+ * The entity type being tested.
+ * @param array $expectedMessages
+ * The ids of the messages that should be showing on the current page. All
+ * other messsages should not be displayed.
*/
- public function testRulesEventsNone() {
- $assert = $this->assertSession();
+ public function checkMessages(string $entityTypeId = NULL, array $expectedMessages = []) {
+ // Add the required entity offset to each message id in the expected array.
+ $offset = ['node' => 0, 'media' => 6, 'commerce_product' => 12];
+ array_walk($expectedMessages, function (&$item) use ($offset, $entityTypeId) {
+ $item = $item + $offset[$entityTypeId];
+ });
+
+ // Check that all the expected messages are shown.
+ foreach ($expectedMessages as $i) {
+ $this->assertSession()->pageTextContains($this->message[$i]);
+ }
- // Create a node without any scheduled dates, using node/add/ not
- // drupalCreateNode(), and check that no events are triggered.
- $edit = [
- 'title[0][value]' => 'Test for no events',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Edit the node and check that no events are triggered.
- $edit = [
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
+ // Check that none of the other messages are shown.
+ $notExpecting = array_diff(array_keys($this->message), $expectedMessages);
+ foreach ($notExpecting as $i) {
+ $this->assertSession()->pageTextNotContains($this->message[$i]);
+ }
+ }
+
+ /**
+ * Tests that no events are triggered when there are no scheduling dates.
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testRulesEventsNone($entityTypeId, $bundle) {
+ // Add and save an entity without any scheduled dates and check that no
+ // events are triggered.
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $title = 'A. Create with no dates';
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->submitForm(["{$titleField}[0][value]" => $title], 'Save');
+ $this->checkMessages();
+
+ // Edit the entity and check that no events are triggered.
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => 'B. Edit with no dates'], 'Save');
+ $this->checkMessages();
}
/**
- * Tests the three events related to publishing a node.
+ * Tests the three events related to publishing an entity.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testRulesEventsPublish() {
- $assert = $this->assertSession();
+ public function testRulesEventsPublish($entityTypeId, $bundle) {
+ // Allow dates in the past.
+ $this->entityTypeObject($entityTypeId, $bundle)
+ ->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
- // Create a node with a publish-on date, and check that only event 1 is
+ // Create an entity with a publish-on date, and check that only event 1 is
// triggered.
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $title = 'C. Create with publish-on date';
$edit = [
- 'title[0][value]' => 'Create node with publish-on date',
- 'publish_on[0][value][date]' => date('Y-m-d', time() + 3),
- 'publish_on[0][value][time]' => date('H:i:s', time() + 3),
- 'body[0][value]' => $this->randomString(30),
+ "{$titleField}[0][value]" => $title,
+ 'publish_on[0][value][date]' => date('Y-m-d', time() - 60),
+ 'publish_on[0][value][time]' => date('H:i:s', time() - 60),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
- $assert->pageTextContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Edit this node and check that only event 2 is triggered.
- $edit = [
- 'title[0][value]' => 'Edit node with publish-on date',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Delay to ensure that the date entered is now in the past so that the node
- // will be processed during cron, and assert that only event 3 is triggered.
- sleep(5);
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->submitForm($edit, 'Save');
+ $this->checkMessages($entityTypeId, [1]);
+
+ // Edit the entity and check that only event 2 is triggered.
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => 'D. Edit with publish-on date'], 'Save');
+ $this->checkMessages($entityTypeId, [2]);
+
+ // Run cron and check that only event 3 is triggered.
$this->cronRun();
- $this->drupalGet('admin/reports/dblog');
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
+ $this->drupalGet($entity->toUrl());
+ $this->checkMessages($entityTypeId, [3]);
}
/**
- * Tests the three events related to unpublishing a node.
+ * Tests the three events related to unpublishing an entity.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testRulesEventsUnpublish() {
- $assert = $this->assertSession();
-
- // Create a node with an unpublish-on date, and check that only event 4 is
- // triggered.
+ public function testRulesEventsUnpublish($entityTypeId, $bundle) {
+ // Create an entity with an unpublish-on date, and check that only event 4
+ // is triggered.
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $title = 'E. Create with unpublish-on date';
$edit = [
- 'title[0][value]' => 'Create node with unpublish-on date',
- 'unpublish_on[0][value][date]' => date('Y-m-d', time() + 3),
- 'unpublish_on[0][value][time]' => date('H:i:s', time() + 3),
- 'body[0][value]' => $this->randomString(30),
+ "{$titleField}[0][value]" => $title,
+ 'unpublish_on[0][value][date]' => date('Y-m-d', time() + 5),
+ 'unpublish_on[0][value][time]' => date('H:i:s', time() + 5),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Edit this node and check that only event 5 is triggered.
- $edit = [
- 'title[0][value]' => 'Edit node with unpublish-on date',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Delay to ensure that the date entered is now in the past so that the node
- // will be processed during cron, and assert that only event 6 is triggered.
- sleep(5);
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->submitForm($edit, 'Save');
+ $this->checkMessages($entityTypeId, [4]);
+
+ // Edit the entity and check that only event 5 is triggered.
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => 'F. Edit with unpublish-on date'], 'Save');
+ $this->checkMessages($entityTypeId, [5]);
+
+ // Delay to ensure that the dates are in the past so that the entity will be
+ // processed during cron, and check that only event 6 is triggered.
+ sleep(6);
$this->cronRun();
- $this->drupalGet('admin/reports/dblog');
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextContains($this->message[6]);
+ $this->drupalGet($entity->toUrl());
+ $this->checkMessages($entityTypeId, [6]);
}
/**
- * Tests all six events related to publishing and unpublishing a node.
+ * Tests all six events related to publishing and unpublishing an entity.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testRulesEventsBoth() {
- $assert = $this->assertSession();
-
- // Create a node with both publish-on and unpublish-on dates, and check that
- // both event 1 and event 4 are triggered.
+ public function testRulesEventsBoth($entityTypeId, $bundle) {
+ // Allow dates in the past.
+ $this->entityTypeObject($entityTypeId, $bundle)
+ ->setThirdPartySetting('scheduler', 'publish_past_date', 'schedule')->save();
+
+ // Create an entity with both publish-on and unpublish-on dates, and check
+ // that both event 1 and event 4 are triggered.
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $title = 'G. Create with both dates';
$edit = [
- 'title[0][value]' => 'Create node with both dates',
- 'publish_on[0][value][date]' => date('Y-m-d', time() + 3),
- 'publish_on[0][value][time]' => date('H:i:s', time() + 3),
- 'unpublish_on[0][value][date]' => date('Y-m-d', time() + 4),
- 'unpublish_on[0][value][time]' => date('H:i:s', time() + 4),
- 'body[0][value]' => $this->randomString(30),
+ "{$titleField}[0][value]" => $title,
+ 'publish_on[0][value][date]' => date('Y-m-d', time() - 60),
+ 'publish_on[0][value][time]' => date('H:i:s', time() - 60),
+ 'unpublish_on[0][value][date]' => date('Y-m-d', time() + 5),
+ 'unpublish_on[0][value][time]' => date('H:i:s', time() + 5),
];
- $this->drupalPostForm('node/add/' . $this->type, $edit, 'Save');
- $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
- $assert->pageTextContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextContains($this->message[4]);
- $assert->pageTextNotContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Edit this node and check that events 2 and 5 are triggered.
- $edit = [
- 'title[0][value]' => 'Edit node with both dates',
- 'body[0][value]' => $this->randomString(30),
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextContains($this->message[2]);
- $assert->pageTextNotContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextContains($this->message[5]);
- $assert->pageTextNotContains($this->message[6]);
-
- // Delay to ensure that the dates are now in the past so that the node will
- // be processed during cron, and assert that events 3, 5 & 6 are triggered.
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
+ $this->submitForm($edit, 'Save');
+ $this->checkMessages($entityTypeId, [1, 4]);
+
+ // Edit the entity and check that only events 2 and 5 are triggered.
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm(["{$titleField}[0][value]" => 'H. Edit with both dates'], 'Save');
+ $this->checkMessages($entityTypeId, [2, 5]);
+
+ // Delay to ensure that the dates are in the past so that the entity will be
+ // processed during cron, and assert that events 3, 5 and 6 are triggered.
sleep(6);
$this->cronRun();
- $this->drupalGet('admin/reports/dblog');
- $assert->pageTextNotContains($this->message[1]);
- $assert->pageTextNotContains($this->message[2]);
- $assert->pageTextContains($this->message[3]);
- $assert->pageTextNotContains($this->message[4]);
- $assert->pageTextContains($this->message[5]);
- $assert->pageTextContains($this->message[6]);
-
+ $this->drupalGet($entity->toUrl());
+ $this->checkMessages($entityTypeId, [3, 5, 6]);
}
}
diff --git a/tests/src/Functional/SchedulerScheduledContentListAccessTest.php b/tests/src/Functional/SchedulerScheduledContentListAccessTest.php
deleted file mode 100644
index 33fd887..0000000
--- a/tests/src/Functional/SchedulerScheduledContentListAccessTest.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-
-namespace Drupal\Tests\scheduler\Functional;
-
-/**
- * Tests access to the scheduled content overview page and user tab.
- *
- * @group scheduler
- */
-class SchedulerScheduledContentListAccessTest extends SchedulerBrowserTestBase {
-
- /**
- * Additional modules required.
- *
- * @var array
- */
- protected static $modules = ['views'];
-
- /**
- * {@inheritdoc}
- */
- protected function setUp(): void {
- parent::setUp();
-
- $base_permissions = [
- 'access content',
- 'create ' . $this->type . ' content',
- 'view own unpublished content',
- ];
-
- $this->editorUser = $this->drupalCreateUser(array_merge($base_permissions, ['access content overview']));
- $this->schedulerUser = $this->drupalCreateUser(array_merge($base_permissions, ['schedule publishing of nodes']));
- $this->schedulerManager = $this->drupalCreateUser(array_merge($base_permissions, ['view scheduled content']));
-
- // Create nodes scheduled for publishing and for unpublishing.
- $this->node1 = $this->drupalCreateNode([
- 'title' => 'Node created by Scheduler User for publishing',
- 'uid' => $this->schedulerUser->id(),
- 'status' => FALSE,
- 'type' => $this->type,
- 'publish_on' => strtotime('+1 week'),
- ]);
- $this->node2 = $this->drupalCreateNode([
- 'title' => 'Node created by Scheduler User for unpublishing',
- 'uid' => $this->schedulerUser->id(),
- 'status' => TRUE,
- 'type' => $this->type,
- 'unpublish_on' => strtotime('+1 week'),
- ]);
- $this->node3 = $this->drupalCreateNode([
- 'title' => 'Node created by Scheduler Manager for publishing',
- 'uid' => $this->schedulerManager->id(),
- 'status' => FALSE,
- 'type' => $this->type,
- 'publish_on' => strtotime('+1 week'),
- ]);
- $this->node4 = $this->drupalCreateNode([
- 'title' => 'Node created by Scheduler Manager for unpublishing',
- 'uid' => $this->schedulerManager->id(),
- 'status' => TRUE,
- 'type' => $this->type,
- 'unpublish_on' => strtotime('+1 week'),
- ]);
- }
-
- /**
- * Tests the scheduled content tab on the user page.
- */
- public function testViewScheduledContentUser() {
- $assert = $this->assertSession();
-
- // Access a scheduled content user tab as an anonymous visitor.
- $this->drupalGet("user/{$this->schedulerUser->id()}/scheduled");
- // An anonymous visitor cannot access a user's scheduled content tab.
- $assert->statusCodeEquals(403);
-
- // Try to access a users own scheduled content tab when they do not have
- // any scheduler permissions. This should give "403 Access Denied".
- $this->drupalLogin($this->editorUser);
- $this->drupalGet("user/{$this->editorUser->id()}/scheduled");
- $assert->statusCodeEquals(403);
-
- // Access a users own scheduled content tab when they have only
- // 'schedule publishing of nodes' permission. This will give "200 OK".
- $this->drupalLogin($this->schedulerUser);
- $this->drupalGet("user/{$this->schedulerUser->id()}/scheduled");
- $assert->statusCodeEquals(200);
- $assert->pageTextContains('Node created by Scheduler User for publishing');
- $assert->pageTextContains('Node created by Scheduler User for unpublishing');
- $assert->pageTextNotContains('Node created by Scheduler Manager for unpublishing');
-
- // Access another users scheduled content tab as "Scheduler User". This
- // should not be possible and will give "403 Access Denied".
- $this->drupalGet("user/{$this->schedulerManager->id()}/scheduled");
- $assert->statusCodeEquals(403);
-
- // Access the users own scheduled content tab as "Scheduler Manager" with
- // only 'view scheduled content' permission.
- $this->drupalLogin($this->schedulerManager);
- $this->drupalGet("user/{$this->schedulerManager->id()}/scheduled");
- $assert->statusCodeEquals(200);
- $assert->pageTextContains('Node created by Scheduler Manager for publishing');
- $assert->pageTextContains('Node created by Scheduler Manager for unpublishing');
- $assert->pageTextNotContains('Node created by Scheduler User for unpublishing');
-
- // Access another users scheduled content tab as "Scheduler Manager".
- // The published and unpublished content should be listed.
- $this->drupalGet("user/{$this->schedulerUser->id()}/scheduled");
- $assert->statusCodeEquals(200);
- $assert->pageTextContains('Node created by Scheduler User for publishing');
- $assert->pageTextContains('Node created by Scheduler User for unpublishing');
- }
-
- /**
- * Tests the scheduled content overview.
- */
- public function testViewScheduledContentOverview() {
- $assert = $this->assertSession();
-
- // Access the scheduled content overview as anonymous visitor.
- $this->drupalGet('admin/content/scheduled');
- $assert->statusCodeEquals(403);
-
- // Access the scheduled content overview as "Editor" without any
- // scheduler permissions.
- $this->drupalLogin($this->editorUser);
- $this->drupalGet('admin/content/scheduled');
- $assert->statusCodeEquals(403);
-
- // Access the scheduled content overview as "Scheduler User" with only
- // 'schedule publishing of nodes' permission.
- $this->drupalLogin($this->schedulerUser);
- $this->drupalGet('admin/content/scheduled');
- $assert->statusCodeEquals(403);
-
- // Access the scheduled content overview as "Scheduler Manager" with only
- // 'view scheduled content' permission. They should be able to see the
- // scheduled published and unpublished content by all users.
- $this->drupalLogin($this->schedulerManager);
- $this->drupalGet('admin/content/scheduled');
- $assert->statusCodeEquals(200);
- $assert->pageTextContains('Node created by Scheduler User for publishing');
- $assert->pageTextContains('Node created by Scheduler User for unpublishing');
- $assert->pageTextContains('Node created by Scheduler Manager for publishing');
- $assert->pageTextContains('Node created by Scheduler Manager for unpublishing');
- }
-
-}
diff --git a/tests/src/Functional/SchedulerTokenReplaceTest.php b/tests/src/Functional/SchedulerTokenReplaceTest.php
index 5aeebb0..a26b70e 100644
--- a/tests/src/Functional/SchedulerTokenReplaceTest.php
+++ b/tests/src/Functional/SchedulerTokenReplaceTest.php
@@ -11,25 +11,27 @@ class SchedulerTokenReplaceTest extends SchedulerBrowserTestBase {
/**
* Creates a node, then tests the tokens generated from it.
+ *
+ * @dataProvider dataSchedulerTokenReplacement()
*/
- public function testSchedulerTokenReplacement() {
+ public function testSchedulerTokenReplacement($entityTypeId, $bundle) {
$this->drupalLogin($this->schedulerUser);
// Define timestamps for consistent use when repeated throughout this test.
$publish_on_timestamp = $this->requestTime + 3600;
$unpublish_on_timestamp = $this->requestTime + 7200;
- // Create an unpublished page with scheduled dates.
- $node = $this->drupalCreateNode([
- 'type' => $this->type,
+ // Create an unpublished entity with scheduled dates.
+ $entity = $this->createEntity($entityTypeId, $bundle, [
'status' => FALSE,
'publish_on' => $publish_on_timestamp,
'unpublish_on' => $unpublish_on_timestamp,
]);
- // Show that the node is scheduled.
- $this->drupalGet('admin/content/scheduled');
+ // Check that the entity is scheduled.
+ $this->assertFalse($entity->isPublished(), 'The entity is not published');
+ $this->assertNotEmpty($entity->publish_on->value, 'The entity has a publish_on date');
+ $this->assertNotEmpty($entity->unpublish_on->value, 'The entity has an unpublish_on date');
// Create array of test case data.
- // @todo Convert this test to use @dataProvider instead of array and loop?
$test_cases = [
['token_format' => '', 'date_format' => 'medium', 'custom' => ''],
['token_format' => ':long', 'date_format' => 'long', 'custom' => ''],
@@ -41,18 +43,21 @@ class SchedulerTokenReplaceTest extends SchedulerBrowserTestBase {
],
];
+ $storage = $this->entityStorageObject($entityTypeId);
foreach ($test_cases as $test_data) {
- // Edit the node and set the body tokens to use the format being tested.
+ // Edit the entity and set the body tokens to use the format being tested.
$edit = [
- 'body[0][value]' => 'Publish on: [node:scheduler-publish' . $test_data['token_format'] . ']. Unpublish on: [node:scheduler-unpublish' . $test_data['token_format'] . '].',
+ 'body[0][value]' => "Publish on: [{$entityTypeId}:scheduler-publish{$test_data['token_format']}]. Unpublish on: [{$entityTypeId}:scheduler-unpublish{$test_data['token_format']}].",
];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- $this->drupalGet('node/' . $node->id());
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
+ // View the entity.
+ $this->drupalGet($entity->toUrl());
- // Refresh the node and get the body output value.
- $this->nodeStorage->resetCache([$node->id()]);
- $node = $this->nodeStorage->load($node->id());
- $body_output = \Drupal::token()->replace($node->body->value, ['node' => $node]);
+ // Refresh the entity and get the body output value using token replace.
+ $storage->resetCache([$entity->id()]);
+ $entity = $storage->load($entity->id());
+ $body_output = \Drupal::token()->replace($entity->body->value, ["$entityTypeId" => $entity]);
// Create the expected text for the body.
$publish_on_date = $this->dateFormatter->format($publish_on_timestamp, $test_data['date_format'], $test_data['custom']);
@@ -63,4 +68,18 @@ class SchedulerTokenReplaceTest extends SchedulerBrowserTestBase {
}
}
+ /**
+ * Provides test data for TokenReplacement test.
+ *
+ * This test is not run for Media entities because there is no body field.
+ *
+ * @return array
+ * Each array item has the values: [entity type id, bundle id].
+ */
+ public function dataSchedulerTokenReplacement() {
+ $data = $this->dataStandardEntityTypes();
+ unset($data['#media']);
+ return $data;
+ }
+
}
diff --git a/tests/src/Functional/SchedulerValidationTest.php b/tests/src/Functional/SchedulerValidationTest.php
index 370f325..fef5103 100644
--- a/tests/src/Functional/SchedulerValidationTest.php
+++ b/tests/src/Functional/SchedulerValidationTest.php
@@ -13,62 +13,62 @@ class SchedulerValidationTest extends SchedulerBrowserTestBase {
* Tests the validation when editing a node.
*
* The 'required' checks and 'dates in the past' checks are handled in other
- * tests. This test checks validation when fields interact.
+ * tests. This test checks validation when the two fields interact, and covers
+ * the error message text stored in the following constraint variables:
+ * $messageUnpublishOnRequiredIfPublishOnEntered
+ * $messageUnpublishOnRequiredIfPublishing
+ * $messageUnpublishOnTooEarly.
+ *
+ * @dataProvider dataStandardEntityTypes()
*/
- public function testValidationDuringEdit() {
+ public function testValidationDuringEdit($entityTypeId, $bundle) {
$this->drupalLogin($this->adminUser);
- // Set unpublishing to be required.
- $this->nodetype->setThirdPartySetting('scheduler', 'unpublish_required', TRUE)->save();
+ // Set unpublishing to be required for this entity type.
+ $this->entityTypeObject($entityTypeId)->setThirdPartySetting('scheduler', 'unpublish_required', TRUE)->save();
+
+ // Create an unpublished entity.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => FALSE]);
- // Create an unpublished page node, then edit the node and check that if a
- // publish-on date is entered then an unpublish-on date is also needed.
- $node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- ]);
+ // Edit the unpublished entity and try to save a publish-on date.
$edit = [
'publish_on[0][value][date]' => date('Y-m-d', strtotime('+1 day', $this->requestTime)),
'publish_on[0][value][time]' => date('H:i:s', strtotime('+1 day', $this->requestTime)),
];
- $this->drupalGet('node/' . $node->id() . '/edit');
-
+ $this->drupalGet($entity->toUrl('edit-form'));
$this->submitForm($edit, 'Save');
// Check that validation prevents entering a publish-on date with no
// unpublish-on date if unpublishing is required.
$this->assertSession()->pageTextContains("If you set a 'publish on' date then you must also set an 'unpublish on' date.");
- $this->assertSession()->pageTextNotContains(sprintf('%s %s has been updated.', $this->typeName, $node->title->value));
+ $this->assertSession()->pageTextNotMatches('/has been (updated|successfully saved)/');
- // Create an unpublished page node, then edit the node and check that if the
- // status is changed to published, then an unpublish-on date is also needed.
- $node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- ]);
+ // Create an unpublished entity.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => FALSE]);
+
+ // Edit the unpublished entity and try to change the status to 'published'.
$edit = ['status[value]' => TRUE];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
- // Check that validation prevents publishing the node directly without an
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
+ // Check that validation prevents publishing the entity directly without an
// unpublish-on date if unpublishing is required.
$this->assertSession()->pageTextContains("Either you must set an 'unpublish on' date or save this node as unpublished.");
- $this->assertSession()->pageTextNotContains(sprintf('%s %s has been updated.', $this->typeName, $node->title->value));
+ $this->assertSession()->pageTextNotMatches('/has been (updated|successfully saved)/');
- // Create an unpublished node, edit the node and check that if both dates
- // are entered then the unpublish date is later than the publish date.
- $node = $this->drupalCreateNode([
- 'type' => $this->type,
- 'status' => FALSE,
- ]);
+ // Create an unpublished entity, and try to edit and save with a publish-on
+ // date later than the unpublish-on date.
+ $entity = $this->createEntity($entityTypeId, $bundle, ['status' => FALSE]);
$edit = [
- 'publish_on[0][value][date]' => $this->dateFormatter->format($this->requestTime + 8100, 'custom', 'Y-m-d'),
- 'publish_on[0][value][time]' => $this->dateFormatter->format($this->requestTime + 8100, 'custom', 'H:i:s'),
+ 'publish_on[0][value][date]' => $this->dateFormatter->format($this->requestTime + 7200, 'custom', 'Y-m-d'),
+ 'publish_on[0][value][time]' => $this->dateFormatter->format($this->requestTime + 7200, 'custom', 'H:i:s'),
'unpublish_on[0][value][date]' => $this->dateFormatter->format($this->requestTime + 1800, 'custom', 'Y-m-d'),
'unpublish_on[0][value][time]' => $this->dateFormatter->format($this->requestTime + 1800, 'custom', 'H:i:s'),
];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ $this->drupalGet($entity->toUrl('edit-form'));
+ $this->submitForm($edit, 'Save');
// Check that validation prevents entering an unpublish-on date which is
// earlier than the publish-on date.
$this->assertSession()->pageTextContains("The 'unpublish on' date must be later than the 'publish on' date.");
- $this->assertSession()->pageTextNotContains(sprintf('%s %s has been updated.', $this->typeName, $node->title->value));
+ $this->assertSession()->pageTextNotMatches('/has been (updated|successfully saved)/');
}
}
diff --git a/tests/src/Functional/SchedulerViewsAccessTest.php b/tests/src/Functional/SchedulerViewsAccessTest.php
new file mode 100644
index 0000000..24ec938
--- /dev/null
+++ b/tests/src/Functional/SchedulerViewsAccessTest.php
@@ -0,0 +1,201 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Functional;
+
+/**
+ * Tests access to the scheduled content overview page and user tab.
+ *
+ * @group scheduler
+ */
+class SchedulerViewsAccessTest extends SchedulerBrowserTestBase {
+
+ /**
+ * Additional modules required.
+ *
+ * @var array
+ */
+ protected static $modules = ['views'];
+
+ /**
+ * Create users and scheduled content for the entity type being tested.
+ */
+ protected function createScheduledItems($entityTypeId, $bundle) {
+ // For backwards-compatibility the node permission names have to end with
+ // 'nodes' and 'content'. For all other entity types we use $entityTypeId.
+ if ($entityTypeId == 'node') {
+ $edit_key = 'nodes';
+ $view_key = 'content';
+ }
+ else {
+ $edit_key = $view_key = $entityTypeId;
+ }
+ $base_permissions = [
+ "view own unpublished $view_key",
+ ];
+
+ $this->webUser = $this->drupalCreateUser(array_merge($base_permissions, ["access $view_key overview"]));
+ $this->webUser->set('name', 'Webisa the Web User')->save();
+
+ $this->schedulerEditor = $this->drupalCreateUser(array_merge($base_permissions, ["schedule publishing of $edit_key"]));
+ $this->schedulerEditor->set('name', 'Eddie the Scheduler Editor')->save();
+
+ $this->schedulerViewer = $this->drupalCreateUser(array_merge($base_permissions, ["view scheduled $view_key"]));
+ $this->schedulerViewer->set('name', 'Vicenza the Scheduler Viewer')->save();
+
+ $this->addPermissionsToUser($this->adminUser, ['access user profiles']);
+
+ // Create content scheduled for publishing and for unpublishing. The first
+ // two are authored by schedulerEditor, the second two by schedulerViewer.
+ $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "$entityTypeId created by Scheduler Editor for publishing",
+ 'uid' => $this->schedulerEditor->id(),
+ 'status' => FALSE,
+ 'publish_on' => strtotime('+1 week'),
+ ]);
+ $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "$entityTypeId created by Scheduler Editor for unpublishing",
+ 'uid' => $this->schedulerEditor->id(),
+ 'status' => TRUE,
+ 'unpublish_on' => strtotime('+1 week'),
+ ]);
+ $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "$entityTypeId created by Scheduler Viewer for publishing",
+ 'uid' => $this->schedulerViewer->id(),
+ 'status' => FALSE,
+ 'publish_on' => strtotime('+1 week'),
+ ]);
+ $this->createEntity($entityTypeId, $bundle, [
+ 'title' => "$entityTypeId created by Scheduler Viewer for unpublishing",
+ 'uid' => $this->schedulerViewer->id(),
+ 'status' => TRUE,
+ 'unpublish_on' => strtotime('+1 week'),
+ ]);
+ }
+
+ /**
+ * Tests the scheduled content tab on the user page.
+ *
+ * @dataProvider dataViewScheduledContentUser()
+ */
+ public function testViewScheduledContentUser($entityTypeId, $bundle) {
+ $this->createScheduledItems($entityTypeId, $bundle);
+ $url_end = ($entityTypeId == 'node') ? 'scheduled' : "scheduled_{$entityTypeId}";
+ $assert = $this->assertSession();
+
+ // Try to access a scheduled content user tab as an anonymous visitor. This
+ // should not be allowed, and will give "403 Access Denied".
+ $this->drupalGet("user/{$this->schedulerEditor->id()}/$url_end");
+ $assert->statusCodeEquals(403);
+
+ // Try to access a user's own scheduled content tab when they do not have
+ // any scheduler permissions. This should give "403 Access Denied".
+ $this->drupalLogin($this->webUser);
+ $this->drupalGet("user/{$this->webUser->id()}/$url_end");
+ $assert->statusCodeEquals(403);
+
+ // Access a user's own scheduled content tab when they have only
+ // 'schedule publishing of {type}' permission. This should give "200 OK".
+ $this->drupalLogin($this->schedulerEditor);
+ $this->drupalGet("user/{$this->schedulerEditor->id()}/$url_end");
+ $assert->statusCodeEquals(200);
+ $assert->pageTextContains("$entityTypeId created by Scheduler Editor for publishing");
+ $assert->pageTextContains("$entityTypeId created by Scheduler Editor for unpublishing");
+ $assert->pageTextNotContains("$entityTypeId created by Scheduler Viewer for publishing");
+ $assert->pageTextNotContains("$entityTypeId created by Scheduler Viewer for unpublishing");
+
+ // Access another user's scheduled content tab. This should not be possible
+ // and will give "403 Access Denied".
+ $this->drupalGet("user/{$this->schedulerViewer->id()}/$url_end");
+ $assert->statusCodeEquals(403);
+
+ // Try to access a user's own scheduled content tab when that user only has
+ // 'view scheduled {type}' and not 'schedule publishing of {type}'. This is
+ // not allowed and the tab will not be availbale as that view will always be
+ // empty because the user will never have any scheduled content.
+ $this->drupalLogin($this->schedulerViewer);
+ $this->drupalGet("user/{$this->schedulerViewer->id()}/$url_end");
+ $assert->statusCodeEquals(403);
+
+ // Access another user's scheduled content tab. This should not be possible
+ // and will give "403 Access Denied".
+ $this->drupalGet("user/{$this->schedulerEditor->id()}/$url_end");
+ $assert->statusCodeEquals(403);
+
+ // Log in as Admin who has 'access user profiles' permission and access the
+ // user who can schedule content. This is allowed and the content just for
+ // that user should be listed.
+ $this->drupalLogin($this->adminUser);
+ $this->drupalGet("user/{$this->schedulerEditor->id()}/$url_end");
+ $assert->statusCodeEquals(200);
+ $assert->pageTextContains("$entityTypeId created by Scheduler Editor for publishing");
+ $assert->pageTextContains("$entityTypeId created by Scheduler Editor for unpublishing");
+ $assert->pageTextNotContains("$entityTypeId created by Scheduler Viewer for publishing");
+ $assert->pageTextNotContains("$entityTypeId created by Scheduler Viewer for unpublishing");
+
+ // Try to access the scheduled tab for a user who cannot schedule content.
+ // No tab will be shown and access is denied as it will always be empty.
+ $this->drupalGet("user/{$this->schedulerViewer->id()}/$url_end");
+ $assert->statusCodeEquals(403);
+ }
+
+ /**
+ * Provides test data for user view test.
+ *
+ * There is no user view for scheduled Commerce Products so this entity type
+ * is removed.
+ *
+ * @return array
+ * Each array item has the values: [entity type id, bundle id].
+ */
+ public function dataViewScheduledContentUser() {
+ $data = $this->dataStandardEntityTypes();
+ unset($data['#commerce_product']);
+ return $data;
+ }
+
+ /**
+ * Tests the scheduled content overview.
+ *
+ * @dataProvider dataStandardEntityTypes()
+ */
+ public function testViewScheduledContentOverview($entityTypeId, $bundle) {
+ $this->createScheduledItems($entityTypeId, $bundle);
+ $scheduled_urls = [
+ 'node' => 'admin/content/scheduled',
+ 'media' => 'admin/content/media/scheduled',
+ 'commerce_product' => 'admin/commerce/products/scheduled',
+ ];
+ $scheduled_url = $scheduled_urls[$entityTypeId];
+ $assert = $this->assertSession();
+
+ // Try to access the scheduled content overview as an anonymous visitor.
+ $this->drupalGet($scheduled_url);
+ $assert->statusCodeEquals(403);
+
+ // Try to access the scheduled content overview as a user who has no
+ // scheduler permissions. This should not be possible.
+ $this->drupalLogin($this->webUser);
+ $this->drupalGet($scheduled_url);
+ $assert->statusCodeEquals(403);
+
+ // Try to access the scheduled content overview as a user with only
+ // 'schedule publishing of {type}' permission. This should not be possible.
+ $this->drupalLogin($this->schedulerEditor);
+ $this->drupalGet($scheduled_url);
+ $assert->statusCodeEquals(403);
+
+ // Access the scheduled content overview as a user who only has
+ // 'view scheduled {type}' permission. This is allowed and they should see
+ // the scheduled published content by all users and their own unpublished
+ // content. Unpublished node and media items by other users are also listed
+ // but products are not. Therefore do not check for the unpublished item
+ // by Scheduler Editor.
+ $this->drupalLogin($this->schedulerViewer);
+ $this->drupalGet($scheduled_url);
+ $assert->statusCodeEquals(200);
+ $assert->pageTextContains("$entityTypeId created by Scheduler Editor for unpublishing");
+ $assert->pageTextContains("$entityTypeId created by Scheduler Viewer for publishing");
+ $assert->pageTextContains("$entityTypeId created by Scheduler Viewer for unpublishing");
+ }
+
+}
diff --git a/tests/src/FunctionalJavascript/SchedulerJavascriptDefaultTimeTest.php b/tests/src/FunctionalJavascript/SchedulerJavascriptDefaultTimeTest.php
index 7bf365f..e6f5e8f 100644
--- a/tests/src/FunctionalJavascript/SchedulerJavascriptDefaultTimeTest.php
+++ b/tests/src/FunctionalJavascript/SchedulerJavascriptDefaultTimeTest.php
@@ -30,7 +30,7 @@ class SchedulerJavascriptDefaultTimeTest extends SchedulerJavascriptTestBase {
$this->drupalLogin($this->schedulerUser);
$this->drupalGet('node/add/' . $this->type);
$page = $this->getSession()->getPage();
- $title = 'Date format test ' . $this->randomString(12);
+ $title = 'Determine the date-picker format';
$page->fillField('edit-title-0-value', $title);
$page->clickLink('Scheduling options');
// Set the date using a day and month which could be correctly interpreted
@@ -50,8 +50,10 @@ class SchedulerJavascriptDefaultTimeTest extends SchedulerJavascriptTestBase {
*
* @dataProvider dataTimeWhenSchedulingIsRequired()
*/
- public function testTimeWhenSchedulingIsRequired($field) {
+ public function testTimeWhenSchedulingIsRequired($entityTypeId, $bundle, $field) {
$config = $this->config('scheduler.settings');
+ $titleField = ($entityTypeId == 'media') ? 'name' : 'title';
+ $entityType = $this->entityTypeObject($entityTypeId);
// This test is only relevant when the configuration allows a date only with
// a default time specified. Testing with 'allow_date_only' = false is
@@ -67,18 +69,21 @@ class SchedulerJavascriptDefaultTimeTest extends SchedulerJavascriptTestBase {
$scheduling_time = new \DateTime();
$scheduling_time->add(new \DateInterval('P1D'))->setTime(19, 30, 20);
+ // Node and Media entities are revisionable and the 'Revision Information'
+ // tab is the default active one, so needs a click on 'Scheduling Options'.
+ // Products do not have this link, so the click would fail. A simple way to
+ // resolve this is display the scheduler options as a separate fieldset.
+ $entityType->setThirdPartySetting('scheduler', 'fields_display_mode', 'fieldset')->save();
+
foreach ([TRUE, FALSE] as $required) {
- // Set the publish-on/unpublish-on date to the $required setting.
- $this->nodetype->setThirdPartySetting('scheduler', $field . '_required', $required)->save();
+ // Set the publish_on/unpublish_on required setting.
+ $entityType->setThirdPartySetting('scheduler', $field . '_required', $required)->save();
- // Create a node.
- $this->drupalGet('node/add/' . $this->type);
+ // Create an entity.
+ $this->drupalGet($this->entityAddUrl($entityTypeId, $bundle));
$page = $this->getSession()->getPage();
-
- $title = ucfirst($field) . ($required ? ' required ' : ' not required ') . $this->randomString(12);
- $page->fillField('edit-title-0-value', $title);
- $page->fillField('edit-body-0-value', 'datepickerFormat = ' . $this->datepickerFormat);
- $page->clickLink('Scheduling options');
+ $title = ucfirst($field) . ($required ? ' required' : ' not required') . ', datepickerFormat = ' . $this->datepickerFormat;
+ $page->fillField("edit-$titleField-0-value", $title);
if ($required) {
// Fill in the date value but do nothing with the time field.
$page->fillField('edit-' . $field . '-on-0-value-date', $scheduling_time->format($this->datepickerFormat));
@@ -86,17 +91,17 @@ class SchedulerJavascriptDefaultTimeTest extends SchedulerJavascriptTestBase {
$page->pressButton('Save');
// Test that the content has saved properly.
- $this->assertSession()->pageTextContains(sprintf('%s %s has been created', $this->typeName, $title));
+ $this->assertSession()->pageTextMatches('/' . preg_quote($title, '/') . ' has been (created|successfully saved)/');
- $node = $this->drupalGetNodeByTitle($title);
- $this->assertNotEmpty($node, 'The node could not be found');
+ $entity = $this->getEntityByTitle($entityTypeId, $title);
+ $this->assertNotEmpty($entity, 'The entity object can be found by title');
if ($required) {
// Check that the scheduled date and time are correct.
- $this->assertEquals($scheduling_time->getTimestamp(), (int) $node->{$field . '_on'}->value);
+ $this->assertEquals($scheduling_time->getTimestamp(), (int) $entity->{$field . '_on'}->value);
}
else {
// Check that no scheduled date was stored.
- $this->assertEmpty($node->{$field . '_on'}->value);
+ $this->assertEmpty($entity->{$field . '_on'}->value);
}
}
}
@@ -104,14 +109,19 @@ class SchedulerJavascriptDefaultTimeTest extends SchedulerJavascriptTestBase {
/**
* Provides data for testTimeWhenSchedulingIsRequired().
*
+ * The data in dataStandardEntityTypes() is expanded to test each entity type
+ * with each of the scheduler date fields.
+ *
* @return array
- * The test data.
+ * Each array item has the values: [entity type id, bundle id, field name].
*/
public function dataTimeWhenSchedulingIsRequired() {
- return [
- ['publish'],
- ['unpublish'],
- ];
+ $data = [];
+ foreach ($this->dataStandardEntityTypes() as $key => $values) {
+ $data["$key-1"] = array_merge($values, ['publish']);
+ $data["$key-2"] = array_merge($values, ['unpublish']);
+ }
+ return $data;
}
}
diff --git a/tests/src/FunctionalJavascript/SchedulerJavascriptTestBase.php b/tests/src/FunctionalJavascript/SchedulerJavascriptTestBase.php
index 0e92de9..32adbdd 100644
--- a/tests/src/FunctionalJavascript/SchedulerJavascriptTestBase.php
+++ b/tests/src/FunctionalJavascript/SchedulerJavascriptTestBase.php
@@ -3,6 +3,8 @@
namespace Drupal\Tests\scheduler\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\scheduler\Traits\SchedulerCommerceProductSetupTrait;
+use Drupal\Tests\scheduler\Traits\SchedulerMediaSetupTrait;
use Drupal\Tests\scheduler\Traits\SchedulerSetupTrait;
/**
@@ -13,6 +15,8 @@ use Drupal\Tests\scheduler\Traits\SchedulerSetupTrait;
abstract class SchedulerJavascriptTestBase extends WebDriverTestBase {
use SchedulerSetupTrait;
+ use SchedulerMediaSetupTrait;
+ use SchedulerCommerceProductSetupTrait;
/**
* The standard modules to load for all javascript tests.
@@ -21,7 +25,11 @@ abstract class SchedulerJavascriptTestBase extends WebDriverTestBase {
*
* @var array
*/
- protected static $modules = ['scheduler'];
+ protected static $modules = [
+ 'scheduler',
+ 'media',
+ 'commerce_product',
+ ];
/**
* The profile to install as a basis for testing.
@@ -40,8 +48,17 @@ abstract class SchedulerJavascriptTestBase extends WebDriverTestBase {
*/
protected function setUp(): void {
parent::setUp();
- // Call the common set-up function defined in the trait.
+ // Call the common set-up functions defined in the traits.
$this->schedulerSetUp();
+ // $this->getName() includes the test class and the dataProvider key. We can
+ // use this to save time and resources by avoiding calls to the media and
+ // product setup functions when they are not needed.
+ if (stristr($this->getName(), 'media')) {
+ $this->schedulerMediaSetUp();
+ }
+ if (stristr($this->getName(), 'product')) {
+ $this->SchedulerCommerceProductSetUp();
+ }
}
/**
diff --git a/tests/src/Traits/SchedulerCommerceProductSetupTrait.php b/tests/src/Traits/SchedulerCommerceProductSetupTrait.php
new file mode 100644
index 0000000..b786f8d
--- /dev/null
+++ b/tests/src/Traits/SchedulerCommerceProductSetupTrait.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Traits;
+
+/**
+ * Additional setup trait for Scheduler tests that use Commerce Product.
+ */
+trait SchedulerCommerceProductSetupTrait {
+
+ /**
+ * The internal name of the standard product type for testing.
+ *
+ * Use the pre-existing 'default' product type. This is a short-cut.
+ *
+ * @var string
+ */
+ protected $productTypeName = 'test_product';
+
+ /**
+ * The readable label of the standard product type for testing.
+ *
+ * @var string
+ */
+ protected $productTypeLabel = 'Test Product';
+
+ /**
+ * The product type object which is enabled for scheduling.
+ *
+ * @var Drupal\commerce_product\Entity\ProductType
+ */
+ protected $productType;
+
+ /**
+ * The default commerce store to which all products are added.
+ *
+ * @var Drupal\commerce_store\Entity\Store
+ */
+ protected $store;
+
+ /**
+ * The internal name of the product type not enabled for scheduling.
+ *
+ * @var string
+ */
+ protected $nonSchedulerProductTypeName = 'non_enabled_product';
+
+ /**
+ * The readable label of the product type not enabled for scheduling.
+ *
+ * @var string
+ */
+ protected $nonSchedulerProductTypeLabel = 'Non-scheduler Product';
+
+ /**
+ * The product type object which is not enabled for scheduling.
+ *
+ * @var Drupal\commerce_product\Entity\ProductType
+ */
+ protected $nonSchedulerProductType;
+
+ /**
+ * The product entity storage.
+ *
+ * Is this really needed now that we can use $this->entityStorageObject() ?
+ *
+ * @var Drupal\commerce\CommerceContentEntityStorage
+ */
+ protected $productStorage;
+
+ /**
+ * Set common properties, define content types and create users.
+ */
+ public function schedulerCommerceProductSetUp() {
+
+ /** @var Store $store */
+ $this->store = $this->entityStorageObject('commerce_store')->create([
+ 'type' => 'online',
+ 'name' => 'My Test Store',
+ ]);
+ $this->store->save();
+
+ $product_type_storage = $this->container->get('entity_type.manager')->getStorage('commerce_product_type');
+
+ // Create a test product type that is enabled for scheduling.
+ /** @var Drupal\commerce_product\Entity\ProductType $productType */
+ $this->productType = $product_type_storage->create([
+ 'id' => $this->productTypeName,
+ 'label' => $this->productTypeLabel,
+ 'variationType' => 'default',
+ ]);
+
+ // Add scheduler functionality to the product type, then save.
+ $this->productType->setThirdPartySetting('scheduler', 'publish_enable', TRUE)
+ ->setThirdPartySetting('scheduler', 'unpublish_enable', TRUE)
+ ->save();
+
+ // Add the body field using the existing commerce_product function.
+ commerce_product_add_body_field($this->productType);
+
+ // Create a test product type which is not enabled for scheduling.
+ /** @var Drupal\commerce_product\Entity\ProductType $nonSchedulerProductType */
+ $this->nonSchedulerProductType = $product_type_storage->create([
+ 'id' => $this->nonSchedulerProductTypeName,
+ 'label' => $this->nonSchedulerProductTypeLabel,
+ 'variationType' => 'default',
+ ]);
+ // Requires a separate save, not part of the create() above, if not doing
+ // any other save() on the product type.
+ $this->nonSchedulerProductType->save();
+
+ /** @var Drupal\commerce\CommerceContentEntityStorage $productStorage */
+ $this->productStorage = $this->container->get('entity_type.manager')->getStorage('commerce_product');
+
+ // Add extra permisssions to the role assigned to the adminUser.
+ $this->addPermissionsToUser($this->adminUser, [
+ 'create ' . $this->productTypeName . ' commerce_product',
+ 'update any ' . $this->productTypeName . ' commerce_product',
+ 'delete any ' . $this->productTypeName . ' commerce_product',
+ 'create ' . $this->nonSchedulerProductTypeName . ' commerce_product',
+ 'update any ' . $this->nonSchedulerProductTypeName . ' commerce_product',
+ 'delete any ' . $this->nonSchedulerProductTypeName . ' commerce_product',
+ // 'administer commerce_store' is needed to see and use any store, i.e
+ // cannot add a product without this. Is it a bug?
+ 'administer commerce_store',
+ 'access commerce_product overview',
+ 'view own unpublished commerce_product',
+ 'schedule publishing of commerce_product',
+ 'view scheduled commerce_product',
+ ]);
+
+ // Add extra permisssions to the role assigned to the schedulerUser.
+ $this->addPermissionsToUser($this->schedulerUser, [
+ 'create ' . $this->productTypeName . ' commerce_product',
+ 'update any ' . $this->productTypeName . ' commerce_product',
+ 'delete any ' . $this->productTypeName . ' commerce_product',
+ // 'administer commerce_store' is needed to see and use any store, i.e
+ // cannot add a product without this. Is it a bug?
+ 'administer commerce_store',
+ 'view own unpublished commerce_product',
+ 'schedule publishing of commerce_product',
+ ]);
+ }
+
+ /**
+ * Creates a product entity.
+ *
+ * @param array $values
+ * The values to use for the entity.
+ *
+ * @return Drupal\commerce_product\Entity\ProductInterface
+ * The created product object.
+ */
+ public function createProduct(array $values = []) {
+ // Provide defaults for the critical values.
+ $values += [
+ 'type' => $this->productTypeName,
+ 'title' => $this->randomstring(12),
+ ];
+ /** @var \Drupal\commerce_product\ProductInterface $product */
+ $product = $this->productStorage->create($values);
+ $product->save();
+ return $product;
+ }
+
+ /**
+ * Gets a product from storage.
+ *
+ * For nodes, there is drupalGetNodeByTitle() but nothing similar exists to
+ * help Product testing. See getMediaItem for more details.
+ *
+ * @param string $name
+ * Optional name text to match on. If given and no match, returns NULL.
+ * If no $name is given then returns the product with the highest id value.
+ *
+ * @return \Drupal\commerce_product\Entity\ProductInterface
+ * The commerce product object.
+ */
+ public function getProduct(string $name = NULL) {
+ $query = $this->productStorage->getQuery()
+ ->accessCheck(FALSE)
+ ->sort('product_id', 'DESC');
+ if (!empty($name)) {
+ $query->condition('title', $name);
+ }
+ $result = $query->execute();
+ if (count($result)) {
+ $id = reset($result);
+ return $this->productStorage->load($id);
+ }
+ else {
+ return NULL;
+ }
+ }
+
+}
diff --git a/tests/src/Traits/SchedulerMediaSetupTrait.php b/tests/src/Traits/SchedulerMediaSetupTrait.php
new file mode 100644
index 0000000..04f34fc
--- /dev/null
+++ b/tests/src/Traits/SchedulerMediaSetupTrait.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace Drupal\Tests\scheduler\Traits;
+
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+
+/**
+ * Additional setup trait for Scheduler tests that use Media.
+ *
+ * This builds on the standard SchedulerSetupTrait.
+ */
+trait SchedulerMediaSetupTrait {
+
+ use MediaTypeCreationTrait;
+
+ /**
+ * The internal name of the standard media type created for testing.
+ *
+ * @var string
+ */
+ protected $mediaTypeName = 'test_video';
+
+ /**
+ * The readable label of the standard media type created for testing.
+ *
+ * @var string
+ */
+ protected $mediaTypeLabel = 'Test Video';
+
+ /**
+ * The media type object which is enabled for scheduling.
+ *
+ * @var \Drupal\media\MediaTypeInterface
+ */
+ protected $mediaType;
+
+ /**
+ * The internal name of the media type not enabled for scheduling.
+ *
+ * @var string
+ */
+ protected $nonSchedulerMediaTypeName = 'test_audio_not_enabled';
+
+ /**
+ * The readable label of the media type not enabled for scheduling.
+ *
+ * @var string
+ */
+ protected $nonSchedulerMediaTypeLabel = 'Test Audio - not for scheduling';
+
+ /**
+ * The media type object which is not enabled for scheduling.
+ *
+ * @var \Drupal\media\MediaTypeInterface
+ */
+ protected $nonSchedulerMediaType;
+
+ /**
+ * The media entity storage.
+ *
+ * @var \Drupal\Core\Entity\ContentEntityStorageInterface
+ */
+ protected $mediaStorage;
+
+ /**
+ * Set common properties, define content types and create users.
+ */
+ public function schedulerMediaSetUp() {
+
+ // Create a test media type for video that is enabled for scheduling.
+ /** @var \Drupal\media\Entity\MediaTypeInterface $mediaType */
+ $this->mediaType = $this->createMediaType('video_file', [
+ 'id' => $this->mediaTypeName,
+ 'label' => $this->mediaTypeLabel,
+ ]);
+
+ // Add scheduler functionality to the video media type.
+ $this->mediaType->setThirdPartySetting('scheduler', 'publish_enable', TRUE)
+ ->setThirdPartySetting('scheduler', 'unpublish_enable', TRUE)
+ ->save();
+
+ // Create a test media type for audio which is not enabled for scheduling.
+ /** @var \Drupal\media\Entity\MediaTypeInterface $nonSchedulerMediaType */
+ $this->nonSchedulerMediaType = $this->createMediaType('audio_file', [
+ 'id' => $this->nonSchedulerMediaTypeName,
+ 'label' => $this->nonSchedulerMediaTypeLabel,
+ ]);
+
+ // Define mediaStorage for use in many tests.
+ /** @var MediaStorageInterface $mediaStorage */
+ $this->mediaStorage = $this->container->get('entity_type.manager')->getStorage('media');
+
+ // Add extra permisssions to the role assigned to the adminUser.
+ $this->addPermissionsToUser($this->adminUser, [
+ 'create ' . $this->mediaTypeName . ' media',
+ 'edit any ' . $this->mediaTypeName . ' media',
+ 'delete any ' . $this->mediaTypeName . ' media',
+ 'create ' . $this->nonSchedulerMediaTypeName . ' media',
+ 'edit any ' . $this->nonSchedulerMediaTypeName . ' media',
+ 'delete any ' . $this->nonSchedulerMediaTypeName . ' media',
+ 'access media overview',
+ 'view own unpublished media',
+ 'schedule publishing of media',
+ 'view scheduled media',
+ ]);
+
+ // Add extra permisssions to the role assigned to the schedulerUser.
+ $this->addPermissionsToUser($this->schedulerUser, [
+ 'create ' . $this->mediaTypeName . ' media',
+ 'edit own ' . $this->mediaTypeName . ' media',
+ 'delete own ' . $this->mediaTypeName . ' media',
+ 'view own unpublished media',
+ 'schedule publishing of media',
+ ]);
+
+ // By default, media items cannot be viewed directly, and the url media/mid
+ // gives a 404 not found. Changing this setting makes debugging the tests
+ // easier. It is also required for the meta information test.
+ $configFactory = $this->container->get('config.factory');
+ $configFactory->getEditable('media.settings')
+ ->set('standalone_url', TRUE)
+ ->save(TRUE);
+ $this->container->get('router.builder')->rebuild();
+
+ // Set the media file attachments to be optional not required, to simplify
+ // editing and saving media entities.
+ $configFactory->getEditable('field.field.media.test_video.field_media_video_file')
+ ->set('required', FALSE)
+ ->save(TRUE);
+ $configFactory->getEditable('field.field.media.test_audio_not_enabled.field_media_audio_file')
+ ->set('required', FALSE)
+ ->save(TRUE);
+ }
+
+ /**
+ * Creates a media entity.
+ *
+ * @param array $values
+ * The values to use for the entity.
+ *
+ * @return \Drupal\media\MediaInterface
+ * The created media object.
+ */
+ public function createMediaItem(array $values) {
+ // Provide defaults for the critical values.
+ $values += [
+ 'bundle' => $this->mediaTypeName,
+ 'name' => $this->randomstring(12),
+ ];
+ /** @var \Drupal\media\MediaInterface $media */
+ $media = $this->mediaStorage->create($values);
+ $media->save();
+ return $media;
+ }
+
+ /**
+ * Gets a media item from storage.
+ *
+ * For nodes, there is drupalGetNodeByTitle() but nothing similar exists to
+ * help Media testing. But this function goes one better - if a name is given,
+ * then a match will be attempted on the name, and fail if none found. But if
+ * no name is supplied then the media entity with the highest id value (the
+ * newest item created) is returned, as this is often what is required.
+ *
+ * @param string $name
+ * Optional name text to match on. If given and no match, returns NULL.
+ * If no $name is given then returns the media with the highest id value.
+ *
+ * @return \Drupal\media\MediaInterface
+ * The media object.
+ */
+ public function getMediaItem(string $name = NULL) {
+ $query = $this->mediaStorage->getQuery()
+ ->accessCheck(FALSE)
+ ->sort('mid', 'DESC');
+ if (!empty($name)) {
+ $query->condition('name', $name);
+ }
+ $result = $query->execute();
+ if (count($result)) {
+ $media_id = reset($result);
+ return $this->mediaStorage->load($media_id);
+ }
+ else {
+ return NULL;
+ }
+ }
+
+}
diff --git a/tests/src/Traits/SchedulerSetupTrait.php b/tests/src/Traits/SchedulerSetupTrait.php
index 476bbd4..0dd5da9 100644
--- a/tests/src/Traits/SchedulerSetupTrait.php
+++ b/tests/src/Traits/SchedulerSetupTrait.php
@@ -2,6 +2,9 @@
namespace Drupal\Tests\scheduler\Traits;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Url;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\Traits\Core\CronRunTrait;
/**
@@ -13,6 +16,13 @@ trait SchedulerSetupTrait {
use CronRunTrait;
+ use NodeCreationTrait {
+ // Allow this trait to be used in Kernel tests (which do not use
+ // BrowserTestBase) and hence will not have these two functions.
+ getNodeByTitle as drupalGetNodeByTitle;
+ createNode as drupalCreateNode;
+ }
+
// @todo Remove this when core 8.8 is the lowest supported version.
// @see https://www.drupal.org/project/scheduler/issues/3136744
use PhpunitCompatibilityCore87Trait;
@@ -36,14 +46,14 @@ trait SchedulerSetupTrait {
*
* @var string
*/
- protected $type;
+ protected $type = 'testpage';
/**
* The readable name of the standard content type created for testing.
*
* @var string
*/
- protected $typeName;
+ protected $typeName = 'Test Page';
/**
* The node type object.
@@ -52,12 +62,26 @@ trait SchedulerSetupTrait {
*/
protected $nodetype;
+ /**
+ * The machine name of the content type which is not enabled for scheduling.
+ *
+ * @var string
+ */
+ protected $nonSchedulerType = 'not_for_scheduler';
+
+ /**
+ * The readable name of content type which is not enabled for scheduling.
+ *
+ * @var string
+ */
+ protected $nonSchedulerTypeName = 'Not For Scheduler';
+
/**
* The node type object which is not enabled for scheduling.
*
* @var \Drupal\node\Entity\NodeType
*/
- protected $nonSchedulerNodetype;
+ protected $nonSchedulerNodeType;
/**
* The node storage object.
@@ -92,12 +116,10 @@ trait SchedulerSetupTrait {
*/
public function schedulerSetUp() {
- // Create a test content type with id 'testpage' and name 'Test Page'.
- // The tests should use $this->type and $this->typeName and not use
+ // Create a test content type using the type and name constants defined
+ // above. The tests should use $this->type and $this->typeName and not use
// $this->nodetype->get('type') or $this->nodetype->get('name'), nor have
// the hard-coded strings 'testpage' or 'Test Page'.
- $this->type = 'testpage';
- $this->typeName = 'Test Page';
/** @var NodeTypeInterface $nodetype */
$this->nodetype = $this->drupalCreateContentType([
'type' => $this->type,
@@ -112,8 +134,8 @@ trait SchedulerSetupTrait {
// The majority of tests use the standard Scheduler-enabled content type but
// we also need a content type which is not enabled for Scheduler.
$this->nonSchedulerNodeType = $this->drupalCreateContentType([
- 'type' => 'not-for-scheduler',
- 'name' => 'Not For Scheduler',
+ 'type' => $this->nonSchedulerType,
+ 'name' => $this->nonSchedulerTypeName,
]);
// Define nodeStorage for use in many tests.
@@ -124,23 +146,26 @@ trait SchedulerSetupTrait {
// rights on the test content type and all of the Scheduler permissions.
// 'access site reports' is required for admin/reports/dblog.
// 'administer site configuration' is required for admin/reports/status.
+ // 'administer content types' is required for admin/structure/types/manage.
$this->adminUser = $this->drupalCreateUser([
'access content',
'access content overview',
'access site reports',
'administer nodes',
+ 'administer content types',
'administer site configuration',
'create ' . $this->type . ' content',
'edit any ' . $this->type . ' content',
'delete any ' . $this->type . ' content',
- 'create ' . $this->nonSchedulerNodeType->id() . ' content',
- 'edit any ' . $this->nonSchedulerNodeType->id() . ' content',
- 'delete any ' . $this->nonSchedulerNodeType->id() . ' content',
+ 'create ' . $this->nonSchedulerType . ' content',
+ 'edit any ' . $this->nonSchedulerType . ' content',
+ 'delete any ' . $this->nonSchedulerType . ' content',
'view own unpublished content',
'administer scheduler',
'schedule publishing of nodes',
'view scheduled content',
]);
+ $this->adminUser->set('name', 'Admolly the Admin user')->save();
// Create an ordinary Scheduler user, with permission to create and schedule
// content but not with administrator permissions.
@@ -150,8 +175,8 @@ trait SchedulerSetupTrait {
'delete own ' . $this->type . ' content',
'view own unpublished content',
'schedule publishing of nodes',
- 'view scheduled content',
]);
+ $this->schedulerUser->set('name', 'Shelly the Scheduler user')->save();
// Store the database connection for re-use in the actual tests.
$this->database = $this->container->get('database');
@@ -164,4 +189,234 @@ trait SchedulerSetupTrait {
}
+ /**
+ * Adds a set of permissions to an existing user.
+ *
+ * This avoids having to create new users when a test requires additional
+ * permissions, as that leads to having a list of existing permissions which
+ * has to be kept in sync with the standard user permissions.
+ *
+ * Each test user has two roles, 'authenticated' and one other randomly-named
+ * role assigned when the user is created, and unique to that user. This is
+ * the role to which these permissions are added.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $user
+ * The user object.
+ * @param array $permissions
+ * The machine names of new permissions to add to the user's unique role.
+ */
+ public function addPermissionsToUser(AccountInterface $user, array $permissions) {
+ /** @var \Drupal\user\Entity\RoleStorageInterface $roleStorage */
+ $roleStorage = $this->container->get('entity_type.manager')->getStorage('user_role');
+ foreach ($user->getRoles() as $rid) {
+ // The user will have two roles, 'authenticated' and one other.
+ if ($rid != 'authenticated') {
+ $role = $roleStorage->load($rid);
+ foreach ($permissions as $permission) {
+ $role->grantPermission($permission);
+ }
+ $role->save();
+ }
+ }
+ }
+
+ /**
+ * Creates a test entity.
+ *
+ * This is called to generate a node, media or product entity, for tests that
+ * process all types of entities, either in loops or via a data provider.
+ *
+ * @param string $entityTypeId
+ * The name of the entity type - 'node', 'media' or 'commerce_product'.
+ * @param string $bundle
+ * The name of the bundle. Optional, will default to $this->type for nodes
+ * $this->mediaTypeName for media, or $this->productTypeName for products.
+ * @param array $values
+ * Values for the new entity.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * The created entity object.
+ */
+ public function createEntity(string $entityTypeId, string $bundle = NULL, array $values = []) {
+
+ switch ($entityTypeId) {
+ case 'media':
+ $values += ['bundle' => $bundle ?? $this->mediaTypeName];
+ // For Media, the title is stored in the 'name' field, so get the title
+ // when the 'name' is not defined, to allow the same $value parameters
+ // as for Node.
+ if (isset($values['title'])) {
+ $values['name'] = $values['name'] ?? $values['title'];
+ unset($values['title']);
+ }
+ $entity = $this->createMediaItem($values);
+ break;
+
+ case 'commerce_product':
+ $values += ['type' => $bundle ?? $this->productTypeName];
+ $entity = $this->createProduct($values);
+ break;
+
+ case 'node':
+ default:
+ // For nodes the field for bundle is called 'type'.
+ $values += ['type' => $bundle ?? $this->type];
+ $entity = $this->drupalCreateNode($values);
+ break;
+ }
+ return $entity;
+ }
+
+ /**
+ * Gets an entity by title, a direct replacement of drupalGetNodeByTitle().
+ *
+ * This allows the same test code to be run for Nodes, Media and Products.
+ *
+ * @param string $entityTypeId
+ * The machine id of the entity type - 'node', 'media', 'commerce_product'.
+ * @param string $title
+ * The title to match with.
+ *
+ * @return mixed
+ * Either a node object, media object, commerce_product object, or none.
+ */
+ public function getEntityByTitle(string $entityTypeId, string $title) {
+ switch ($entityTypeId) {
+ case 'node':
+ return $this->drupalGetNodeByTitle($title);
+
+ case 'media':
+ return $this->getMediaItem($title);
+
+ case 'commerce_product':
+ return $this->getProduct($title);
+
+ default:
+ // Incorrect parameter value.
+ throw new \Exception(sprintf('Unrecognised entityTypeId value "%s" passed to getEntityByTitle()', $entityTypeId));
+ }
+ }
+
+ /**
+ * Returns the stored entity type object from a type id and bundle id.
+ *
+ * This allows previous usages of $this->nodetype to be replaced by
+ * entityTypeObject($entityTypeId) or entityTypeObject($entityTypeId, $bundle)
+ * when expanding tests to cover Media and Product entities.
+ *
+ * @param string $entityTypeId
+ * The machine id of the entity type - 'node', 'media', 'commerce_product'.
+ * @param string $bundle
+ * The machine name of the bundle, for example 'testpage', 'test_video',
+ * 'not_for_scheduler', etc. Optional. Defaults to the enabled bundle. Also
+ * accepts the fixed string 'non-enabled' to indicate the non-enabled bundle
+ * for the entity type.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface
+ * The stored entity type object.
+ */
+ public function entityTypeObject(string $entityTypeId, string $bundle = NULL) {
+ switch (TRUE) {
+ case ($entityTypeId == 'node' && (empty($bundle) || $bundle == $this->type)):
+ return $this->nodetype;
+
+ case ($entityTypeId == 'node' && ($bundle == 'non-enabled' || $bundle == $this->nonSchedulerType)):
+ return $this->nonSchedulerNodeType;
+
+ case ($entityTypeId == 'media' && (empty($bundle) || $bundle == $this->mediaTypeName)):
+ return $this->mediaType;
+
+ case ($entityTypeId == 'media' && ($bundle == 'non-enabled' || $bundle == $this->nonSchedulerMediaTypeName)):
+ return $this->nonSchedulerMediaType;
+
+ case ($entityTypeId == 'commerce_product' && (empty($bundle) || $bundle == $this->productTypeName)):
+ return $this->productType;
+
+ case ($entityTypeId == 'commerce_product' && ($bundle == 'non-enabled' || $bundle == $this->nonSchedulerProductTypeName)):
+ return $this->nonSchedulerProductType;
+
+ default:
+ // Incorrect parameter values.
+ throw new \Exception(sprintf('Unrecognised entityTypeId and bundle combination "%s" and "%s" passed to entityTypeObject()', $entityTypeId, $bundle));
+ }
+ }
+
+ /**
+ * Returns the url for adding an entity, for use in drupalGet().
+ *
+ * @param string $entityTypeId
+ * The machine id of the entity type - 'node', 'media', 'commerce_product'.
+ * @param string $bundle
+ * The machine name of the bundle, for example 'testpage', 'test_video',
+ * 'not_for_scheduler', etc. Optional. Defaults to the enabled bundle. Also
+ * accepts the fixed string 'non-enabled' to indicate the non-enabled bundle
+ * for the entity type.
+ *
+ * @return \Drupal\Core\Url
+ * The url object for adding the required entity.
+ */
+ public function entityAddUrl(string $entityTypeId, string $bundle = NULL) {
+ switch ($entityTypeId) {
+ case 'node':
+ $bundle = ($bundle == 'non-enabled') ? $this->nonSchedulerType : ($bundle ?? $this->type);
+ $route = 'node.add';
+ $type_parameter = 'node_type';
+ break;
+
+ case 'media':
+ $bundle = ($bundle == 'non-enabled') ? $this->nonSchedulerMediaTypeName : ($bundle ?? $this->mediaTypeName);
+ $route = 'entity.media.add_form';
+ $type_parameter = 'media_type';
+ break;
+
+ case 'commerce_product':
+ $bundle = ($bundle == 'non-enabled') ? $this->nonSchedulerProductTypeName : ($bundle ?? $this->productTypeName);
+ $route = 'entity.commerce_product.add_form';
+ $type_parameter = 'commerce_product_type';
+ break;
+
+ default:
+ }
+ if (!$url = Url::fromRoute($route, [$type_parameter => $bundle])) {
+ // Incorrect parameter values.
+ throw new \Exception(sprintf('Invalid entityTypeId "%s" or bundle "%s" passed to entityAddUrl()', $entityTypeId, $bundle));
+ }
+ return $url;
+ }
+
+ /**
+ * Returns the storage object of the entity type passed by string.
+ *
+ * This allows previous usage of the hard-coded $this->nodeStorage to be
+ * replaced with $this->entityStorageObject($entityTypeId) when expanding the
+ * tests to cover media and product entity types.
+ *
+ * @param string $entityTypeId
+ * The machine id of the entity type.
+ *
+ * @return \Drupal\Core\Entity\ContentEntityStorageInterface
+ * The entity storage object.
+ */
+ public function entityStorageObject(string $entityTypeId) {
+ return $this->container->get('entity_type.manager')->getStorage($entityTypeId);
+ }
+
+ /**
+ * Provides test data containing the standard entity types.
+ *
+ * @return array
+ * Each array item has the values: [entity type id, bundle id]. The array
+ * key is #entity_type_id, to allow easy removal of unwanted rows later.
+ */
+ public function dataStandardEntityTypes() {
+ // The data provider has access to $this where the values are set in the
+ // property definition.
+ $data = [
+ '#node' => ['node', $this->type],
+ '#media' => ['media', $this->mediaTypeName],
+ '#commerce_product' => ['commerce_product', $this->productTypeName],
+ ];
+ return $data;
+ }
+
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment