From ad462cc943dcbc0cac640447b642c9504a305bb0 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 18 Sep 2020 15:12:26 +0200 Subject: [PATCH] Drupal: Update module scheduler --- .../all/modules/scheduler/scheduler.admin.inc | 69 +- .../all/modules/scheduler/scheduler.api.php | 68 +- .../all/modules/scheduler/scheduler.cron.inc | 24 +- .../all/modules/scheduler/scheduler.drush.inc | 39 + .../all/modules/scheduler/scheduler.edit.inc | 20 +- .../all/modules/scheduler/scheduler.info | 13 +- .../all/modules/scheduler/scheduler.install | 11 +- .../all/modules/scheduler/scheduler.module | 57 +- .../all/modules/scheduler/scheduler.rules.inc | 58 +- .../scheduler/scheduler.rules_defaults.inc | 6 +- .../modules/scheduler/scheduler.tokens.inc | 25 +- .../all/modules/scheduler/scheduler.views.inc | 24 +- ...uler_handler_field_scheduler_countdown.inc | 44 +- .../scheduler/scheduler_vertical_tabs.js | 6 +- .../tests/modules/scheduler_test.info | 13 + .../tests/modules/scheduler_test.install | 53 + .../tests/modules/scheduler_test.module | 39 + .../modules/scheduler/tests/scheduler.test | 1692 +++++++++++++++++ .../scheduler/tests/scheduler_api.test | 30 +- 19 files changed, 2105 insertions(+), 186 deletions(-) create mode 100644 frontend/drupal/sites/all/modules/scheduler/scheduler.drush.inc create mode 100644 frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.info create mode 100644 frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.install create mode 100644 frontend/drupal/sites/all/modules/scheduler/tests/modules/scheduler_test.module create mode 100644 frontend/drupal/sites/all/modules/scheduler/tests/scheduler.test 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("You can test Scheduler's lightweight cron process interactively") . ': ', + '#prefix' => '
' . $prefix_text . ': ', '#value' => t("Run Scheduler's lightweight cron now"), '#submit' => array('_scheduler_lightweight_cron_submit'), '#suffix' => "
\n", @@ -403,12 +418,6 @@ function _scheduler_lightweight_cron($form, &$form_state) { '#default_value' => variable_get('scheduler_lightweight_log', 1), '#description' => t("When this option is checked, Scheduler will write an entry to the log every time the lightweight cron process is started and completed. This is useful during set up and testing, but can result in a large number of log entries. Any actions performed during the lightweight cron run will always be logged regardless of this setting."), ); - $form['scheduler_cron_settings']['scheduler_cache_clear_all'] = array( - '#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.'), - ); $form['scheduler_cron_settings']['scheduler_lightweight_access_key'] = array( '#type' => 'textfield', '#title' => t('Lightweight cron access key'), @@ -418,7 +427,12 @@ function _scheduler_lightweight_cron($form, &$form_state) { ); if (isset($form_state['scheduler_generate_new_key'])) { $new_access_key = substr(md5(rand()), 0, 20); + // @codingStandardsIgnoreStart + // We have to use $form_state['input'] here as storing the new key in + // $form_state['values'] does not work. It is acceptable to use it on the + // left of an assignment even if PHPCS does not think so. $form_state['input']['scheduler_lightweight_access_key'] = $new_access_key; + // @codingStandardsIgnoreEnd drupal_set_message(t('A new random key has been generated but not saved. If you wish to use this, first "Save Configuration" to store the value, then modify your crontab job.'), 'warning'); } $form['scheduler_cron_settings']['create_key'] = array( @@ -466,7 +480,7 @@ function _scheduler_timecheck() { * * @ingroup themeable */ -function theme_scheduler_timecheck($variables) { +function theme_scheduler_timecheck(array $variables) { global $user; $now = $variables['now']; @@ -487,9 +501,10 @@ function theme_scheduler_timecheck($variables) { '@admin_regional_settings' => url('admin/config/regional/settings'), ); - $output = '

' . t('Time check') . '

' - . '

' . 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('Time check') + . '

' . 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.