diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.admin.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.admin.inc index 1f9be3f9c..3e8e184b1 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.admin.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.admin.inc @@ -9,7 +9,9 @@ * Form constructor for the main admin form for configuring Scheduler. */ function scheduler_admin() { - $now = t('Example: %date', array('%date' => format_date(REQUEST_TIME, 'custom', variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT)))); + $now = t('Example: %date', array( + '%date' => format_date(REQUEST_TIME, 'custom', variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT)), + )); $form['scheduler_date_format'] = array( '#type' => 'textfield', '#title' => t('Date format'), @@ -21,7 +23,7 @@ function scheduler_admin() { '#description' => t('The format for entering scheduled dates and times. For the date use the letters !date_letters and for the time use !time_letters. See !url for more details.', array( '!date_letters' => SCHEDULER_DATE_LETTERS, '!time_letters' => SCHEDULER_TIME_LETTERS, - '!url' => l(t('the PHP date() function'), 'http://www.php.net/manual/en/function.date.php') + '!url' => l(t('the PHP date() function'), 'http://www.php.net/manual/en/function.date.php'), )), ); @@ -45,14 +47,14 @@ function scheduler_admin() { // Variable 'date_popup_timepicker' holds the type of timepicker selected. $timepicker_enabled = (variable_get('date_popup_timepicker', '') != 'none'); $options = array('@date_popup_config' => url('admin/config/date/date_popup')); - $description = t('Restrict the time entry to specific minute increments.') . ' ' - . ($timepicker_enabled + $description[] = t('Restrict the time entry to specific minute increments.'); + $description[] = ($timepicker_enabled ? t('The timepicker type can be selected via the Date Popup configuration page.', $options) : t('The timepicker is not enabled - turn it on via the Date Popup configuration page.', $options)); $form['scheduler_date_popup_minute_increment'] = array( '#type' => 'textfield', '#title' => t('Date Popup minute increment'), - '#description' => $description, + '#description' => implode(' ', $description), '#field_suffix' => t('minutes'), '#size' => 2, '#maxlength' => 2, @@ -99,6 +101,14 @@ function scheduler_admin() { '#description' => t('The text entered into this field will be displayed above the scheduling fields in the node edit form.'), ); + $form['scheduler_cache_clear_all'] = array( + '#prefix' => '', + '#type' => 'checkbox', + '#title' => t('Clear all expired block and page caches after publishing or unpublishing via cron.'), + '#default_value' => variable_get('scheduler_cache_clear_all', 0), + '#description' => t('If a node has been published or unpublished by Scheduler during a cron run, this option will clear the caches instead of relying on the Drupal core system cron task. Warning: This may have a detrimental effect on performance for large sites.'), + ); + // Add a submit handler function. $form['#submit'][] = 'scheduler_admin_submit'; @@ -131,8 +141,12 @@ function scheduler_admin_validate($form, &$form_state) { // The Date Popup function date_popup_time_formats() only returns the values // 'H:i:s' and 'h:i:sA' but Scheduler can accept more variations than just // these. Firstly, we add the lowercase 'a' alternative. Secondly timepicker - // always requires hours and minutes, but seconds are optional. - $acceptable = array('H:i:s', 'h:i:sA', 'h:i:sa', 'H:i', 'h:iA', 'h:ia'); + // always requires hours and minutes, but seconds are optional. Spaces are + // allowed before the 'A' or 'a'. + $acceptable = array( + 'H:i:s', 'h:i:sA', 'h:i:s A', 'h:i:sa', 'h:i:s a', + 'H:i', 'h:iA', 'h:i A', 'h:ia', 'h:i a', + ); if ($time_format && !in_array($time_format, $acceptable)) { form_set_error('scheduler_date_format', t('When using the Date Popup module, the allowed time formats are: !formats', array('!formats' => implode(', ', $acceptable)))); @@ -237,7 +251,7 @@ function _scheduler_form_node_type_form_alter(&$form, $form_state) { '#group' => 'additional_settings', '#attached' => array( 'js' => array( - 'vertical-tabs' => drupal_get_path('module', 'scheduler') . "/scheduler_vertical_tabs.js", + 'vertical-tabs' => _scheduler_get_vertical_tabs_js(), ), ), ); @@ -386,9 +400,10 @@ function _scheduler_form_node_type_form_alter(&$form, $form_state) { */ function _scheduler_lightweight_cron($form, &$form_state) { $form = array(); + $prefix_text = t("You can test Scheduler's lightweight cron process interactively"); $form['scheduler_cron'] = array( '#type' => 'submit', - '#prefix' => '
' . t('Your server\'s time is @utc. In most cases this should match Greenwich Mean Time (GMT) / Coordinated Universal Time (UTC)', $t_options) . '
' - . '' . t('The website default timezone is @date_default_timezone (@date_default_code) which is offset from GMT by @date_default_offset hours. This timezone can be changed by admin users with the appropriate access.', $t_options) . '
'; + $output = '' . t('Your server\'s time is @utc. In most cases this should match Greenwich Mean Time (GMT) / Coordinated Universal Time (UTC)', $t_options) + . '
' . t('The website default timezone is @date_default_timezone (@date_default_code) which is offset from GMT by @date_default_offset hours. This timezone can be changed by admin users with the appropriate access.', $t_options) + . '
'; if (variable_get('configurable_timezones', 1)) { $output .= '' . t('Your local time is @localtime (@daylight_saving). You can change this via your user account.', $t_options) . '
'; @@ -512,16 +527,15 @@ function theme_scheduler_timecheck($variables) { * admin/content/scheduler. It is also shown on the 'My account' page * user/{uid}/scheduler if the user has permission to schedule content. * - * @param string + * @param string $show * 'user_only' if viewing a user page, NULL otherwise. - * - * @param int + * @param int $uid * The uid when viewing a user page, NULL otherwise. * * @return array * A render array for a page containing a list of nodes. */ -function scheduler_list() { +function scheduler_list($show, $uid) { $header = array( array( 'data' => t('Title'), @@ -570,11 +584,10 @@ function scheduler_list() { // the nodes owned by that user. If the current user is viewing another users' // profile and they do not have 'administer nodes' permission then it won't // even get this far, as the tab will not be accessible. - $args = func_get_args(); - if ($args[0] == 'user_only') { - $query->condition('n.uid', $args[1], '='); + if ($show == 'user_only') { + $query->condition('n.uid', $uid, '='); // Get user account for use later. - $user = user_load($args[1]); + $user = user_load($uid); } $query = $query->extend('TableSort')->orderByHeader($header); $result = $query->execute(); @@ -627,7 +640,7 @@ function scheduler_list() { '#theme' => 'table', '#header' => $header, '#rows' => $rows, - '#empty' => ($args[0] == 'user_only') ? t('There are no scheduled nodes for @username.', array('@username' => $user->name)) : t('There are no scheduled nodes.'), + '#empty' => ($show == 'user_only') ? t('There are no scheduled nodes for @username.', array('@username' => $user->name)) : t('There are no scheduled nodes.'), ); return $build; } diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.api.php b/frontend/drupal/sites/all/modules/scheduler/scheduler.api.php index db2b96df1..e5c91404f 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.api.php +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.api.php @@ -11,13 +11,15 @@ */ /** - * Modules can implement hook_scheduler_api() to react to the Scheduler - * operation being done on a node. The hook is invoked during cron processing - * and also from scheduler_node_presave(). + * Allow modules to react to Scheduler node operations. + * + * Modules can implement hook_scheduler_api() to react to the Scheduler action + * being performed on a node. This hook is invoked during cron processing for + * 'pre_publish', 'publish', 'pre_unpublish' and 'unpublish' and from + * scheduler_node_presave() for 'publish_immediately'. * * @param object $node * The scheduled node object that is being processed. - * * @param string $action * $action determines what is being done to the node. The value will be * 'pre_publish', 'publish', 'publish_immediately', 'pre_unpublish' @@ -25,27 +27,35 @@ */ function hook_scheduler_api($node, $action) { switch ($action) { - case 'pre_publish' : + case 'pre_publish': break; - case 'publish' : + + case 'publish': break; - case 'publish_immediately' : + + case 'publish_immediately': break; - case 'pre_unpublish' : + + case 'pre_unpublish': break; - case 'unpublish' : + + case 'unpublish': break; + default: } } /** + * Allow modules to add node ids to the list being processed. + * * Modules can implement hook_scheduler_nid_list() to add more node ids into the * list to be processed in the current cron run. This hook is invoked during - * cron runs only. + * cron runs only. It is maintained for backwards compatibility but has been + * superceded by hook_scheduler_nid_list_alter(), which has more functionality. * * @param string $action - * $action determines what is being done to the node. + * Indicates what is being done to the node. * The value will be 'publish' or 'unpublish'. * * @return array @@ -58,32 +68,35 @@ function hook_scheduler_nid_list($action) { } /** + * Allows modules to add or remove node ids from the list to be processed. + * * Modules can implement hook_scheduler_nid_list_alter() to add or remove node * ids from the list to be processed in the current cron run. This hook is * invoked during cron runs only. * * @param array $nids - * $nids is an array of node ids being processed. - * + * Array of node ids being processed. * @param string $action - * $action determines what is being done to the node. + * Indicates what is being done to the node. * The value will be 'publish' or 'unpublish'. * * @return array * The full array of node ids to process, adjusted as required. */ -function hook_scheduler_nid_list_alter(&$nids, $action) { +function hook_scheduler_nid_list_alter(array &$nids, $action) { + // Do some processing to add or removed node ids from the $nids array. return $nids; } /** - * Modules can implement hook_scheduler_allow_publishing() to prevent - * publication of a scheduled node. + * Allows modules to prevent publication of a scheduled node. * - * The node can 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. + * Modules can implement hook_scheduler_allow_publishing() to prevent publishing + * of a scheduled node. The node can be scheduled for publishing as usual, 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. * * @param object $node * The scheduled node that is about to be published. @@ -110,13 +123,14 @@ function hook_scheduler_allow_publishing($node) { } /** - * Modules can implement hook_scheduler_allow_unpublishing() to prevent - * unpublication of a scheduled node. + * Allows modules to prevent unpublication of a scheduled node. * - * The node can 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. + * Modules can implement hook_scheduler_allow_unpublishing() to prevent + * unpublishing of a scheduled node. The node can be scheduled for unpublishing + * as usual, 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. * * @param object $node * The scheduled node that is about to be unpublished. diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.cron.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.cron.inc index 7b8543af7..8e0f34e10 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.cron.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.cron.inc @@ -42,15 +42,16 @@ function _scheduler_publish() { foreach ($nids as $nid) { $n = node_load($nid); + // Check that scheduled publishing is (still) enabled for this type. + if (!variable_get('scheduler_publish_enable_' . $n->type, 0)) { + continue; + } + // Check that other modules allow the action on this node. if (!_scheduler_allow($n, $action)) { continue; } - // Invoke Scheduler API for modules to react before the node is published. - // @todo For D8 move the 'pre' call to here. - // See https://www.drupal.org/node/2311273 - // Update timestamps. $n->changed = $n->publish_on; $old_creation_date = $n->created; @@ -74,7 +75,8 @@ function _scheduler_publish() { // Invoke scheduler API to allow modules to alter the node before it is // saved. - // @todo For D8, remove this from here. + // For 8.x this 'pre' call is moved up to just before 'Update timestamps'. + // See https://www.drupal.org/node/2311273 _scheduler_scheduler_api($n, 'pre_' . $action); // Use the actions system to publish the node. @@ -131,6 +133,11 @@ function _scheduler_unpublish() { foreach ($nids as $nid) { $n = node_load($nid); + // Check that scheduled publishing is (still) enabled for this type. + if (!variable_get('scheduler_unpublish_enable_' . $n->type, 0)) { + continue; + } + // Check that other modules allow the action on this node. if (!_scheduler_allow($n, $action)) { continue; @@ -143,10 +150,6 @@ function _scheduler_unpublish() { continue; } - // Invoke scheduler API for modules to react before the node is unpublished. - // @todo For D8, move the 'pre' call to here. - // See https://www.drupal.org/node/2311273 - // Update timestamps. $old_change_date = $n->changed; $n->changed = $n->unpublish_on; @@ -167,7 +170,8 @@ function _scheduler_unpublish() { // Invoke scheduler API to allow modules to alter the node before it is // saved. - // @todo For D8, remove this from here. + // For 8.x this 'pre' call is moved up to just before 'Update timestamps'. + // See https://www.drupal.org/node/2311273 _scheduler_scheduler_api($n, 'pre_' . $action); // Use the actions system to unpublish the node. diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.drush.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.drush.inc new file mode 100644 index 000000000..4a6bcabba --- /dev/null +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.drush.inc @@ -0,0 +1,39 @@ + 'Lighweight cron to process the Scheduler module tasks.', + 'core' => array('7'), + 'aliases' => array('sch-cron'), + 'category' => 'scheduler', + 'options' => array( + 'nomsg' => 'to avoid the "cron completed" message being written to the terminal.', + ), + ); + + return $items; +} + +/** + * Run lighweight scheduler cron. + */ +function drush_scheduler_cron() { + // Load the cron functions file then run scheduler cron. + module_load_include('inc', 'scheduler', 'scheduler.cron'); + // Running the lightweight cron function _scheduler_run_cron() gives the dblog + // rows but also kills drush. If we wanted to use that function, we can check + // function_exists('drush_main') to do conditional code. + scheduler_cron(); + $nomsg = drush_get_option('nomsg', NULL); + $nomsg ? NULL : drupal_set_message(t('Scheduler lightweight cron completed.')); +} diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.edit.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.edit.inc index 5772075cc..767168354 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.edit.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.edit.inc @@ -27,7 +27,7 @@ function _scheduler_form_alter(&$form, $form_state) { // If this is a preview then get the values from the form, not the // database. if (isset($form_state['values']['op']) && $form_state['values']['op'] == t('Preview')) { - $defaults = new StdClass; + $defaults = new stdClass(); $defaults->publish_on = $publishing_enabled ? $form_state['values']['publish_on'] : NULL; $defaults->unpublish_on = $unpublishing_enabled ? $form_state['values']['unpublish_on'] : NULL; } @@ -40,7 +40,7 @@ function _scheduler_form_alter(&$form, $form_state) { } else { // Initialise standard values. - $defaults = new StdClass; + $defaults = new stdClass(); // Respect presets added by functions like // scheduler_field_attach_prepare_translation_alter(). $defaults->publish_on = isset($node->publish_on) ? $node->publish_on : NULL; @@ -86,7 +86,7 @@ function _scheduler_form_alter(&$form, $form_state) { // Add Scheduler settings to Vertical Tabs group and attach the javascript. if ($use_vertical_tabs) { $form['scheduler_settings']['#group'] = 'additional_settings'; - $form['scheduler_settings']['#attached']['js'][] = drupal_get_path('module', 'scheduler') . '/scheduler_vertical_tabs.js'; + $form['scheduler_settings']['#attached']['js'][] = _scheduler_get_vertical_tabs_js(); } $extra_info = variable_get('scheduler_extra_info', ''); @@ -178,12 +178,18 @@ function _scheduler_form_alter(&$form, $form_state) { /** * Callback function for the Scheduler date entry elements. */ -function scheduler_date_value_callback(&$element, $input = FALSE, &$form_state) { +function scheduler_date_value_callback(&$element, $input, &$form_state) { // When processing a delete operation the user should not be forced to enter a // date. Hence set the scheduler date element's #required attribute to FALSE. - // Test the input operation against $form_state['values']['delete'] as this - // will match the value of the Delete button even if translated. - if (isset($form_state['input']['op']) && isset($form_state['values']['delete']) && $form_state['input']['op'] == $form_state['values']['delete']) { + // Test the 'triggering_element' value against $form_state['values']['delete'] + // as this will match Delete button even if the text is translated. + // @see https://www.drupal.org/node/1614880 + if (isset($form_state['triggering_element']['#value']) && isset($form_state['values']['delete']) && $form_state['triggering_element']['#value'] == $form_state['values']['delete']) { + // At some point between October 2013 and August 2017 this code became + // unnecessary. Nodes can now be deleted when 'required' is set and when no + // date is entered, even without setting #required to FALSE here. It may be + // due to a core change between 7.23 and 7.56? Leave this line as-is just + // for safety. $element['#required'] = FALSE; } // If using date popup then process the callback that would have been done had diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.info b/frontend/drupal/sites/all/modules/scheduler/scheduler.info index ebf25f557..e3d3a0586 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.info +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.info @@ -2,17 +2,14 @@ name = Scheduler description = This module allows nodes to be published and unpublished on specified dates and time. core = 7.x configure = admin/config/content/scheduler -files[] = scheduler.install -files[] = scheduler.module -files[] = scheduler.test -files[] = scheduler.views.inc files[] = scheduler_handler_field_scheduler_countdown.inc +files[] = tests/scheduler.test files[] = tests/scheduler_api.test test_dependencies[] = date +test_dependencies[] = rules -; Information added by Drupal.org packaging script on 2016-07-24 -version = "7.x-1.5" +; Information added by Drupal.org packaging script on 2020-09-15 +version = "7.x-1.6" core = "7.x" project = "scheduler" -datestamp = "1469372941" - +datestamp = "1600171819" diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.install b/frontend/drupal/sites/all/modules/scheduler/scheduler.install index a9f4812b6..427e61f80 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.install +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.install @@ -106,7 +106,7 @@ function scheduler_update_7100() { // Grant these roles the 'view scheduled content' permission. if ($roles_to_update = $query->execute()->fetchCol()) { - foreach ($roles_to_update as $rid ) { + foreach ($roles_to_update as $rid) { // Use db_merge not db_insert in case the role already has the permission. $query = db_merge('role_permission'); $query->key(array( @@ -123,7 +123,7 @@ function scheduler_update_7100() { } } - return format_plural(sizeof($roles_to_update), '1 role updated with view scheduled content permission.', '@count roles updated with view scheduled content permission.'); + return format_plural(count($roles_to_update), '1 role updated with view scheduled content permission.', '@count roles updated with view scheduled content permission.'); } /** @@ -145,11 +145,12 @@ function scheduler_update_7101() { if ($nids_to_delete = $query->execute()->fetchCol()) { db_delete('scheduler')->condition('nid', $nids_to_delete, 'IN')->execute(); } - return format_plural(sizeof($nids_to_delete), '1 obsolete row deleted from scheduler table.', '@count obsolete rows deleted from scheduler table.'); + return format_plural(count($nids_to_delete), '1 obsolete row deleted from scheduler table.', '@count obsolete rows deleted from scheduler table.'); } /** * Function scheduler_update_7102() removed and replaced by 7103. + * * @see http://www.drupal.org/node/2706119 */ @@ -158,9 +159,9 @@ function scheduler_update_7101() { */ function scheduler_update_7103() { // Change all values of 'schedule (un)publishing of nodes' to the cleaner - // 'schedule publishing of nodes' + // 'schedule publishing of nodes'. // @see http://www.drupal.org/node/2538002 - + // // Updates done in two stages to avoid integrity constraint violation. First // select all role ids which already have the new permission value. $query = db_select('role_permission', 'rp') diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.module b/frontend/drupal/sites/all/modules/scheduler/scheduler.module index d0833bedf..ada37b924 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.module +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.module @@ -232,16 +232,16 @@ function scheduler_form_alter(&$form, $form_state) { * unpublishing, by implementing hook_scheduler_allow_publishing() or * hook_scheduler_allow_unpublishing(). * - * @see hook_scheduler_allow_publishing() - * @see hook_scheduler_allow_unpublishing() - * - * @param stdClass $node + * @param object $node * The node object on which the action is to be performed. * @param string $action * The action that needs to be checked. Can be 'publish' or 'unpublish'. * * @return bool * TRUE if the action is allowed, FALSE if not. + * + * @see hook_scheduler_allow_publishing() + * @see hook_scheduler_allow_unpublishing() */ function _scheduler_allow($node, $action) { // Default to TRUE. @@ -282,7 +282,8 @@ function _scheduler_strtotime($str) { // date_limit_format() to derive the format of the returned string value. $granularity = date_format_order($date_format); $date_format = date_limit_format(DATE_FORMAT_DATETIME, $granularity); - $date_only_format = date_limit_format(DATE_FORMAT_DATETIME, array('day', 'month', 'year')); + $granularity = array('day', 'month', 'year'); + $date_only_format = date_limit_format(DATE_FORMAT_DATETIME, $granularity); } $str = trim(preg_replace('/\s+/', ' ', $str)); $time = _scheduler_strptime($str, $date_format); @@ -340,6 +341,8 @@ function _scheduler_strptime($date, $format) { // Build a regex pattern for each element allowed in the date and time format. $date_entities_and_replacements = array( // Date elements, one for each character in SCHEDULER_DATE_LETTERS. + // Inline comments on each row are useful here, so 'ignore' for standards. + // @codingStandardsIgnoreStart 'd' => '(\d{2})', // Day of the month with leading zero. 'j' => '(\d{1,2})', // Day of the month without leading zero. 'm' => '(\d{2})', // Month number with leading zero. @@ -358,6 +361,7 @@ function _scheduler_strptime($date, $format) { 's' => '(\d{2})', // Seconds with leading zero. 'a' => '([ap]m)', // Lower case meridian. 'A' => '([AP]M)', // Upper case meridian. + // @codingStandardsIgnoreEnd ); $date_entities = array_keys($date_entities_and_replacements); $date_regex_replacements = array_values($date_entities_and_replacements); @@ -370,7 +374,15 @@ function _scheduler_strptime($date, $format) { return FALSE; } - $results = array('day' => 0, 'month' => 0, 'year' => 0, 'hour' => 0, 'minute' => 0, 'second' => 0, 'meridiem' => NULL); + $results = array( + 'day' => 0, + 'month' => 0, + 'year' => 0, + 'hour' => 0, + 'minute' => 0, + 'second' => 0, + 'meridiem' => NULL, + ); $index = 1; foreach ($entity_matches[1] as $entity) { $value = intval($value_matches[$index]); @@ -467,7 +479,7 @@ function scheduler_node_load($nodes, $types) { /** * Implements hook_node_view(). */ -function scheduler_node_view($node, $view_mode = 'full', $langcode) { +function scheduler_node_view($node, $view_mode, $langcode) { // 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. @@ -487,7 +499,7 @@ function scheduler_node_validate($node, $form, &$form_state) { // Use !== FALSE because the key returned will be 0. // @see https://www.drupal.org/node/2723929 if (!empty($form_state['triggering_element']['#submit']) && array_search('node_form_delete_submit', $form_state['triggering_element']['#submit']) !== FALSE) { - if ($errors = form_get_errors()) { + if (form_get_errors()) { // If there are already errors (from date_popup) remove them to allow // deletion to proceed. form_clear_error(); @@ -504,13 +516,13 @@ function scheduler_node_validate($node, $form, &$form_state) { // passed as an array this means we are using the Date Popup module and a // validation error has occurred. In this case we should skip validation as // it is being handled by Date Popup. - $date_format = variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT); + $args = array('%time' => format_date(REQUEST_TIME, 'custom', $date_format)); if (!empty($node->publish_on) && !is_numeric($node->publish_on) && !is_array($node->publish_on)) { $publishtime = _scheduler_strtotime($node->publish_on); if ($publishtime === FALSE) { - form_set_error('publish_on', t("The 'publish on' value does not match the expected format of %time", array('%time' => format_date(REQUEST_TIME, 'custom', $date_format)))); + form_set_error('publish_on', t("The 'publish on' value does not match the expected format of %time", $args)); } elseif ($publishtime && variable_get('scheduler_publish_past_date_' . $node->type, 'error') == 'error' && $publishtime < REQUEST_TIME) { form_set_error('publish_on', t("The 'publish on' date must be in the future")); @@ -520,7 +532,7 @@ function scheduler_node_validate($node, $form, &$form_state) { if (!empty($node->unpublish_on) && !is_numeric($node->unpublish_on) && !is_array($node->unpublish_on)) { $unpublishtime = _scheduler_strtotime($node->unpublish_on); if ($unpublishtime === FALSE) { - form_set_error('unpublish_on', t("The 'unpublish on' value does not match the expected format of %time", array('%time' => format_date(REQUEST_TIME, 'custom', $date_format)))); + form_set_error('unpublish_on', t("The 'unpublish on' value does not match the expected format of %time", $args)); } elseif ($unpublishtime && $unpublishtime < REQUEST_TIME) { form_set_error('unpublish_on', t("The 'unpublish on' date must be in the future")); @@ -533,7 +545,10 @@ function scheduler_node_validate($node, $form, &$form_state) { // The unpublish-on 'required' form attribute may not be set, but in some // cases a value must still be entered. - if (variable_get('scheduler_unpublish_required_' . $node->type) && empty($node->unpublish_on)) { + if (variable_get('scheduler_unpublish_enable_' . $node->type) + && variable_get('scheduler_unpublish_required_' . $node->type) + && empty($node->unpublish_on) + ) { // ... when also setting a publish-on date. if (!empty($node->publish_on)) { form_set_error('unpublish_on', t("If you set a 'publish-on' date then you must also set an 'unpublish-on' date.")); @@ -585,7 +600,9 @@ function scheduler_node_presave($node) { // message themselves explaining why publication is denied. if ($publication_allowed) { $date_format = variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT); - drupal_set_message(t('This post is unpublished and will be published @publish_time.', array('@publish_time' => format_date($node->publish_on, 'custom', $date_format))), 'status', FALSE); + drupal_set_message(t('This post is unpublished and will be published @publish_time.', array( + '@publish_time' => format_date($node->publish_on, 'custom', $date_format), + )), 'status', FALSE); } } } @@ -704,6 +721,7 @@ function scheduler_cron() { if (variable_get('scheduler_cache_clear_all', 0) && ($nodes_published || $nodes_unpublished)) { // Clear the page and block caches. cache_clear_all(); + watchdog('scheduler', 'Page and block caches cleared.', array(), WATCHDOG_NOTICE, l(t('settings'), 'admin/config/content/scheduler')); } // Reset the static scheduler_cron flag. @@ -882,8 +900,8 @@ function scheduler_feeds_set_target($source, $entity, $target, $value, $mapping) * Implements hook_ctools_plugin_directory(). */ function scheduler_ctools_plugin_directory($owner, $plugin_type) { - // Declare a form pane (panels content type) for use in ctools and page - // manager. This allows the Scheduler fieldset to be placed in a panel. + // Declare a form pane (panels content type) for use in ctools and page + // manager. This allows the Scheduler fieldset to be placed in a panel. if ($owner == 'ctools' && $plugin_type == 'content_types') { return 'plugins/content_types'; } @@ -942,3 +960,12 @@ function scheduler_date_popup_pre_validate_alter($element, $form_state, &$input) $input['time'] = format_date($default_time, 'custom', variable_get('scheduler_time_only_format', SCHEDULER_TIME_ONLY_FORMAT)); } } + +/** + * Internal function to add js 'theme' setting and return the js filename. + */ +function _scheduler_get_vertical_tabs_js() { + global $theme; + drupal_add_js(array('scheduler_vertical_tabs' => array('theme' => $theme)), array('type' => 'setting')); + return drupal_get_path('module', 'scheduler') . "/scheduler_vertical_tabs.js"; +} diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.rules.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.rules.inc index 4e53ebaed..530852647 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.rules.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.rules.inc @@ -7,11 +7,11 @@ /** * Implements hook_rules_event_info(). + * + * This hook function defines four Scheduler events which can be used by Rules + * to trigger other actions. */ function scheduler_rules_event_info() { - // This hook function defines four Scheduler events which can be used by Rules - // to trigger other actions. - // Create an array of variables, as these are the same for each of the events. $variables = array( 'node' => array( @@ -118,9 +118,9 @@ function scheduler_rules_action_info() { /** * Set the publish_on date for the node. * - * @param $node + * @param object $node * The node object to be scheduled for publishing. - * @param $date + * @param int $date * The date for publishing, a unix timestamp integer. */ function scheduler_set_publish_date_action($node, $date) { @@ -142,9 +142,9 @@ function scheduler_set_publish_date_action($node, $date) { /** * Set the unpublish_on date for the node. * - * @param $node + * @param object $node * The node object to be scheduled for unpublishing. - * @param $date + * @param int $date * The date for unpublishing, a unix timestamp integer. */ function scheduler_set_unpublish_date_action($node, $date) { @@ -162,7 +162,7 @@ function scheduler_set_unpublish_date_action($node, $date) { /** * Remove the publish_on date for the node. * - * @param $node + * @param object $node * The node object from which to remove the publish_on date. */ function scheduler_remove_publish_date_action($node) { @@ -180,7 +180,7 @@ function scheduler_remove_publish_date_action($node) { /** * Remove the unpublish_on date for the node. * - * @param $node + * @param object $node * The node object from which to remove the unpublish_on date. */ function scheduler_remove_unpublish_date_action($node) { @@ -213,19 +213,23 @@ function scheduler_rules_condition_info() { // 1. Condition to check if publishing is enabled for the content type. $conditions['scheduler_condition_publishing_is_enabled'] = array( - 'label' => t('Scheduled publishing is enabled for this content type')) + $default; + 'label' => t('Scheduled publishing is enabled for this content type'), + ) + $default; // 2. Condition to check if unpublishing is enabled for the content type. $conditions['scheduler_condition_unpublishing_is_enabled'] = array( - 'label' => t('Scheduled unpublishing is enabled for this content type')) + $default; + 'label' => t('Scheduled unpublishing is enabled for this content type'), + ) + $default; // 3. Condition to check if the node is scheduled for publishing. $conditions['scheduler_condition_node_is_scheduled_for_publishing'] = array( - 'label' => t('The node is scheduled for publishing')) + $default; + 'label' => t('The node is scheduled for publishing'), + ) + $default; // 4. Condition to check if the node is scheduled for unpublishing. $conditions['scheduler_condition_node_is_scheduled_for_unpublishing'] = array( - 'label' => t('The node is scheduled for unpublishing')) + $default; + 'label' => t('The node is scheduled for unpublishing'), + ) + $default; return $conditions; } @@ -233,9 +237,10 @@ function scheduler_rules_condition_info() { /** * Determines whether scheduled publishing is enabled for this node type. * - * @param $node - * A node object. - * @return + * @param object $node + * The node to check. + * + * @return bool * TRUE if scheduled publishing is enabled for the node type, FALSE if not. */ function scheduler_condition_publishing_is_enabled($node) { @@ -245,9 +250,10 @@ function scheduler_condition_publishing_is_enabled($node) { /** * Determines whether scheduled unpublishing is enabled for this node type. * - * @param $node - * A node object. - * @return + * @param object $node + * The node to check. + * + * @return bool * TRUE if scheduled unpublishing is enabled for the node type, FALSE if not. */ function scheduler_condition_unpublishing_is_enabled($node) { @@ -257,9 +263,10 @@ function scheduler_condition_unpublishing_is_enabled($node) { /** * Determines whether a node is scheduled for publishing. * - * @param $node - * A node object. - * @return + * @param object $node + * The node to check. + * + * @return bool * TRUE if the node is scheduled for publishing, FALSE if not. */ function scheduler_condition_node_is_scheduled_for_publishing($node) { @@ -269,9 +276,10 @@ function scheduler_condition_node_is_scheduled_for_publishing($node) { /** * Determines whether a node is scheduled for unpublishing. * - * @param $node - * A node object. - * @return + * @param object $node + * The node to check. + * + * @return bool * TRUE if the node is scheduled for unpublishing, FALSE if not. */ function scheduler_condition_node_is_scheduled_for_unpublishing($node) { diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.rules_defaults.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.rules_defaults.inc index 9aae77e6a..4badd202b 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.rules_defaults.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.rules_defaults.inc @@ -8,14 +8,13 @@ /** * Implements hook_default_rules_configuration(). * - * @return - * An array of rules configurations with the configuration names as keys. + * This function returns an array of rules configurations with the configuration + * names as keys. Two reaction rules and four components are provided. */ function scheduler_default_rules_configuration() { // 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. - // 1. Reaction rule to send an email when Scheduler publishes content. $rule = rules_reaction_rule(); $rule->label = t('Send e-mail when content is published by Scheduler'); @@ -48,7 +47,6 @@ function scheduler_default_rules_configuration() { // Define four components which will be available in the 'Components' tab in // Rules admin. These are also available in Views Bulk Operations, to allow // a user to set or remove scheduling dates in bulk. - // 1. Component to set the publishing date on a node. $rule = rule(array( 'scheduler_node' => array( diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.tokens.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.tokens.inc index 1523e4a46..13fad755f 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.tokens.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.tokens.inc @@ -31,6 +31,12 @@ function scheduler_tokens($type, $tokens, array $data = array(), array $options $replacements = array(); if ($type == 'node' && !empty($data['node'])) { + + // Initialise the two field variables. The syntax ${$field} = NULL inside + // the foreach loop does work but we get warnings for Drupal Coding Practice + // that the variables are not initialised, hence do it simply here instead. + $publish_on = $unpublish_on = NULL; + // Usually the tokens are generated on saved node data, where the scheduler // fields are numeric timestamps. However, if the tokens are required during // the process of saving a node before hook_node_presave() has been executed @@ -38,14 +44,15 @@ function scheduler_tokens($type, $tokens, array $data = array(), array $options // @see https://www.drupal.org/node/2750467 $node = $data['node']; foreach (array('publish_on', 'unpublish_on') as $field) { - if (empty($node->$field)) { - ${$field} = NULL; - } - elseif (is_numeric($node->$field)) { - ${$field} = $node->$field; - } - else { - ${$field} = _scheduler_strtotime($node->$field); + if (isset($node->$field)) { + if (is_numeric($node->$field)) { + // We want the numeric value. + ${$field} = $node->$field; + } + elseif (!empty($node->$field)) { + // Convert the text to a numeric value. + ${$field} = _scheduler_strtotime($node->$field); + } } } @@ -58,11 +65,13 @@ function scheduler_tokens($type, $tokens, array $data = array(), array $options $replacements[$original] = format_date($publish_on, 'medium', '', NULL, $language_code); } break; + case 'scheduler-unpublish': if (!empty($unpublish_on)) { $replacements[$original] = format_date($unpublish_on, 'medium', '', NULL, $language_code); } break; + default: } } diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler.views.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler.views.inc index 3aa0e4c12..bd705638a 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler.views.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler.views.inc @@ -28,10 +28,11 @@ function scheduler_views_data() { // Define how the node table is linked to the Scheduler table. This is needed // when 'scheduler' is the base table, to give access to the node fields. + // Type = 'inner' will exclude any bad data rows in the scheduler table. $tables['node']['table']['join']['scheduler'] = array( 'left_field' => 'nid', 'field' => 'nid', - 'type' => 'inner', // to exclude any bad data rows in the scheduler table. + 'type' => 'inner', ); // Describe the two fields in the Scheduler database table. @@ -72,9 +73,9 @@ function scheduler_views_data() { // Describe the two extra derived fields provided for Views. $tables['scheduler']['publish_countdown'] = array( 'title' => t('Publish countdown'), - 'help' => t('Time until the article will be automatically published'), + 'help' => t('Time until the content will be published'), 'field' => array( - 'handler' => 'scheduler_handler_field_scheduler_countdown', + 'handler' => 'SchedulerHandlerFieldSchedulerCountdown', 'click sortable' => FALSE, 'timestamp_field' => 'publish_on', ), @@ -82,9 +83,9 @@ function scheduler_views_data() { $tables['scheduler']['unpublish_countdown'] = array( 'title' => t('Unpublish countdown'), - 'help' => t('Time until the article will be automatically unpublished'), + 'help' => t('Time until the content will be unpublished'), 'field' => array( - 'handler' => 'scheduler_handler_field_scheduler_countdown', + 'handler' => 'SchedulerHandlerFieldSchedulerCountdown', 'click sortable' => FALSE, 'timestamp_field' => 'unpublish_on', ), @@ -92,16 +93,3 @@ function scheduler_views_data() { return $tables; } - -/** - * Implements hook_views_handlers(). - */ -function scheduler_views_handlers() { - return array( - 'handlers' => array( - 'scheduler_handler_field_scheduler_countdown' => array( - 'parent' => 'views_handler_field', - ), - ), - ); -} diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler_handler_field_scheduler_countdown.inc b/frontend/drupal/sites/all/modules/scheduler/scheduler_handler_field_scheduler_countdown.inc index 3a884a93b..8ddf6edc5 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler_handler_field_scheduler_countdown.inc +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler_handler_field_scheduler_countdown.inc @@ -10,16 +10,19 @@ /** * Field handler to display a countdown until a scheduled action. * - * Defines class scheduler_handler_field_scheduler_countdown. + * Defines class SchedulerHandlerFieldSchedulerCountdown. + * The structure is [module]_handler_[type]_[tablename]_[fieldname] + * However, for standards compliance, CamelCase is now used where possible. * * @see http://www.ericschaefer.org/blog/2011/01/09/custom-field-handlers-for-views-2-drupal */ -class scheduler_handler_field_scheduler_countdown extends views_handler_field { - CONST SECOND_SCALE = 1; - CONST MINUTE_SCALE = 60; - CONST HOUR_SCALE = 3600; - CONST DAY_SCALE = 86400; - CONST WEEK_SCALE = 604800; +class SchedulerHandlerFieldSchedulerCountdown extends views_handler_field { + + const SECOND_SCALE = 1; + const MINUTE_SCALE = 60; + const HOUR_SCALE = 3600; + const DAY_SCALE = 86400; + const WEEK_SCALE = 604800; /** * Add the timestamp_field into the SQL query. @@ -28,7 +31,7 @@ class scheduler_handler_field_scheduler_countdown extends views_handler_field { * of seconds from now until publishing. If publish_on is in the past then * NULL is returned. */ - function query() { + public function query() { $this->ensure_my_table(); $this->node_table = $this->query->ensure_table('node', $this->relationship); $time_field = $this->definition['timestamp_field']; @@ -38,10 +41,17 @@ class scheduler_handler_field_scheduler_countdown extends views_handler_field { /** * Define our display options and provide defaults. * + * The name of this function fails the coding standard sniff + * Drupal.NamingConventions.ValidFunctionName.ScopeNotCamelCaps + * However, the name is defined in Views module and has to match that, hence + * we need to ignore this fault and not report it. + * * @return array * An associative array containing the options. */ - function option_definition() { + // @codingStandardsIgnoreStart + public function option_definition() { + // @codingStandardsIgnoreEnd $options = parent::option_definition(); $options['countdown_display'] = array('default' => 'smart'); $options['units_display'] = array('default' => 'long'); @@ -51,7 +61,9 @@ class scheduler_handler_field_scheduler_countdown extends views_handler_field { /** * Defines the form for the user to select the display options. */ - function options_form(&$form, &$form_state) { + // @codingStandardsIgnoreStart + public function options_form(&$form, &$form_state) { + // @codingStandardsIgnoreEnd parent::options_form($form, $form_state); $form['countdown_display'] = array( '#title' => t('Display countdown as'), @@ -79,17 +91,19 @@ class scheduler_handler_field_scheduler_countdown extends views_handler_field { } /** - * Callback function to keep only the array scale values which are smaller - * than the countdown value being displayed. + * Callback function for array_filter. + * + * Keep only the array scale values which are smaller than the countdown + * value being displayed. */ - function scale_filter_callback($array_value) { + public function scaleFilterCallback($array_value) { return ($this->raw_value >= $array_value); } /** * Renders the countdown value in the units required. */ - function render($values) { + public function render($values) { $countdown_display = $this->options['countdown_display']; $this->raw_value = $values->{$this->field_alias}; @@ -102,7 +116,7 @@ class scheduler_handler_field_scheduler_countdown extends views_handler_field { ); // If the field has been set to 'Smart', determine the right timescale. if ($countdown_display == 'smart') { - $scales = array_filter($scales, array($this, 'scale_filter_callback')); + $scales = array_filter($scales, array($this, 'scaleFilterCallback')); $scale = empty($scales) ? self::SECOND_SCALE : reset($scales); } // Otherwise use the fixed display requested. diff --git a/frontend/drupal/sites/all/modules/scheduler/scheduler_vertical_tabs.js b/frontend/drupal/sites/all/modules/scheduler/scheduler_vertical_tabs.js index f04334864..96228e544 100644 --- a/frontend/drupal/sites/all/modules/scheduler/scheduler_vertical_tabs.js +++ b/frontend/drupal/sites/all/modules/scheduler/scheduler_vertical_tabs.js @@ -1,6 +1,6 @@ /** * @file - * jQuery to provide summary information inside vertical tabs. + * JQuery to provide summary information inside vertical tabs. */ (function ($) { @@ -20,7 +20,7 @@ Drupal.behaviors.scheduler_settings = { $('div.vertical-tabs').addClass(theme); // Provide summary when editing a node. - $('fieldset#edit-scheduler-settings', context).drupalSetSummary(function(context) { + $('fieldset#edit-scheduler-settings', context).drupalSetSummary(function (context) { var vals = []; if ($('#edit-publish-on').val() || $('#edit-publish-on-datepicker-popup-0').val()) { vals.push(Drupal.t('Scheduled for publishing')); @@ -35,7 +35,7 @@ Drupal.behaviors.scheduler_settings = { }); // Provide summary during content type configuration. - $('fieldset#edit-scheduler', context).drupalSetSummary(function(context) { + $('fieldset#edit-scheduler', context).drupalSetSummary(function (context) { var vals = []; if ($('#edit-scheduler-publish-enable', context).is(':checked')) { vals.push(Drupal.t('Publishing enabled')); diff --git a/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.info b/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.info new file mode 100644 index 000000000..d49909f0c --- /dev/null +++ b/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.info @@ -0,0 +1,13 @@ +name = "Scheduler tests" +description = "Support module for Scheduler related testing." +package = Testing +core = 7.x +hidden = TRUE +dependencies[] = list +dependencies[] = options + +; Information added by Drupal.org packaging script on 2020-09-15 +version = "7.x-1.6" +core = "7.x" +project = "scheduler" +datestamp = "1600171819" diff --git a/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.install b/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.install new file mode 100644 index 000000000..d8e3c22f7 --- /dev/null +++ b/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.install @@ -0,0 +1,53 @@ + 'field_scheduler_test_approved', + 'type' => 'list_integer', + 'entity_types' => array('node'), + 'cardinality' => 2, + 'settings' => array( + 'allowed_values' => array( + 1 => $t('Approved for publication by the CEO'), + ), + ), + ); + $field = field_create_field($field); + } + + $instance = field_info_instance('node', 'field_scheduler_test_approved', 'scheduler_test'); + if (empty($instance)) { + $instance = array( + 'bundle' => 'scheduler_test', + 'display' => array( + 'default' => array('type' => 'list_default'), + 'teaser' => array('type' => 'hidden'), + ), + 'entity_type' => 'node', + 'field_name' => 'field_scheduler_test_approved', + 'label' => 'Approved', + 'widget' => array('type' => 'options_buttons'), + ); + field_create_instance($instance); + } +} diff --git a/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.module b/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.module new file mode 100644 index 000000000..be59bfa38 --- /dev/null +++ b/frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.module @@ -0,0 +1,39 @@ + array( + 'name' => t('Scheduler test'), + 'base' => 'node_content', + 'description' => t('This content type is used to test the Scheduler module.'), + 'has_title' => '1', + 'title_label' => t('Title'), + ), + ); + return $items; +} + +/** + * Implements hook_scheduler_allow_publishing(). + */ +function scheduler_test_scheduler_allow_publishing($node) { + // Only publish nodes that have the 'Approved for publication by the CEO' + // checkbox ticked. + $items = field_get_items('node', $node, 'field_scheduler_test_approved'); + $allowed = !empty($items[0]['value']); + + // If publication is denied then inform the user why. + if (!$allowed) { + drupal_set_message(t('The content will only be published after approval by the CEO.'), 'status', FALSE); + } + + return $allowed; +} diff --git a/frontend/drupal/sites/all/modules/scheduler/tests/scheduler.test b/frontend/drupal/sites/all/modules/scheduler/tests/scheduler.test new file mode 100644 index 000000000..94d81c754 --- /dev/null +++ b/frontend/drupal/sites/all/modules/scheduler/tests/scheduler.test @@ -0,0 +1,1692 @@ +drupalCreateContentType(array('type' => 'page', 'name' => t('Basic page'))); + + // Create an administrator user. + // 'access site reports' is required for admin/reports/dblog. + // 'administer site configuration' is required for admin/reports/status. + // 'access content overview' is required for admin/content. + // 'view scheduled content' is required for admin/content/scheduler. + $this->adminUser = $this->drupalCreateUser(array( + 'access content overview', + 'access site reports', + 'administer nodes', + 'administer scheduler', + 'administer site configuration', + 'create page content', + 'delete own page content', + 'edit own page content', + 'schedule publishing of nodes', + 'view scheduled content', + 'view own unpublished content', + )); + + // Add scheduler functionality to the page node type. + variable_set('scheduler_publish_enable_page', 1); + variable_set('scheduler_unpublish_enable_page', 1); + variable_set('scheduler_field_type', 'textfield'); + } + + /** + * Helper function for testScheduler(). Schedules content and asserts status. + * + * @param array $edit + * Node data, as if it was sent from the edit form. + * @param bool $scheduler_cron_only + * TRUE to only run Scheduler cron, FALSE to run default full Drupal cron. + */ + public function helpTestScheduler(array $edit, $scheduler_cron_only = FALSE) { + // Add a page. + $langcode = LANGUAGE_NONE; + $title = $this->randomName(); + $edit["title"] = $title; + $body = $this->randomName(); + $edit["body[$langcode][0][value]"] = $body; + $this->drupalLogin($this->adminUser); + $this->drupalPost('node/add/page', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($title); + // Show the specific page for an anonymous visitor, then assert that the + // node is correctly published or unpublished. + $this->drupalLogout(); + $this->drupalGet("node/{$node->nid}"); + if (isset($edit['publish_on'])) { + $key = 'publish_on'; + $this->assertResponse(403, t('Node is unpublished')); + } + else { + $key = 'unpublish_on'; + $this->assertText($body, t('Node is published')); + } + // Verify that the scheduler table is not empty. + $this->assertTrue(db_query_range('SELECT 1 FROM {scheduler}', 0, 1)->fetchField(), 'Scheduler table is not empty'); + // Modify the scheduler row to a time far enough in the past because + // scheduler_cron uses REQUEST_TIME and our timestamp has to be before that. + db_update('scheduler')->fields(array($key => time() - 3600))->execute(); + if ($scheduler_cron_only) { + scheduler_cron(); + } + else { + $this->cronRun(); + } + // Verify that the scheduler table is empty. + $this->assertFalse(db_query_range('SELECT 1 FROM {scheduler}', 0, 1)->fetchField(), 'Scheduler table is empty'); + // Show the specific page for an anonymous visitor, then assert that the + // node is correctly published or unpublished. + $this->drupalGet("node/{$node->nid}"); + if (isset($edit['publish_on'])) { + $this->assertText($body, t('Node is published')); + } + else { + $this->assertResponse(403, t('Node is unpublished')); + } + } + + /** + * Simulates the scheduled (un)publication of a node. + * + * @param object $node + * The node to schedule. + * @param string $action + * The action to perform: either 'publish' or 'unpublish'. Defaults to + * 'publish'. + * + * @return object + * The updated node, after scheduled (un)publication. + */ + public function schedule($node, $action = 'publish') { + // Simulate scheduling by setting the (un)publication date in the past and + // running cron. + $node->{$action . '_on'} = strtotime('-1 day', REQUEST_TIME); + node_save($node); + scheduler_cron(); + return node_load($node->nid, NULL, TRUE); + } + + /** + * Check if the latest revision log message of a node matches a given string. + * + * @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. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + public function assertRevisionLogMessage($nid, $value, $message = '', $group = 'Other') { + $log_message = db_select('node_revision', 'r') + ->fields('r', array('log')) + ->condition('nid', $nid) + ->orderBy('vid', 'DESC') + ->range(0, 1) + ->execute() + ->fetchColumn(); + return $this->assertEqual($log_message, $value, $message, $group); + } + + /** + * Check if the number of revisions for a node 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 string $message + * The message to display along with the assertion. + * @param string $group + * The type of assertion - examples are "Browser", "PHP". + * + * @return bool + * TRUE if the assertion succeeded, FALSE otherwise. + */ + public function assertRevisionCount($nid, $value, $message = '', $group = 'Other') { + $count = db_select('node_revision', 'r') + ->fields('r', array('vid')) + ->condition('nid', $nid) + ->countQuery() + ->execute() + ->fetchColumn(); + return $this->assertEqual($count, $value, $message, $group); + } + +} + +/** + * Tests the scheduler interface. + */ +class SchedulerFunctionalTest extends SchedulerTestBase { + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Scheduler functionality', + 'description' => 'Tests the Scheduler functions which do not require other modules.', + 'group' => 'Scheduler', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp('scheduler', 'dblog'); + parent::commonSettings(); + } + + /** + * Tests basic scheduling of content. + * + * @param bool $scheduler_cron_only + * TRUE to only run Scheduler cron, FALSE to run default full Drupal cron. + */ + public function testBasicScheduling($scheduler_cron_only = FALSE) { + // Create node values. Set time to one hour in the future. + $edit = array( + 'publish_on' => format_date(time() + 3600, 'custom', 'Y-m-d H:i:s'), + 'status' => 1, + 'promote' => 1, + ); + // Test scheduled publishing. + $this->helpTestScheduler($edit, $scheduler_cron_only); + // Test scheduled unpublishing. + $edit['unpublish_on'] = $edit['publish_on']; + unset($edit['publish_on']); + $this->helpTestScheduler($edit, $scheduler_cron_only); + } + + /** + * Tests scheduler when not all cron tasks are run during cron. + * + * Verify that we can set variable 'scheduler_cache_clear_all' so the page + * cache is still cleared. + * + * @uses testScheduler() + */ + public function testBasicSchedulingWithOnlySchedulerCron() { + // Cache pages for anonymous users. + variable_set('cache', 1); + // Instruct scheduler to clear caches itself, instead of relying on + // system_cron. + variable_set('scheduler_cache_clear_all', 1); + // Instruct the helper method to run only the scheduler cron. + $scheduler_cron_only = TRUE; + + $this->testBasicScheduling($scheduler_cron_only); + } + + /** + * Test the different options for past publication dates. + */ + public function testPastDates() { + // Log in. + $this->drupalLogin($this->adminUser); + + // Create an unpublished page node. + $node = $this->drupalCreateNode(array('type' => 'page', 'status' => FALSE)); + + // Test the default behavior: an error message should be shown when the user + // enters a publication date that is in the past. + $edit = array( + 'title' => $this->randomName(), + 'publish_on' => format_date(strtotime('-1 day', REQUEST_TIME), 'custom', 'Y-m-d H:i:s'), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->assertText("The 'publish on' date must be in the future", 'An error message is shown when the publication date is in the past and the "error" behavior is chosen.'); + + // Test the 'publish' behavior: the node should be published immediately. + variable_set('scheduler_publish_past_date_page', 'publish'); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->assertNoText("The 'publish on' date must be in the future", 'No error message is shown when the publication date is in the past and the "publish" behavior is chosen.'); + $this->assertText(sprintf('%s %s has been updated.', 'Basic page', $edit['title']), 'The node is saved successfully when the publication date is in the past and the "publish" behavior is chosen.'); + + // Reload the changed node and check that it is published. + $node = node_load($node->nid, NULL, TRUE); + $this->assertTrue($node->status, 'The node has been published immediately when the publication date is in the past and the "publish" behavior is chosen.'); + + // Test the 'schedule' behavior: the node should be unpublished and become + // published on the next cron run. + variable_set('scheduler_publish_past_date_page', 'schedule'); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->assertNoText("The 'publish on' date must be in the future", 'No error message is shown when the publication date is in the past and the "schedule" behavior is chosen.'); + $this->assertText(sprintf('%s %s has been updated.', 'Basic page', check_plain($edit['title'])), 'The node is saved successfully when the publication date is in the past and the "schedule" behavior is chosen.'); + $this->assertText(sprintf('This post is unpublished and will be published %s.', $edit['publish_on']), 'The node is scheduled to be published when the publication date is in the past and the "schedule" behavior is chosen.'); + + // Reload the node and check that it is unpublished but scheduled correctly. + $node = node_load($node->nid, NULL, TRUE); + $this->assertFalse($node->status, 'The node has been unpublished when the publication date is in the past and the "schedule" behavior is chosen.'); + $this->assertEqual(format_date($node->publish_on, 'custom', 'Y-m-d H:i:s'), $edit['publish_on'], 'The node is scheduled for the required date'); + + // Simulate a cron run and check that the node is published. + scheduler_cron(); + $node = node_load($node->nid, NULL, TRUE); + $this->assertTrue($node->status, 'The node with publication date in the past and the "schedule" behavior has now been published by cron.'); + } + + /** + * Tests the creation of new revisions on scheduling. + */ + public function testRevisioning() { + // Create a scheduled node that is not automatically revisioned. + $created = strtotime('-2 day', REQUEST_TIME); + $settings = array( + 'revision' => 0, + 'created' => $created, + ); + $node = $this->drupalCreateNode($settings); + + // First test scheduled publication with revisioning disabled. + $node = $this->schedule($node); + $this->assertRevisionCount($node->nid, 1, 'No new revision was created when a node was published with revisioning disabled.'); + + // Test scheduled unpublication. + $node = $this->schedule($node, 'unpublish'); + $this->assertRevisionCount($node->nid, 1, 'No new revision was created when a node was unpublished with revisioning disabled.'); + + // Enable revisioning. + variable_set('scheduler_publish_revision_page', 1); + variable_set('scheduler_unpublish_revision_page', 1); + + // Test scheduled publication with revisioning enabled. + $node = $this->schedule($node); + $this->assertRevisionCount($node->nid, 2, 'A new revision was created when revisioning is enabled.'); + $expected_message = t('Node published by Scheduler on @now. Previous creation date was @date.', array( + '@now' => format_date(REQUEST_TIME, 'short'), + '@date' => format_date($created, 'short'), + )); + $this->assertRevisionLogMessage($node->nid, $expected_message, 'The correct message was found in the node revision log after scheduled publishing.'); + + // Test scheduled unpublication with revisioning enabled. + $node = $this->schedule($node, 'unpublish'); + $this->assertRevisionCount($node->nid, 3, 'A new revision was created when a node was unpublished with revisioning enabled.'); + $expected_message = t('Node unpublished by Scheduler on @now. Previous change date was @date.', array( + '@now' => format_date(REQUEST_TIME, 'short'), + '@date' => format_date(REQUEST_TIME, 'short'), + )); + $this->assertRevisionLogMessage($node->nid, $expected_message, 'The correct message was found in the node revision log after scheduled unpublishing.'); + } + + /** + * Tests if options can both be displayed as extra fields and vertical tabs. + */ + public function testExtraFields() { + $this->drupalLogin($this->adminUser); + + // Test if the options are shown as vertical tabs by default. + $this->drupalGet('node/add/page'); + $this->assertTrue($this->xpath('//div[contains(@class, "vertical-tabs-panes")]/fieldset[@id = "edit-scheduler-settings"]'), 'By default the scheduler options are shown as a vertical tab.'); + + // Test if the options are shown as extra fields when configured to do so. + variable_set('scheduler_use_vertical_tabs_page', 0); + $this->drupalGet('node/add/page'); + $this->assertFalse($this->xpath('//div[contains(@class, "vertical-tabs-panes")]/fieldset[@id = "edit-scheduler-settings"]'), 'The scheduler options are not shown as a vertical tab when they are configured to show as an extra field.'); + $this->assertTrue($this->xpath('//fieldset[@id = "edit-scheduler-settings" and contains(@class, "collapsed")]'), 'The scheduler options are shown as a collapsed fieldset when they are configured to show as an extra field.'); + + // Test the option to expand the fieldset. + variable_set('scheduler_expand_fieldset_page', 1); + $this->drupalGet('node/add/page'); + $this->assertFalse($this->xpath('//div[contains(@class, "vertical-tabs-panes")]/fieldset[@id = "edit-scheduler-settings"]'), 'The scheduler options are not shown as a vertical tab when they are configured to show as an expanded fieldset.'); + $this->assertTrue($this->xpath('//fieldset[@id = "edit-scheduler-settings" and not(contains(@class, "collapsed"))]'), 'The scheduler options are shown as an expanded fieldset.'); + } + + /** + * Tests creating and editing nodes with required scheduling enabled. + */ + public function testRequiredScheduling() { + $this->drupalLogin($this->adminUser); + + // Define test scenarios with expected results. + $test_cases = array( + + // A. Test scenarios that require scheduled publishing. + // The 1-10 numbering used below matches the test cases described in + // http://drupal.org/node/1198788#comment-7816119 + // + // When creating a new unpublished node it is required to enter a + // publication date. + array( + 'id' => 1, + 'required' => 'publish', + 'operation' => 'add', + 'status' => 0, + 'expected' => 'required', + 'message' => 'When scheduled publishing is required and a new unpublished node is created, entering a date in the publish on field is required.', + ), + + // When creating a new published node it is required to enter a + // publication date. The node will be unpublished on form submit. + array( + 'id' => 2, + 'required' => 'publish', + 'operation' => 'add', + 'status' => 1, + 'expected' => 'required', + 'message' => 'When scheduled publishing is required and a new published node is created, entering a date in the publish on field is required.', + ), + + // When editing a published node it is not needed to enter a publication + // date since the node is already published. + array( + 'id' => 3, + 'required' => 'publish', + 'operation' => 'edit', + 'scheduled' => 0, + 'status' => 1, + 'expected' => 'not required', + 'message' => 'When scheduled publishing is required and an existing published, unscheduled node is edited, entering a date in the publish on field is not required.', + ), + + // When editing an unpublished node that is scheduled for publication it + // is required to enter a publication date. + array( + 'id' => 4, + 'required' => 'publish', + 'operation' => 'edit', + 'scheduled' => 1, + 'status' => 0, + 'expected' => 'required', + 'message' => 'When scheduled publishing is required and an existing unpublished, scheduled node is edited, entering a date in the publish on field is required.', + ), + + // When editing an unpublished node that is not scheduled for publication + // it is not required to enter a publication date since this means that + // the node has already gone through a publication > unpublication cycle. + array( + 'id' => 5, + 'required' => 'publish', + 'operation' => 'edit', + 'scheduled' => 0, + 'status' => 0, + 'expected' => 'not required', + 'message' => 'When scheduled publishing is required and an existing unpublished, unscheduled node is edited, entering a date in the publish on field is not required.', + ), + + // B. Test scenarios that require scheduled unpublishing. + // + // When creating a new unpublished node it is required to enter an + // unpublication date since it is to be expected that the node will be + // published at some point and should subsequently be unpublished. + array( + 'id' => 6, + 'required' => 'unpublish', + 'operation' => 'add', + 'status' => 0, + 'expected' => 'required', + 'message' => 'When scheduled unpublishing is required and a new unpublished node is created, entering a date in the unpublish on field is required.', + ), + + // When creating a new published node it is required to enter an + // unpublication date. + array( + 'id' => 7, + 'required' => 'unpublish', + 'operation' => 'add', + 'status' => 1, + 'expected' => 'required', + 'message' => 'When scheduled unpublishing is required and a new published node is created, entering a date in the unpublish on field is required.', + ), + + // When editing a published node it is required to enter an unpublication + // date. + array( + 'id' => 8, + 'required' => 'unpublish', + 'operation' => 'edit', + 'scheduled' => 0, + 'status' => 1, + 'expected' => 'required', + 'message' => 'When scheduled unpublishing is required and an existing published, unscheduled node is edited, entering a date in the unpublish on field is required.', + ), + + // When editing an unpublished node that is scheduled for publication it + // it is required to enter an unpublication date. + array( + 'id' => 9, + 'required' => 'unpublish', + 'operation' => 'edit', + 'scheduled' => 1, + 'status' => 0, + 'expected' => 'required', + 'message' => 'When scheduled unpublishing is required and an existing unpublished, scheduled node is edited, entering a date in the unpublish on field is required.', + ), + + // When editing an unpublished node that is not scheduled for publication + // it is not required to enter an unpublication date since this means that + // the node has already gone through a publication - unpublication cycle. + array( + 'id' => 10, + 'required' => 'unpublish', + 'operation' => 'edit', + 'scheduled' => 0, + 'status' => 0, + 'expected' => 'not required', + 'message' => 'When scheduled unpublishing is required and an existing unpublished, unscheduled node is edited, entering a date in the unpublish on field is not required.', + ), + ); + + foreach ($test_cases as $test_case) { + // Enable required (un)publishing as stipulated by the test case. + variable_set('scheduler_publish_required_page', $test_case['required'] == 'publish'); + variable_set('scheduler_unpublish_required_page', $test_case['required'] == 'unpublish'); + + // Set the default node status, used when creating a new node. + $node_options_page = !empty($test_case['status']) ? array('status') : array(); + variable_set('node_options_page', $node_options_page); + + // To assist viewing and analysing the generated test result pages create + // a text string showing all the test case parameters. + $title_data = array(); + foreach ($test_case as $key => $value) { + if ($key != 'message') { + $title_data[] = $key . ' = ' . $value; + } + } + $title = implode(', ', $title_data); + + // If the test case requires editing a node, we need to create one first. + if ($test_case['operation'] == 'edit') { + $options = array( + 'title' => $title, + 'type' => 'page', + 'status' => $test_case['status'], + 'publish_on' => !empty($test_case['scheduled']) ? strtotime('+ 1 day', REQUEST_TIME) : 0, + ); + $node = $this->drupalCreateNode($options); + } + + // Make sure the publication date fields are empty so we can check if they + // throw form validation errors when they are required. + $edit = array( + 'title' => $title, + 'publish_on' => '', + 'unpublish_on' => '', + ); + $path = $test_case['operation'] == 'add' ? 'node/add/page' : 'node/' . $node->nid . '/edit'; + $this->drupalPost($path, $edit, t('Save')); + + // Check for the expected result. + switch ($test_case['expected']) { + case 'required': + $this->assertText(sprintf('%s field is required.', ucfirst($test_case['required']) . ' on'), $test_case['id'] . '. ' . $test_case['message']); + break; + + case 'not required': + $op = $test_case['operation'] == 'add' ? 'created' : 'updated'; + $this->assertText(sprintf('%s %s has been %s.', 'Basic page', $title, $op), $test_case['id'] . '. ' . $test_case['message']); + break; + } + } + } + + /** + * Test that Scheduler does not interfere with non-scheduler-enabled nodes. + */ + public function testNonEnabledType() { + // Create a 'Non-enabled' content type. + $this->drupalCreateContentType(array('type' => 'story', 'name' => t('Story Book'))); + + // Create a user who can add and edit story content, and log in. + $this->drupalLogin($this->drupalCreateUser(array( + 'access content overview', + 'access site reports', + 'create story content', + 'delete own story content', + 'edit own story content', + 'schedule publishing of nodes', + 'view scheduled content', + 'view own unpublished content', + ))); + + foreach ($this->dataNonEnabledType() as $data) { + list($id, $description, $publishing_enabled, $unpublishing_enabled) = $data; + + // The first test case specifically checks the behavior of the default + // unchanged settings, so only change these settings for later runs. + if ($id > 0) { + variable_set('scheduler_publish_enable_story', $publishing_enabled); + variable_set('scheduler_unpublish_enable_story', $unpublishing_enabled); + } + + // Create info string to show what combinations are being tested. + $info = 'Publishing ' . ($publishing_enabled ? 'enabled' : 'not enabled') + . ', Unpublishing ' . ($unpublishing_enabled ? 'enabled' : 'not enabled') + . ', ' . $description; + + // Check that the field(s) are displayed only for the correct settings. + $title = $id . 'a - ' . $info; + $this->drupalGet('node/add/story'); + if ($publishing_enabled) { + $this->assertFieldByName('publish_on', '', "The Publish-on field is shown: $title"); + } + else { + $this->assertNoFieldByName('publish_on', '', "The Publish-on field is not shown: $title"); + } + + if ($unpublishing_enabled) { + $this->assertFieldByName('unpublish_on', '', "The Unpublish-on field is shown: $title"); + } + else { + $this->assertNoFieldByName('unpublish_on', '', "The Unpublish-on field is not shown: $title"); + } + + // 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. + variable_set('scheduler_publish_required_story', !$publishing_enabled); + variable_set('scheduler_unpublish_required_story', !$unpublishing_enabled); + + $this->drupalPost('node/add/story', array('title' => $title), t('Save')); + // Check that the node has saved OK. + $string = sprintf('%s %s has been created.', 'Story Book', check_plain($title)); + $this->assertText($string, "Node added: $title"); + + // Check that the node can be editted and saved again. + $node = $this->drupalGetNodeByTitle($title); + if ($node) { + $this->drupalPost('node/' . $node->nid . '/edit', array(), t('Save')); + $string = sprintf('%s %s has been updated.', 'Story Book', check_plain($title)); + $this->assertText($string, "Node updated: $title"); + } + else { + $this->fail("No node to edit: $title"); + } + + // 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 + // being enabled for publishing then being disabled before publishing. + $title = $id . 'b - ' . $info; + $edit = array( + 'title' => $title, + 'status' => FALSE, + 'type' => 'story', + 'publish_on' => strtotime('- 2 min', REQUEST_TIME), + ); + $node = $this->drupalCreateNode($edit); + + // Run cron and display the dblog. + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + + // Reload the node. + $node = node_load($node->nid, NULL, TRUE); + // Check if the node has been published or remains unpublished. + if ($publishing_enabled) { + $this->assertTrue($node->status, 'The unpublished node has been published: ' . $title); + } + else { + $this->assertFalse($node->status, 'The unpublished node remains unpublished: ' . $title); + } + + // Do the same for unpublishing. + $title = $id . 'c - ' . $info; + $edit = array( + 'title' => $title, + 'status' => TRUE, + 'type' => 'story', + 'unpublish_on' => strtotime('- 2 min', REQUEST_TIME), + ); + $node = $this->drupalCreateNode($edit); + + // Run cron and display the dblog. + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + + // Reload the node. + $node = node_load($node->nid, NULL, TRUE); + // Check if the node has been unpublished or remains published. + if ($unpublishing_enabled) { + $this->assertFalse($node->status, 'The published node has been unpublished: ' . $title); + } + else { + $this->assertTrue($node->status, 'The published node remains published: ' . $title); + } + + // 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/scheduler'); + + } + + } + + /** + * Provides data for testNonEnabledType(). + * + * @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 + */ + public function dataNonEnabledType() { + $data = [ + // By default check that the scheduler date fields are not displayed. + 0 => [0, 'Default', FALSE, FALSE], + + // Explicitly disable this content type for both settings. + 1 => [1, 'Disabling both settings', FALSE, FALSE], + + // Turn on scheduled publishing only. + 2 => [2, 'Enabling publishing only', TRUE, FALSE], + + // Turn on scheduled unpublishing only. + 3 => [3, '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. + return $data; + + } + + /** + * 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 the two fields interact. + */ + public function testValidationDuringEdit() { + $this->drupalLogin($this->adminUser); + + // Set unpublishing to be required. + variable_set('scheduler_unpublish_required_page', TRUE); + + // 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(array('type' => 'page', 'status' => FALSE)); + $edit = array( + 'publish_on' => date('Y-m-d H:i:s', strtotime('+1 day', REQUEST_TIME)), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->assertText("If you set a 'publish-on' date then you must also set an 'unpublish-on' date.", 'Validation prevents entering a publish-on date with no unpublish-on date if unpublishing is required.'); + + // 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(array('type' => 'page', 'status' => FALSE)); + $edit = array( + 'status' => TRUE, + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->assertText("To publish this node you must also set an 'unpublish-on' date.", 'Validation prevents publishing the node directly without an unpublish-on date if unpublishing is required.'); + + // Create an unpublished page 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(array('type' => 'page', 'status' => FALSE)); + $edit = array( + 'publish_on' => date('Y-m-d H:i:s', strtotime('+2 day', REQUEST_TIME)), + 'unpublish_on' => date('Y-m-d H:i:s', strtotime('+1 day', REQUEST_TIME)), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->assertText("The 'unpublish on' date must be later than the 'publish on' date.", 'Validation prevents entering an unpublish-on date which is earlier than the publish-on date.'); + + } + + /** + * Tests the deletion of a scheduled node. + */ + public function testScheduledNodeDelete() { + // Log in. + $this->drupalLogin($this->adminUser); + + // 1. Test if it is possible to delete a node that does not have a + // publication date set, when scheduled publishing is required, and likewise + // for unpublishing. + // @see https://drupal.org/node/1614880 + // + // Create a published and an unpublished node, both without scheduling. + $published_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 1)); + $unpublished_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 0)); + + // Make scheduled publishing and unpublishing required. + variable_set('scheduler_publish_required_page', TRUE); + variable_set('scheduler_unpublish_required_page', TRUE); + + // Check that deleting the nodes does not throw form validation errors. + // 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->drupalPost('node/' . $published_node->nid . '/edit', array(), t('Delete')); + $this->assertNoText('Error message', 'No error messages are shown when trying to delete a published node with no scheduling information.'); + $this->assertText('Are you sure you want to delete', 'The deletion warning message is shown immediately when trying to delete a published node with no scheduling information.'); + + $this->drupalPost('node/' . $unpublished_node->nid . '/edit', array(), t('Delete')); + $this->assertNoText('Error message', 'No error messages are shown when trying to delete an unpublished node with no scheduling information.'); + $this->assertText('Are you sure you want to delete', 'The deletion warning message is shown immediately when trying to delete an unpublished node with no scheduling information.'); + + // 2. Test that nodes can be deleted with no validation errors if the dates + // are in the past. + // @see http://drupal.org/node/2627370 + // + // Create nodes with publish_on and unpublish_on dates in the past. + $values = array( + 'type' => 'page', + 'status' => TRUE, + 'unpublish_on' => strtotime('- 2 day', REQUEST_TIME), + ); + $published_node_past = $this->drupalCreateNode($values); + + $values = array( + 'type' => 'page', + 'status' => FALSE, + 'publish_on' => strtotime('- 2 day', REQUEST_TIME), + ); + $unpublished_node_past = $this->drupalCreateNode($values); + + // Attempt to delete the published node and check for no validation error. + $this->drupalPost('node/' . $published_node_past->nid . '/edit', array(), t('Delete')); + $this->assertNoText('Error message', 'No error messages are shown when trying to delete a node with an unpublish date in the past.'); + $this->assertText('Are you sure you want to delete', 'The deletion warning message is shown immediately when trying to delete a node with an unpublish date in the past.'); + + // Attempt to delete the unpublished node and check for no validation error. + $this->drupalPost('node/' . $unpublished_node_past->nid . '/edit', array(), t('Delete')); + $this->assertNoText('Error message', 'No error messages are shown when trying to delete a node with a publish date in the past.'); + $this->assertText('Are you sure you want to delete', 'The deletion warning message is shown immediately when trying to delete a node with a publish date in the past.'); + + } + + /** + * Tests meta-information on scheduled nodes. + * + * 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. + */ + public function testMetaInformation() { + // Log in. + $this->drupalLogin($this->adminUser); + + // Create a published node without scheduling. + $published_node = $this->drupalCreateNode(array('type' => 'page', 'status' => 1)); + $this->drupalGet('node/' . $published_node->nid); + + // Since we did not set an unpublish date, there should be no X-Robots-Tag + // header on the response. + $this->assertFalse($this->drupalGetHeader('X-Robots-Tag'), 'X-Robots-Tag is not present when no unpublish date is set.'); + + // Set a scheduler unpublish date on the node. + $unpublish_date = strtotime('+1 day', REQUEST_TIME); + $edit = array( + 'unpublish_on' => format_date($unpublish_date, 'custom', 'Y-m-d H:i:s'), + ); + $this->drupalPost('node/' . $published_node->nid . '/edit', $edit, t('Save')); + + // The node page should now have an X-Robots-Tag header with an + // unavailable_after-directive and RFC850 date- and time-value. + $this->drupalGet('node/' . $published_node->nid); + $robots_tag = $this->drupalGetHeader('X-Robots-Tag'); + $this->assertEqual($robots_tag, 'unavailable_after: ' . date(DATE_RFC850, $unpublish_date), 'X-Robots-Tag is present with correct timestamp derived from unpublish_on date.'); + } + + /** + * Tests that users without permission do not see the scheduler date fields. + */ + public function testPermissions() { + // 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(array( + 'access content', + 'create page content', + 'edit own page content', + 'view own unpublished content', + 'administer nodes', + )); + $this->drupalLogin($this->webUser); + + // Set the defaults for a new node. Nothing in array means all OFF for + // 'status', 'promote' and 'sticky'. + variable_set('node_options_page', array()); + + // Check that neither of the fields are displayed when creating a node. + $this->drupalGet('node/add/page'); + $this->assertNoFieldByName('publish_on', '', 'The Publish-on field is not shown for users who do not have permission to schedule content'); + $this->assertNoFieldByName('unpublish_on', '', 'The Unpublish-on field is not shown for users who do not have permission to schedule content'); + + // Initially run tests when publishing and unpublishing are not required. + variable_set('scheduler_publish_required_page', FALSE); + variable_set('scheduler_unpublish_required_page', FALSE); + + // Check that a new node can be saved and published. + $title = $this->randomString(15); + $this->drupalPost('node/add/page', array('title' => $title, 'status' => TRUE), t('Save')); + // check_plain() is required because the title may have % & or ' in it. + // Could use randomName() to get round this instead but it is good to use + // the full variety of characters available in randomString. + $this->assertText(sprintf('%s %s has been created.', 'Basic page', check_plain($title)), 'A node can be created and published when the user does not have scheduler permissions, and scheduling is not required.'); + $node = $this->drupalGetNodeByTitle($title); + $this->assertTrue($node->status, 'The new node is published.'); + + // Check that a new node can be saved as unpublished. + $title = $this->randomString(15); + $this->drupalPost('node/add/page', array('title' => $title, 'status' => FALSE), t('Save')); + $this->assertText(sprintf('%s %s has been created.', 'Basic page', check_plain($title)), 'A node can be created and saved as unpublished when the user does not have scheduler permissions, and scheduling is not required.'); + $node = $this->drupalGetNodeByTitle($title); + $this->assertFalse($node->status, 'The new node is unpublished.'); + + // Set publishing and unpublishing to required, to make it a stronger test. + variable_set('scheduler_publish_required_page', TRUE); + variable_set('scheduler_unpublish_required_page', TRUE); + + // @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 + // "Conflict between 'required publishing' and not having permission" + } + + /** + * Tests Scheduler token support. + */ + public function testTokenReplacement() { + // Log in. + $this->drupalLogin($this->adminUser); + + // Define timestamps for consistent use when repeated throughout this test. + $publish_on_timestamp = REQUEST_TIME + 3600; + $unpublish_on_timestamp = REQUEST_TIME + 7200; + + // Create an unpublished page with scheduled dates. + $settings = array( + 'type' => 'page', + 'status' => FALSE, + ); + $node = $this->drupalCreateNode($settings); + + // Create array of test case data. + $test_cases = array( + array( + 'token_format' => '', + 'date_format' => 'medium', + 'custom' => '', + ), + array( + 'token_format' => ':long', + 'date_format' => 'long', + 'custom' => '', + ), + array( + 'token_format' => ':raw', + 'date_format' => 'custom', + 'custom' => 'U', + ), + array( + 'token_format' => ':custom:jS F g:ia e O', + 'date_format' => 'custom', + 'custom' => 'jS F g:ia e O', + ), + ); + + foreach ($test_cases as $test_data) { + // Define a variable containing the template of tokens to be replaced. + // The template is not held in the node body, as that is confusing when + // viewing the test debug, becuase the tokens will not be replaced unless + // a text format for tokens is added. That is unnecessary for the tests. + $template = 'Publish on: [node:scheduler-publish' . $test_data['token_format'] . ']. Unpublish on: [node:scheduler-unpublish' . $test_data['token_format'] . '].'; + + // With each of the test cases, test using both numeric and string input. + foreach (array('numeric', 'string') as $test_data['input_type']) { + if ($test_data['input_type'] == 'numeric') { + // Set the node fields to numeric timestanps, as they will be in the + // final stored node, after hook_node_presave() has been executed. + $node->publish_on = $publish_on_timestamp; + $node->unpublish_on = $unpublish_on_timestamp; + } + else { + // Replicate the scheduler fields as if just input by a user during + // edit, before hook_node_presave() has been executed. + // @see https://www.drupal.org/node/2750467 + $node->publish_on = format_date($publish_on_timestamp, 'custom', 'Y-m-d H:i:s'); + $node->unpublish_on = format_date($unpublish_on_timestamp, 'custom', 'Y-m-d H:i:s'); + } + + // Get the output value after tokens have been replaced. + $token_output = token_replace($template, array('node' => $node)); + + // Create the expected text. + $publish_on_date = format_date($publish_on_timestamp, $test_data['date_format'], $test_data['custom']); + $unpublish_on_date = format_date($unpublish_on_timestamp, $test_data['date_format'], $test_data['custom']); + $expected_output = 'Publish on: ' . $publish_on_date . '. Unpublish on: ' . $unpublish_on_date . '.'; + + // Check that the actual text matches the expected value. + $tested_format = $test_data['token_format'] ? '"' . $test_data['token_format'] . '"' : 'default'; + $this->assertEqual($token_output, $expected_output, 'Scheduler tokens replaced correctly for ' . $tested_format . ' format with ' . $test_data['input_type'] . ' input data.'); + } + + // Remove the scheduled dates and check that token replacment still works. + unset($node->publish_on); + unset($node->unpublish_on); + $token_output = token_replace($template, array('node' => $node)); + $expected_output = 'Publish on: [node:scheduler-publish' . $test_data['token_format'] . ']. Unpublish on: [node:scheduler-unpublish' . $test_data['token_format'] . '].'; + $this->assertEqual($token_output, $expected_output, 'Scheduler tokens replaced correctly for ' . $tested_format . ' format with no scheduled dates.'); + } + } + + /** + * Tests the 'touch' option to update the created date during publishing. + */ + public function testAlterCreationDate() { + // Ensure nodes with past dates will be scheduled not published immediately. + variable_set('scheduler_publish_past_date_page', 'schedule'); + + // Create a node with a 'created' date two days in the past. + $created = strtotime('-2 day', REQUEST_TIME); + $settings = array( + 'type' => 'page', + 'created' => $created, + 'status' => FALSE, + ); + $node = $this->drupalCreateNode($settings); + // Check that the node is not published. + $this->assertFalse($node->status, 'Before cron, the node 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. + $this->assertTrue($node->status, 'After cron, the node has been published.'); + $created_after_cron = $node->created; + $this->assertEqual($created, $created_after_cron, 'The node creation date is not changed by default.'); + + // Set option to change the created date to match the publish_on date. + variable_set('scheduler_publish_touch_page', TRUE); + + // Schedule the node again and run cron. + $node = $this->schedule($node, 'publish'); + // Check that the created date has changed to match the publish_on date. + $created_after_cron = $node->created; + $this->assertEqual(strtotime('-1 day', REQUEST_TIME), $created_after_cron, "With 'touch' option set, the node creation date is changed to match the publishing date."); + } + + /** + * Test scheduler lightweight cron runs. + */ + public function testLightweightCronRun() { + // Run the lightweight cron anonymously without any cron key. + $this->drupalGet('scheduler/cron'); + $this->assertResponse(200, 'With no cron key (default) scheduler/cron returns "200 OK"'); + + // Generate and set a cron key. + $cron_key = substr(md5(rand()), 0, 20); + variable_set('scheduler_lightweight_access_key', $cron_key); + + // Run the lightweight cron without any cron key. + $this->drupalGet('scheduler/cron'); + $this->assertResponse(403, 'After creating a cron key scheduler/cron returns "403 Not Authorized"'); + + // Run the lightweight cron anonymously with a random (wrong) cron key. + $this->drupalGet('scheduler/cron/' . substr(md5(rand()), 0, 20)); + $this->assertResponse(403, 'scheduler/cron/{wrong key} returns "403 Not Authorized"'); + + // Run the lightweight cron anonymously with the valid cron key. + $this->drupalGet('scheduler/cron/' . $cron_key); + $this->assertResponse(200, 'scheduler/cron/{correct key} returns "200 OK"'); + } + +} + +/** + * Tests the components of the scheduler interface which use the date module. + */ +class SchedulerDateModuleTest extends SchedulerTestBase { + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Scheduler and Date module', + 'description' => 'Tests the functionality which needs the Date module.', + 'group' => 'Scheduler', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp('date', 'date_popup', 'scheduler'); + parent::commonSettings(); + } + + /** + * Test the default time functionality. + */ + public function testDefaultTime() { + $this->drupalLogin($this->adminUser); + + // Perform the checks twice, first using plain text entry then with date + // popup calendars. + foreach (array('textfield', 'date_popup') as $field_type) { + + // Define the form fields and date formats we will test according to + // whether date calendar popups will be used or not. + $using_popup = $field_type == 'date_popup'; + $publish_date_field = $using_popup ? 'publish_on[date]' : 'publish_on'; + $unpublish_date_field = $using_popup ? 'unpublish_on[date]' : 'unpublish_on'; + $publish_time_field = $using_popup ? 'publish_on[time]' : 'publish_on'; + $unpublish_time_field = $using_popup ? 'unpublish_on[time]' : 'unpublish_on'; + $time_format = $using_popup ? 'H:i:s' : 'Y-m-d H:i:s'; + + // We cannot easily test the exact validation messages as they contain the + // REQUEST_TIME of the POST request, which can be one or more seconds in + // the past. Best we can do is check the fixed part of the message. + $publish_validation_message = $using_popup ? 'The value input for field Publish on is invalid:' : "The 'publish on' value does not match the expected format of"; + $unpublish_validation_message = $using_popup ? 'The value input for field Unpublish on is invalid:' : "The 'unpublish on' value does not match the expected format of"; + + // For testing we use an offset of 6 hours 30 minutes (23400 seconds). + // First test with the "date only" functionality disabled. + $settings = array( + 'scheduler_date_format' => 'Y-m-d H:i:s', + 'scheduler_default_time' => '6:30', + 'scheduler_field_type' => $field_type, + 'scheduler_allow_date_only' => FALSE, + ); + $this->drupalPost('admin/config/content/scheduler', $settings, t('Save configuration')); + + // Verify that entering a time is required. + $edit = array( + 'title' => $this->randomName(), + $publish_date_field => date('Y-m-d', strtotime('+1 day', REQUEST_TIME)), + $unpublish_date_field => date('Y-m-d', strtotime('+2 day', REQUEST_TIME)), + ); + $this->drupalPost('node/add/page', $edit, t('Save')); + $this->assertText($publish_validation_message, 'Validation error message is shown correctly. By default it is required to enter a time when scheduling content for publication.'); + $this->assertText($unpublish_validation_message, 'Validation error message is shown correctly. By default it is required to enter a time when scheduling content for unpublication.'); + + // Allow the user to enter only the date. + $settings = array('scheduler_allow_date_only' => TRUE); + $this->drupalPost('admin/config/content/scheduler', $settings, t('Save configuration')); + + // Check that the correct default time is added to the scheduled date. + $this->drupalPost('node/add/page', $edit, t('Save')); + $this->assertNoText("The 'publish on' value does not match the expected format of", 'If the default time option is enabled the user can skip the time when scheduling content for publication.'); + $this->assertNoText("The 'unpublish on' value does not match the expected format of", 'If the default time option is enabled the user can skip the time when scheduling content for unpublication.'); + $publish_time = date('Y-m-d H:i:s', strtotime('tomorrow', REQUEST_TIME) + 23400); + $this->assertText(sprintf('This post is unpublished and will be published %s.', $publish_time), 'The user is informed that the content will be published on the requested date, on the default time.'); + + // Check that the default time has been added to the form fields on edit. + $this->clickLink(t('Edit')); + $this->assertFieldByName($publish_time_field, date($time_format, strtotime('tomorrow', REQUEST_TIME) + 23400), 'The default time offset has been added to the date field when scheduling content for publication.'); + $this->assertFieldByName($unpublish_time_field, date($time_format, strtotime('tomorrow +1 day', REQUEST_TIME) + 23400), 'The default time offset has been added to the date field when scheduling content for unpublication.'); + + // Check that it is not possible for the admin to enter a date format + // without a time if the 'date only' option is not enabled. + $edit = array( + 'scheduler_date_format' => 'Y-m-d', + 'scheduler_allow_date_only' => FALSE, + ); + $this->drupalPost('admin/config/content/scheduler', $edit, t('Save configuration')); + $this->assertText('You must either include a time within the date format or enable the date-only option.', format_string('It is not possible to enter a date format without a time if the "date only" option is not enabled and the field type is set to %field_type.', array('%field_type' => $field_type))); + } + } + + /** + * Tests configuration of different date formats with the Date Popup field. + */ + public function testDatePopupFormats() { + $this->drupalLogin($this->adminUser); + + // Define some date formats to test. + $test_cases = array( + // By default we are not using the 'date only' option, so passing only a + // date should fail. + 'Y-m-d' => FALSE, + 'd-m-Y' => FALSE, + 'm-d-Y' => FALSE, + 'n/j/y' => FALSE, + 'd F Y' => FALSE, + + // Test a number of supported date formats. + 'Y-m-d H:i' => TRUE, + 'd-m-Y h:ia' => TRUE, + 'd m Y h:i a' => TRUE, + 'm-d-Y h:iA' => TRUE, + 'm/d/Y h:i A' => TRUE, + 'n-j-y H:i:s' => TRUE, + 'Y/M/d h:i:sA' => TRUE, + 'Y/M/d h:i:s A' => TRUE, + 'j F y h:i:sa' => TRUE, + 'j F y h:i:s a' => TRUE, + + // Test a number of date formats with invalid time specifications. + 'y-m-d G:i' => FALSE, + 'y-j-n G:i:sa' => FALSE, + 'Y-m-d g:i:sa' => FALSE, + 'y-m-d g:i:s' => FALSE, + 'n-j-y h:i' => FALSE, + 'd-m-y h:i:s' => FALSE, + 'd/M/y H:i:sA' => FALSE, + 'Y F d H:ia' => FALSE, + ); + foreach ($test_cases as $date_format => $expected_result) { + $edit = array( + 'scheduler_date_format' => $date_format, + 'scheduler_field_type' => 'date_popup', + ); + $this->drupalPost('admin/config/content/scheduler', $edit, t('Save configuration')); + $message = format_string('When using date popups the date format %format is @expected', array('%format' => $date_format, '@expected' => $expected_result ? 'allowed' : 'not allowed.')); + $assert = $expected_result ? 'assertNoText' : 'assertText'; + $this->$assert('Error message', $message); + } + } + +} + +/** + * Tests Schedulers interaction with the Rules module. + */ +class SchedulerRulesTest extends SchedulerTestBase { + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Scheduler and Rules Integration', + 'description' => 'Tests the Rules actions and conditions provided by Scheduler.', + 'group' => 'Scheduler', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp('scheduler', 'dblog', 'rules'); + parent::commonSettings(); + + // Create a published node. + $this->node = $this->drupalCreateNode(array( + 'title' => 'Initial Test Node', + 'type' => 'page', + 'uid' => $this->adminUser->uid, + 'status' => TRUE, + )); + } + + /** + * Tests the four actions which set and remove the Scheduler dates. + */ + public function testRulesActions() { + $this->drupalLogin($this->adminUser); + $node = $this->node; + + $message1 = 'RULE 1. Set Publish-on date.'; + $rule = rules_reaction_rule(); + $rule->event('node_presave') + ->condition(rules_condition('data_is', array('data:select' => 'node:title', 'value' => 'Rule 1'))) + ->action(rules_action('scheduler_set_publish_date_action', array('data:select' => 'node:node', 'date' => REQUEST_TIME + 1800))) + ->action('drupal_message', array('message' => $message1)); + // Check access and integrity, then save the rule. + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_1', $message1); + + $message2 = 'RULE 2. Remove Publish-on date.'; + $rule = rules_reaction_rule(); + $rule->event('node_update') + ->condition(rules_condition('data_is', array('data:select' => 'node:title', 'value' => 'Rule 2'))) + ->action(rules_action('scheduler_remove_publish_date_action', array('data:select' => 'node:node'))) + ->action('node_publish') + ->action('drupal_message', array('message' => $message2)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_2', $message2); + + $message3 = 'RULE 3. Set Unpublish-on date.'; + $rule = rules_reaction_rule(); + $rule->event('node_presave') + ->condition(rules_condition('data_is', array('data:select' => 'node:title', 'value' => 'Rule 3'))) + ->action(rules_action('scheduler_set_unpublish_date_action', array('data:select' => 'node:node', 'date' => REQUEST_TIME + 1800))) + ->action('drupal_message', array('message' => $message3)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_3', $message3); + + $message4 = 'RULE 4. Remove Unpublish-on date.'; + $rule = rules_reaction_rule(); + $rule->event('node_update') + ->condition(rules_condition('data_is', array('data:select' => 'node:title', 'value' => 'Rule 4'))) + ->action(rules_action('scheduler_remove_unpublish_date_action', array('data:select' => 'node:node'))) + ->action('drupal_message', array('message' => $message4)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_4', $message4); + + // Edit node without changing title, then reload the node. + $this->drupalPost('node/' . $node->nid . '/edit', array(), t('Save')); + $node = node_load($node->nid, NULL, TRUE); + + // Check that neither of the rules are triggered, no publish and unpublish + // dates are set and the status is still published. + $this->assertNoText($message1, '"' . $message1 . '" is not shown'); + $this->assertNoText($message2, '"' . $message2 . '" is not shown'); + $this->assertNoText($message3, '"' . $message3 . '" is not shown'); + $this->assertNoText($message4, '"' . $message4 . '" is not shown'); + $this->assertTrue(empty($node->publish_on), 'Node is not scheduled for publishing.'); + $this->assertTrue(empty($node->unpublish_on), 'Node is not scheduled for unpublishing.'); + $this->assertTrue($node->status, 'Node remains published for title: "' . $node->title . '".'); + + $this->drupalGet('admin/reports/dblog'); + $this->drupalGet('admin/content/scheduler'); + + // Edit the node, triggering rule 1, then reload the node. + $this->drupalPost('node/' . $node->nid . '/edit', array('title' => 'Rule 1'), t('Save')); + $node = node_load($node->nid, NULL, TRUE); + + // Check that only rule 1 is triggered. + $this->assertText($message1, '"' . $message1 . '" is shown'); + $this->assertNoText($message2, '"' . $message2 . '" is not shown'); + $this->assertNoText($message3, '"' . $message3 . '" is not shown'); + $this->assertNoText($message4, '"' . $message4 . '" is not shown'); + // Check that a publishing date has been set and status is now unpublished. + $this->assertTrue(!empty($node->publish_on), 'Node is scheduled for publishing.'); + $this->assertTrue(empty($node->unpublish_on), 'Node is not scheduled for unpublishing.'); + $this->assertFalse($node->status, 'Node is now unpublished for title: "' . $node->title . '".'); + + $this->drupalGet('admin/reports/dblog'); + $this->drupalGet('admin/content/scheduler'); + + // Edit the node, triggering rule 2, then reload the node. + $this->drupalPost('node/' . $node->nid . '/edit', array('title' => 'Rule 2'), t('Save')); + $node = node_load($node->nid, NULL, TRUE); + + // Check that only rule 2 is triggered. + $this->assertNoText($message1, '"' . $message1 . '" is not shown'); + $this->assertText($message2, '"' . $message2 . '" is shown'); + $this->assertNoText($message3, '"' . $message3 . '" is not shown'); + $this->assertNoText($message4, '"' . $message4 . '" is not shown'); + // Check that the publishing date has been removed. + $this->assertTrue(empty($node->publish_on), 'Node is not scheduled for publishing.'); + $this->assertTrue(empty($node->unpublish_on), 'Node is not scheduled for unpublishing.'); + $this->assertTrue($node->status, 'Node is now published for title: "' . $node->title . '".'); + + $this->drupalGet('admin/reports/dblog'); + $this->drupalGet('admin/content/scheduler'); + + // Edit the node, triggering rule 3, then reload the node. + $this->drupalPost('node/' . $node->nid . '/edit', array('title' => 'Rule 3'), t('Save')); + $node = node_load($node->nid, NULL, TRUE); + + // Check that only rule 3 is triggered. + $this->assertNoText($message1, '"' . $message1 . '" is not shown'); + $this->assertNoText($message2, '"' . $message2 . '" is not shown'); + $this->assertText($message3, '"' . $message3 . '" is shown'); + $this->assertNoText($message4, '"' . $message4 . '" is not shown'); + // Check that an unpublishing date has been set. + $this->assertTrue(empty($node->publish_on), 'Node is not scheduled for publishing.'); + $this->assertTrue(!empty($node->unpublish_on), 'Node is scheduled for unpublishing.'); + $this->assertTrue($node->status, 'Node remains published for title: "' . $node->title . '".'); + + $this->drupalGet('admin/reports/dblog'); + $this->drupalGet('admin/content/scheduler'); + + // Edit the node, triggering rule 4, then reload the node. + $this->drupalPost('node/' . $node->nid . '/edit', array('title' => 'Rule 4'), t('Save')); + $node = node_load($node->nid, NULL, TRUE); + + // Check that only rule 4 is triggered. + $this->assertNoText($message1, '"' . $message1 . '" is not shown'); + $this->assertNoText($message2, '"' . $message2 . '" is not shown'); + $this->assertNoText($message3, '"' . $message3 . '" is not shown'); + $this->assertText($message4, '"' . $message4 . '" is shown'); + // Check that the unpublishing date has been removed. + $this->assertTrue(empty($node->publish_on), 'Node is not scheduled for publishing.'); + $this->assertTrue(empty($node->unpublish_on), 'Node is not scheduled for unpublishing.'); + $this->assertTrue($node->status, 'Node remains published for title: "' . $node->title . '".'); + + $this->drupalGet('admin/reports/dblog'); + $this->drupalGet('admin/content/scheduler'); + } + + /** + * Tests the two conditions for a content type being enabled for scheduling. + */ + public function testRulesConditionsNodetypeEnabled() { + $this->drupalLogin($this->adminUser); + $node = $this->node; + + // Create a reaction rule to display a message when viewing a node of a type + // that is enabled for scheduled publishing. + // "viewing content" actually means "viewing PUBLISHED content". + $message1 = 'RULE 1. This node type is enabled for scheduled publishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_view') + ->condition(rules_condition('scheduler_condition_publishing_is_enabled', array('data:select' => 'node:node'))) + ->action('drupal_message', array('message' => $message1)); + // Check access and integrity, then save the rule. + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_1', $message1); + + // Create a reaction rule to display a message when viewing a node of a type + // that is enabled for scheduled unpublishing. + $message2 = 'RULE 2. This node type is enabled for scheduled unpublishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_view') + ->condition(rules_condition('scheduler_condition_unpublishing_is_enabled', array('data:select' => 'node:node'))) + ->action('drupal_message', array('message' => $message2)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_2', $message2); + + // Create a reaction rule to display a message when viewing a node of a type + // that is NOT enabled for scheduled publishing. + $message3 = 'RULE 3. This node type is not enabled for scheduled publishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_view') + ->condition(rules_condition('scheduler_condition_publishing_is_enabled', array('data:select' => 'node:node'))->negate()) + ->action('drupal_message', array('message' => $message3)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_3', $message3); + + // Create a reaction rule to display a message when viewing a node of a type + // that is not enabled for scheduled unpublishing. + $message4 = 'RULE 4. This node type is not enabled for scheduled unpublishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_view') + ->condition(rules_condition('scheduler_condition_unpublishing_is_enabled', array('data:select' => 'node:node'))->negate()) + ->action('drupal_message', array('message' => $message4)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_4', $message4); + + // View the node and check the default position - that the node type is + // enabled for both publishing and unpublishing. + $this->drupalGet('node/' . $node->nid); + $this->assertText($message1, '"' . $message1 . '" is shown'); + $this->assertText($message2, '"' . $message2 . '" is shown'); + $this->assertNoText($message3, '"' . $message3 . '" is not shown'); + $this->assertNoText($message4, '"' . $message4 . '" is not shown'); + + // Turn off scheduled publishing for the node type and check the rules. + variable_set('scheduler_publish_enable_page', FALSE); + $this->drupalGet('node/' . $node->nid); + $this->assertNoText($message1, '"' . $message1 . '" is not shown'); + $this->assertText($message2, '"' . $message2 . '" is shown'); + $this->assertText($message3, '"' . $message3 . '" is shown'); + $this->assertNoText($message4, '"' . $message4 . '" is not shown'); + + // Turn off scheduled unpublishing for the node type and the check again. + variable_set('scheduler_unpublish_enable_page', FALSE); + $this->drupalGet('node/' . $node->nid); + $this->assertNoText($message1, '"' . $message1 . '" is not shown'); + $this->assertNoText($message2, '"' . $message2 . '" is not shown'); + $this->assertText($message3, '"' . $message3 . '" is shown'); + $this->assertText($message4, '"' . $message4 . '" is shown'); + } + + /** + * Tests the two conditions for whether a node is scheduled. + */ + public function testRulesConditionsNodeIsScheduled() { + $this->drupalLogin($this->adminUser); + $node = $this->node; + + // Create a reaction rule to display a message when a node is updated and + // is not scheduled for publishing. + $message5 = 'RULE 5. This content is not scheduled for publishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_update') + ->condition(rules_condition('scheduler_condition_node_is_scheduled_for_publishing', array('data:select' => 'node:node'))->negate()) + ->action('drupal_message', array('message' => $message5)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_5', $message5); + + // Create a reaction rule to display a message when a node is updated and + // is not scheduled for unpublishing. + $message6 = 'RULE 6. This content is not scheduled for unpublishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_update') + ->condition(rules_condition('scheduler_condition_node_is_scheduled_for_unpublishing', array('data:select' => 'node:node'))->negate()) + ->action('drupal_message', array('message' => $message6)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_6', $message6); + + // Create a reaction rule to display a message when a node is updated and + // is scheduled for publishing. + $message7 = 'RULE 7. This content is scheduled for publishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_update') + ->condition(rules_condition('scheduler_condition_node_is_scheduled_for_publishing', array('data:select' => 'node:node'))) + ->action('drupal_message', array('message' => $message7)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_7', $message7); + + // Create a reaction rule to display a message when a node is updated and + // is scheduled for unpublishing. + $message8 = 'RULE 8. This content is scheduled for unpublishing.'; + $rule = rules_reaction_rule(); + $rule->event('node_update') + ->condition(rules_condition('scheduler_condition_node_is_scheduled_for_unpublishing', array('data:select' => 'node:node'))) + ->action('drupal_message', array('message' => $message8)); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_8', $message8); + + // Edit the node but do not enter any scheduling dates. + $this->drupalPost('node/' . $node->nid . '/edit', array(), t('Save')); + + // Check that the conditions have been met or not as expceted. + $this->assertText($message5, '"' . $message5 . '" is shown'); + $this->assertText($message6, '"' . $message6 . '" is shown'); + $this->assertNoText($message7, '"' . $message7 . '" is not shown'); + $this->assertNoText($message8, '"' . $message8 . '" is not shown'); + + // Edit the node and set a publish_on date. + $edit = array( + 'publish_on' => date('Y-m-d H:i:s', strtotime('+1 day', REQUEST_TIME)), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + + // Check that the conditions have been met (or not) as expected. + $this->assertNoText($message5, '"' . $message5 . '" is not shown'); + $this->assertText($message6, '"' . $message6 . '" is shown'); + $this->assertText($message7, '"' . $message7 . '" is shown'); + $this->assertNoText($message8, '"' . $message8 . '" is not shown'); + + // Edit the node and set an unpublish_on date. + $edit = array( + 'unpublish_on' => date('Y-m-d H:i:s', strtotime('+2 day', REQUEST_TIME)), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + + // Check that the conditions have been met (or not) as expected. + $this->assertNoText($message5, '"' . $message5 . '" is not shown'); + $this->assertNoText($message6, '"' . $message6 . '" is not shown'); + $this->assertText($message7, '"' . $message7 . '" is shown'); + $this->assertText($message8, '"' . $message8 . '" is shown'); + } + + /** + * Helper function to check which events have been triggered. + * + * @param array $expected + * Array of integers to indicate which messages (1-6) should be seen. + */ + private function checkEventText(array $expected = array()) { + for ($i = 1; $i <= 6; $i++) { + $message = $this->eventMessage[$i]; + if (in_array($i, $expected)) { + $this->assertText($message, 'Event message "' . $message . '" is shown'); + } + else { + $this->assertNoText($message, 'Event message "' . $message . '" is not shown'); + } + } + } + + /** + * Tests the six events provided by Scheduler. + * + * This class tests all six events provided by Scheduler, by creating six + * rules which are all active throughout the test. They are all checked in + * this one test class to make the tests stronger, as this 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. + */ + public function testRulesEvents() { + + // Create six reaction rules, one for each event that Scheduler triggers. + $rule_data = array( + 1 => array('scheduler_new_node_is_scheduled_for_publishing_event', 'A new node is created and is scheduled for publishing.'), + 2 => array('scheduler_existing_node_is_scheduled_for_publishing_event', 'An existing node is saved and is scheduled for publishing.'), + 3 => array('scheduler_node_has_been_published_event', 'Scheduler has published this node during cron.'), + 4 => array('scheduler_new_node_is_scheduled_for_unpublishing_event', 'A new node is created and is scheduled for unpublishing.'), + 5 => array('scheduler_existing_node_is_scheduled_for_unpublishing_event', 'An existing node is saved and is scheduled for unpublishing.'), + 6 => array('scheduler_node_has_been_unpublished_event', 'Scheduler has unpublished this node during cron.'), + ); + foreach ($rule_data as $i => $values) { + list($event_name, $description) = $values; + $rule = rules_reaction_rule(); + $this->eventMessage[$i] = 'RULE ' . $i . '. ' . $description; + $rule->event($event_name) + ->action('drupal_message', array('message' => $this->eventMessage[$i])); + $rule->access(); + $rule->integrityCheck(); + $rule->save('rule_id_' . $i, $this->eventMessage[$i]); + } + + $this->drupalLogin($this->adminUser); + + // Create a node without any scheduled dates, using node/add/page not + // drupalCreateNode(), and check that no events are triggered. + $edit = array( + 'title' => 'Test for no events on creation', + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/add/page', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); + $this->checkEventText(); + + // Edit the node and check that no events are triggered. + $edit = array( + 'title' => 'Test for no events on edit', + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->checkEventText(); + + // Create a new node with a publish-on date, and check that only event 1 is + // triggered. Use time() not REQUEST_TIME to guarantee the datetime is in + // the future but only by a few seconds. + $edit = array( + 'title' => 'Create node with publish-on date', + 'publish_on' => date('Y-m-d H:i:s', time() + 3), + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/add/page', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); + $this->checkEventText(array(1)); + + // Edit this node and check that only event 2 is triggered. + $edit = array( + 'title' => 'Edit node with publish-on date', + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->checkEventText(array(2)); + + // Delay before running cron to ensure that the date will be in the past, so + // that the node gets processed. Then assert that only event 3 is triggered. + sleep(5); + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + $this->checkEventText(array(3)); + + // Create a new node with an unpublish-on date, and check that only event 4 + // is triggered. + $edit = array( + 'title' => 'Create node with unpublish-on date', + 'unpublish_on' => date('Y-m-d H:i:s', time() + 3), + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/add/page', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); + $this->checkEventText(array(4)); + + // Edit this node and check that only event 5 is triggered. + $edit = array( + 'title' => 'Edit node with unpublish-on date', + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->checkEventText(array(5)); + + // Delay before running cron to ensure that the date will be in the past, so + // that the node gets processed. Then assert that event 6 is triggered. + sleep(5); + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + $this->checkEventText(array(6)); + + // Create a new node with both publish-on and unpublish-on dates, and check + // that events 1 and event 4 are both triggered. + $edit = array( + 'title' => 'Create node with both dates', + 'publish_on' => date('Y-m-d H:i:s', time() + 3), + 'unpublish_on' => date('Y-m-d H:i:s', time() + 4), + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/add/page', $edit, t('Save')); + $node = $this->drupalGetNodeByTitle($edit['title']); + $this->checkEventText(array(1, 4)); + + // Edit this node and check that events 2 and 5 are triggered. + $edit = array( + 'title' => 'Edit node with both dates', + 'body[' . LANGUAGE_NONE . '][0][value]' => $this->randomString(30), + ); + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->checkEventText(array(2, 5)); + + // Delay before running cron to ensure that the dates will be in the past. + // Then assert that events 3, 5 & 6 are triggered. + sleep(6); + $this->cronRun(); + $this->drupalGet('admin/reports/dblog'); + $this->checkEventText(array(3, 5, 6)); + } + +} diff --git a/frontend/drupal/sites/all/modules/scheduler/tests/scheduler_api.test b/frontend/drupal/sites/all/modules/scheduler/tests/scheduler_api.test index d80a83610..2c6dd2994 100644 --- a/frontend/drupal/sites/all/modules/scheduler/tests/scheduler_api.test +++ b/frontend/drupal/sites/all/modules/scheduler/tests/scheduler_api.test @@ -4,6 +4,10 @@ * @file * Tests for the Scheduler API. */ + +/** + * Tests to cover the Scheduler API functions. + */ class SchedulerApiTestCase extends DrupalWebTestCase { /** @@ -27,7 +31,7 @@ class SchedulerApiTestCase extends DrupalWebTestCase { /** * {@inheritdoc} */ - function setUp() { + public function setUp() { parent::setUp('scheduler', 'scheduler_test'); // Add scheduler functionality to the 'scheduler_test' node type. @@ -46,7 +50,7 @@ class SchedulerApiTestCase extends DrupalWebTestCase { * @todo Create and update the nodes through the interface so we can check if * the correct messages are displayed. */ - function testAllowedPublishing() { + public function testAllowedPublishing() { // Create a node that is not approved for publication. Then simulate a cron // run, and check that the node is not published. $node = $this->createUnapprovedNode(); @@ -101,14 +105,14 @@ class SchedulerApiTestCase extends DrupalWebTestCase { /** * Check to see if a node is not published. * - * @param $nid - * The nid of the node to check. - * @param $message + * @param int $nid + * The id of the node to check. + * @param string $message * The message to display along with the assertion. - * @param $group + * @param string $group * The type of assertion - examples are "Browser", "PHP". * - * @return + * @return bool * TRUE if the assertion succeeded, FALSE otherwise. */ public function assertNodeNotPublished($nid, $message = NULL, $group = 'Other') { @@ -119,14 +123,14 @@ class SchedulerApiTestCase extends DrupalWebTestCase { /** * Check to see if a node is published. * - * @param $nid - * The nid of the node to check. - * @param $message + * @param int $nid + * The id of the node to check. + * @param string $message * The message to display along with the assertion. - * @param $group + * @param string $group * The type of assertion - examples are "Browser", "PHP". * - * @return + * @return bool * TRUE if the assertion succeeded, FALSE otherwise. */ public function assertNodePublished($nid, $message = NULL, $group = 'Other') { @@ -138,7 +142,7 @@ class SchedulerApiTestCase extends DrupalWebTestCase { * Returns the publication status of a node. * * @param int $nid - * The nid of the node for which the publication status is desired. + * The id of the node for which the publication status is desired. * * @return bool * TRUE if the node is published, FALSE otherwise.