--- scheduler.info 2013-07-28 02:47:46.000000000 +0100 +++ scheduler.info 2013-09-02 18:50:44.000000000 +0100 @@ -7,6 +7,7 @@ files[] = scheduler.module files[] = scheduler.views.inc files[] = scheduler.test files[] = scheduler_handler_field_scheduler_countdown.inc +test_dependencies[] = date ; Information added by drupal.org packaging script on 2013-07-28 version = "7.x-1.1+10-dev" --- scheduler.install 2013-07-27 18:42:28.000000000 +0100 +++ scheduler.install 2013-09-02 12:04:07.000000000 +0100 @@ -48,9 +48,13 @@ function scheduler_schema() { */ function scheduler_uninstall() { $variables = array( + 'scheduler_allow_date_only', 'scheduler_date_format', - 'scheduler_field_type', + 'scheduler_date_only_format', + 'scheduler_default_time', 'scheduler_extra_info', + 'scheduler_field_type', + 'scheduler_time_only_format', ); $types = node_type_get_types(); --- scheduler.module 2013-07-27 18:42:28.000000000 +0100 +++ scheduler.module 2013-09-05 17:21:08.000000000 +0100 @@ -1,6 +1,15 @@ variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT), '#size' => 20, '#maxlength' => 20, + '#required' => TRUE, '#description' => t('The input format for the (un)scheduling time/date. See the date() function for formatting options: http://www.php.net/manual/en/function.date.php (only the following format characters are supported (don\'t use \'G\', \'a\' or \'A\' with Date Popup): djmnyYhHgGisaA)'), ); @@ -156,6 +166,29 @@ function scheduler_admin() { $form['scheduler_date_format']['#description'] .= t('If you are using Date Popup, the following time formats are supported: !formats', array('!formats' => $acceptable)); } + // Options for setting date-only with default time. + $form['scheduler_allow_date_only'] = array( + '#type' => 'checkbox', + '#prefix' => '', + '#title' => t('Allow users to enter only a date and provide a default time.'), + '#default_value' => variable_get('scheduler_allow_date_only', FALSE), + '#description' => t('When only a date is entered the time will default to a specified value, but the user can change this if required.'), + ); + + $form['scheduler_default_time'] = array( + '#type' => 'textfield', + '#title' => t('Default time'), + '#default_value' => variable_get('scheduler_default_time', SCHEDULER_DEFAULT_TIME), + '#size' => 20, + '#maxlength' => 20, + '#description' => t('This is the time that will be used if the user does not enter a value. Format: HH:MM:SS.'), + '#states' => array( + 'visible' => array( + ':input[name="scheduler_allow_date_only"]' => array('checked' => TRUE), + ), + ), + ); + $form['scheduler_extra_info'] = array( '#type' => 'textarea', '#title' => t('Extra Info'), @@ -171,16 +204,52 @@ function scheduler_admin() { function scheduler_admin_validate($form, &$form_state) { // Replace all contiguous whitespaces (including tabs and newlines) with a single plain space. $form_state['values']['scheduler_date_format'] = trim(preg_replace('/\s+/', ' ', $form_state['values']['scheduler_date_format'])); + $format = $form_state['values']['scheduler_date_format']; if ($form_state['values']['scheduler_field_type'] == 'date_popup') { - $format = $form_state['values']['scheduler_date_format']; $time_format = date_limit_format($format, array('hour', 'minute', 'second')); $acceptable = date_popup_time_formats(); - - if (!in_array($time_format, $acceptable)) { + if ($time_format && !in_array($time_format, $acceptable)) { form_set_error('scheduler_date_format', t('The Date Popup module only accepts the following formats: !formats', array('!formats' => implode($acceptable, ', ')))); } } + + // If date-only is enabled then check if a valid default time was entered. + // Allow leading zeros to be omitted, eg. 6:30:00 is considered valid. + if ($form_state['values']['scheduler_allow_date_only']) { + if (!$default_time = strptime($form_state['values']['scheduler_default_time'], '%H:%M:%S')) { + form_set_error('scheduler_default_time', t('The default time should be in the format HH:MM:SS')); + } + else { + // Insert any possibly omitted leading zeroes. + $unix_time = mktime($default_time['tm_hour'], $default_time['tm_min'], $default_time['tm_sec']); + $form_state['values']['scheduler_default_time'] = format_date($unix_time, 'custom', 'H:i:s'); + } + } + + // If the form has errors at this point, do not proceed. + if (form_get_errors()) { + return; + } + + // Extract the date part and time part of the full format, for use with the + // default time functionality. Assume the date and time time parts begin + // and end with a letter, but any punctuation between these will be retained. + $time_letters = 'gGhHisaA'; + $time_start = strcspn($format, $time_letters); + $time_length = strlen($format) - strcspn(strrev($format), $time_letters) - $time_start; + $time_only_format = substr($format, $time_start, $time_length); + variable_set('scheduler_time_only_format', $time_only_format); + + $date_letters = 'djFmMnyY'; + $date_start = strcspn($format, $date_letters); + $date_length = strlen($format) - strcspn(strrev($format), $date_letters) - $date_start; + $date_only_format = substr($format, $date_start, $date_length); + variable_set('scheduler_date_only_format', $date_only_format); + drupal_set_message(t('The date part of the Scheduler format is %date_part and the time part is %time_part', array( + '%date_part' => $date_only_format, + '%time_part' => $time_only_format, + ))); } /** @@ -273,6 +342,9 @@ function scheduler_form_alter(&$form, $f $node = $form['#node']; $date_format = variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT); + $date_only_format = variable_get('scheduler_date_only_format', SCHEDULER_DATE_ONLY_FORMAT); + $time_only_format = variable_get('scheduler_time_only_format', SCHEDULER_TIME_ONLY_FORMAT); + $date_only_allowed = variable_get('scheduler_allow_date_only', FALSE); $use_date_popup = _scheduler_use_date_popup(); $internal_date_format = $use_date_popup ? SCHEDULER_DATE_FORMAT : $date_format; @@ -336,11 +408,23 @@ function scheduler_form_alter(&$form, $f ); } - $description_format = t('Format: %time.', array('%time' => format_date(time(), 'custom', $date_format))); + // Define the descriptions depending on whether the time can be skipped. + $descriptions = array(); + if ($date_only_allowed && ($date_only_format != $date_format)) { + $descriptions['format'] = t('Format: %date_only_format or %standard_format.', array('%date_only_format' => format_date(time(), 'custom', $date_only_format), '%standard_format' => format_date(time(), 'custom', $date_format))); + } + else { + $descriptions['format'] = t('Format: %standard_format.', array('%standard_format' => format_date(time(), 'custom', $date_format))); + } + // Show the default time so users know what they will get if they omit the time. + if ($date_only_allowed) { + $default_time = strtotime(variable_get('scheduler_default_time', SCHEDULER_DEFAULT_TIME)); + $descriptions['default'] = t('Leave time blank to use the default of @default_time.', array('@default_time' => format_date($default_time, 'custom', $time_only_format))); + } + if ($publishing_enabled) { - $description_blank = ''; if (!$publishing_required) { - $description_blank .= ' ' . t('Leave blank to disable scheduled publishing.'); + $descriptions['blank'] = t('Leave date blank for no scheduled publishing.'); } $form['scheduler_settings']['publish_on'] = array( @@ -349,14 +433,23 @@ function scheduler_form_alter(&$form, $f '#maxlength' => 25, '#required' => $publishing_required, '#default_value' => isset($defaults->publish_on) && $defaults->publish_on ? format_date($defaults->publish_on, 'custom', $internal_date_format) : '', - '#description' => filter_xss($description_format . $description_blank), + '#description' => filter_xss(implode(' ', $descriptions)), ); + if ($use_date_popup) { + // Make this a popup calendar widget. + $form['scheduler_settings']['publish_on']['#type'] = 'date_popup'; + $form['scheduler_settings']['publish_on']['#date_format'] = $date_format; + $form['scheduler_settings']['publish_on']['#date_year_range'] = '0:+10'; + unset($descriptions['format']); + $form['scheduler_settings']['publish_on']['#description'] = filter_xss(implode(' ', $descriptions)); + unset($form['scheduler_settings']['publish_on']['#maxlength']); + } } if ($unpublishing_enabled) { - $description_blank = ''; + unset($descriptions['blank']); if (!$unpublishing_required) { - $description_blank .= ' ' . t('Leave blank to disable scheduled unpublishing.'); + $descriptions['blank'] = t('Leave date blank for no scheduled unpublishing.'); } $form['scheduler_settings']['unpublish_on'] = array( '#type' => 'textfield', @@ -364,28 +457,16 @@ function scheduler_form_alter(&$form, $f '#maxlength' => 25, '#required' => $unpublishing_required, '#default_value' => isset($defaults->unpublish_on) && $defaults->unpublish_on ? format_date($defaults->unpublish_on, 'custom', $internal_date_format) : '', - '#description' => filter_xss($description_format . $description_blank), + '#description' => filter_xss(implode(' ', $descriptions)), ); - } - if ($use_date_popup) { - // Make this a popup calendar widget if Date Popup module is enabled. - if ($publishing_enabled) { - $form['scheduler_settings']['publish_on']['#type'] = 'date_popup'; - $form['scheduler_settings']['publish_on']['#date_format'] = $date_format; - $form['scheduler_settings']['publish_on']['#date_year_range'] = '0:+10'; - if (!$publishing_required) { - $form['scheduler_settings']['publish_on']['#description'] = t('Leave blank to disable scheduled publishing.'); - } - unset($form['scheduler_settings']['publish_on']['#maxlength']); - } - if ($unpublishing_enabled) { + if ($use_date_popup) { + // Make this a popup calendar widget. $form['scheduler_settings']['unpublish_on']['#type'] = 'date_popup'; $form['scheduler_settings']['unpublish_on']['#date_format'] = $date_format; $form['scheduler_settings']['unpublish_on']['#date_year_range'] = '0:+10'; - if (!$unpublishing_required) { - $form['scheduler_settings']['unpublish_on']['#description'] = t('Leave blank to disable scheduled unpublishing.'); - } + unset($descriptions['format']); + $form['scheduler_settings']['unpublish_on']['#description'] = filter_xss(implode(' ', $descriptions)); unset($form['scheduler_settings']['unpublish_on']['#maxlength']); } } @@ -490,6 +571,27 @@ function _scheduler_strtotime($str) { $date_format = SCHEDULER_DATE_FORMAT; } $time = _scheduler_strptime(trim($str), $date_format); + + // If the time failed using the full $date_format but we allow date-only + // with a default, check if the input matches the "date only" date format. + if (!$time && variable_get('scheduler_allow_date_only', FALSE)) { + $date_only_format = variable_get('scheduler_date_only_format', SCHEDULER_DATE_ONLY_FORMAT); + if ($time = _scheduler_strptime(trim($str), $date_only_format)) { + // A time has been calculated, but also check that there was only a date + // entered and no extra mal-formed time elements. + if (trim($str) == date($date_only_format, $time)) { + // Parse the default time string into elements, then add the total + // offset in seconds to the timestamp. + $default_time = strptime(variable_get('scheduler_default_time', SCHEDULER_DEFAULT_TIME), '%H:%M:%S'); + $time_offset = $default_time['tm_hour'] * 3600 + $default_time['tm_min'] * 60 + $default_time['tm_sec']; + $time += $time_offset; + } + else { + // The date was not the only text entered, so reject it. + $time = FALSE; + } + } + } } else { // $str is empty @@ -1056,3 +1158,16 @@ function scheduler_set_target($source, $ } } +/** + * Implements hook_date_popup_pre_validate_alter(). + */ +function scheduler_date_popup_pre_validate_alter($element, $form_state, &$input) { + // Provide a default time if this is enabled and the time field is empty. + if (variable_get('scheduler_allow_date_only', FALSE) && $element['#array_parents'][0] == 'scheduler_settings' + && $input['date'] != '' && $input['time'] == '') { + // Get the default time as a timestamp number. + $default_time = strtotime(variable_get('scheduler_default_time', SCHEDULER_DEFAULT_TIME)); + // Set the time in the required format just as if the user had typed it. + $input['time'] = format_date($default_time, 'custom', variable_get('scheduler_time_only_format', SCHEDULER_TIME_ONLY_FORMAT)); + } +} --- scheduler.test 2013-07-27 18:42:28.000000000 +0100 +++ scheduler.test 2013-09-05 17:27:15.000000000 +0100 @@ -5,42 +5,132 @@ * Scheduler module test case file. */ class SchedulerTestCase extends DrupalWebTestCase { - public static function getInfo() { - return array( - 'name' => 'Scheduler functionality', - 'description' => 'Publish/unpublish on time.', - 'group' => 'Scheduler', - ); - } + // This class is a common base class for all Scheduler tests. It does not + // contain any tests or the getInfo() function. The actual scheudler test + // groups are called SchedulerNN (NN = 01, 02 etc) and within these classes + // are the actual tests (all starting with the word 'test'). + + /** + * standard setUp. + */ function setUp() { - parent::setUp('scheduler'); - $this->web_user = $this->drupalCreateUser(array('edit own page content', 'create page content', 'administer nodes', 'schedule (un)publishing of nodes')); + parent::setUp('scheduler', 'date', 'date_popup', 'devel'); + + // Create an admin user. + $this->privileged_user = $this->drupalCreateUser(array( + 'access administration pages', + 'administer modules', + 'administer permissions', + 'administer nodes', + 'administer scheduler', + )); + // Create a regular user. + $this->web_user = $this->drupalCreateUser(array( + 'access content', + 'edit own page content', + 'view own unpublished content', + 'create page content', + 'administer nodes', + 'schedule (un)publishing of nodes', + )); + //$this->displayModules(); // Comment out when modules are correct. + // Add scheduler functionality to the page node type. variable_set('scheduler_publish_enable_page', 1); variable_set('scheduler_unpublish_enable_page', 1); + + // Set some default values then save the configuration form. + variable_set('scheduler_field_type', 'textfield'); + variable_set('scheduler_date_format', 'd-m-Y h:i:sA'); + $this->saveConfig(); + } + + /** + * Display the modules and permissions. + */ + public function displayModules() { + debug('Display modules and permissions'); + $this->drupalLogin($this->privileged_user); + // Show the modules page. + $this->drupalGet(url('admin/modules', array('absolute' => TRUE))); + // Show the user permissions page. + $this->drupalGet(url('admin/people/permissions', array('absolute' => TRUE))); + $this->drupalGet(url('admin/people/permissions/roles', array('absolute' => TRUE))); + // Logout. + $this->drupalLogout($this->privileged_user); + } + + /** + * Scheduler Configuration page. + * Go to the scheduler admin settings page and save configuration, so that + * any derived values are also stored. + */ + public function saveConfig() { + $this->drupalLogin($this->privileged_user); + $config = array(); + $this->drupalPost('admin/config/content/scheduler', $config, t('Save configuration')); + // Check that the user has access to the settings page and that the values + // were saved sucessfully. + $this->assertResponse(200, 'User has access to the configuration page.'); + $this->assertText(t('The configuration options have been saved'), 'Configuration settings saved OK.'); + } +} + +/** + * Scheduler01 contains the tests for basic Scheduler functionality. + */ +class Scheduler01 extends SchedulerTestCase { + /** + * Standard information for the test list page. + */ + public static function getInfo() { + return array( + 'name' => 'Scheduler basic functionality', + 'description' => 'Publish/unpublish on time.', + 'group' => 'Scheduler', + ); } + /** + * Test the basic scheduler functions by creating nodes and running cron. + */ function testScheduler() { - // Create node to edit. + // Create node to edit. Set time to now plus 1 hour. $edit = array( - 'title' => 'title', - 'publish_on' => format_date(time() + 3600, 'custom', 'Y-m-d H:i:s'), + 'publish_on' => format_date(time() + 3600, 'custom', variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT)), 'status' => 1, 'promote' => 1, ); + // Login as authenticated web user. + $this->drupalLogin($this->web_user); + + // Test scheduled publishing. $this->helpTestScheduler($edit); + + // Test scheduled unpublishing. $edit['unpublish_on'] = $edit['publish_on']; unset($edit['publish_on']); $this->helpTestScheduler($edit); } + /** + * Helper function for basic functionality. + */ function helpTestScheduler($edit) { + // Add a page. + $edit['title'] = $this->randomName(12); + $body = "publish_on = " . (empty($edit['publish_on']) ? 'empty' : $edit['publish_on']) + . "\nunpublish_on = " . (empty($edit['unpublish_on']) ? 'empty' : $edit['unpublish_on']); $langcode = LANGUAGE_NONE; - $body = $this->randomName(); $edit["body[$langcode][0][value]"] = $body; - $this->drupalLogin($this->web_user); $this->drupalPost('node/add/page', $edit, t('Save')); + + // Show the scheduled content page. + $this->drupalGet(url('user/' . $this->web_user->uid . '/scheduler', array('absolute' => TRUE))); + + // Log out, show the front page for anonymous visitor, then assert that + // the node is correctly published or unpublished. $this->drupalLogout(); $this->drupalGet('node'); if (isset($edit['publish_on'])) { @@ -51,10 +141,16 @@ class SchedulerTestCase extends DrupalWe $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'); + + // Modifiy the scheduler table to a time that has just passed, then run cron. db_update('scheduler')->fields(array($key => time() - 1))->execute(); - $this->assertTrue(db_query('SELECT COUNT(*) FROM {scheduler}')->fetchField(), 'Scheduler table is not empty'); $this->cronRun(); - $this->assertFalse(db_query('SELECT COUNT(*) FROM {scheduler}')->fetchField(), 'Scheduler table is empty'); + + // Show the front page for anonymous visitor, then assert that the node is + // correctly published or unpublished. $this->drupalGet('node'); if (isset($edit['publish_on'])) { $this->assertText($body, t('Node is published')); @@ -62,5 +158,136 @@ class SchedulerTestCase extends DrupalWe else { $this->assertNoText($body, t('Node is unpublished')); } + + // Verify that the scheduler table is empty. + $this->assertFalse(db_query_range('SELECT 1 FROM {scheduler}', 0, 1)->fetchField(), 'Scheduler table is empty'); + + // Login as authenticated web user and show the scheduled content page. + $this->drupalLogin($this->web_user); + $this->drupalGet(url('user/' . $this->web_user->uid . '/scheduler', array('absolute' => TRUE))); + } +} + +/** + * Scheduler02 test the functionality for date-only with default time. + */ +class Scheduler02 extends SchedulerTestCase { + /** + * Standard information for the test list page. + */ + public static function getInfo() { + return array( + 'name' => 'Scheduler date-only with default time', + 'description' => 'Get default time when none entered.', + 'group' => 'Scheduler', + ); + } + + /** + * Test the default time functionality. + */ + public function testDefaultTime() { + // For testing we use an offset of 14 hours 30 minutes (52200 seconds). + // Use a time after 12:00:00 so that the am/pm testing is also validated. + variable_set('scheduler_default_time', '14:30:00'); + $this->offset_seconds = 52200; + + // Define an array of formats to check. + $formats_to_check = array('d-m-Y H:i:s', 'Y-m-d h:i:sa'); + foreach ($formats_to_check as $fmt) { + + // Make sure the derived values are consistent with format. + debug('Change format to ' . $fmt); + variable_set('scheduler_date_format', $fmt); + $this->saveConfig(); + + // Log back in as regular user, not admin. + $this->drupalLogin($this->web_user); + + // First test the regular date entry, without popup. + debug('Testing date entry using a textfield format ' . $fmt); + variable_set('scheduler_field_type', 'textfield'); + $this->checkDefaultTime(); + + // Enable date popup and repeat the test. + debug('Testing date entry using a date popup format ' . $fmt); + variable_set('scheduler_field_type', 'date_popup'); + $this->checkDefaultTime(); + } + } + + /** + * Checks that the default time works as expected. + */ + protected function checkDefaultTime() { + // Define the form fields and date formats we will test according to whether + // date popups have been enabled or not. + $date_format = variable_get('scheduler_date_format', SCHEDULER_DATE_FORMAT); + $date_only_format = variable_get('scheduler_date_only_format', SCHEDULER_DATE_ONLY_FORMAT); + $time_only_format = variable_get('scheduler_time_only_format', SCHEDULER_TIME_ONLY_FORMAT); + $using_popup = variable_get('scheduler_field_type', 'date_popup') == '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'; + $field_format = $using_popup ? $time_only_format : $date_format; + + // 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 as it is when + // passed to t(). This will only work in English. + $publish_validation_message = $using_popup ? t('The value input for field %field is invalid:', array('%field' => 'Publish on')) : "The 'publish on' value does not match the expected format of"; + $unpublish_validation_message = $using_popup ? t('The value input for field %field is invalid:', array('%field' => 'Unpublish on')) : "The 'unpublish on' value does not match the expected format of"; + + // First test with the "date only" functionality disabled. + variable_set('scheduler_allow_date_only', FALSE); + + // Test if entering a time is required. + $edit = array( + 'title' => $this->randomName(), + $publish_date_field => date($date_only_format, strtotime('+1 day', REQUEST_TIME)), + $unpublish_date_field => date($date_only_format, strtotime('+2 day', REQUEST_TIME)), + ); + $langcode = LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $publish_date_field . ' = ' . $edit[$publish_date_field] + . "\n" . $unpublish_date_field . ' = ' . $edit[$unpublish_date_field]; + $this->drupalPost('node/add/page', $edit, t('Save')); + + $this->assertRaw($publish_validation_message, 'By default it is required to enter a time when scheduling content for publication.'); + $this->assertRaw($unpublish_validation_message, 'By default it is required to enter a time when scheduling content for unpublication.'); + + // Allow the user to enter only the date and repeat the test. + variable_set('scheduler_allow_date_only', TRUE); + + $this->drupalPost('node/add/page', $edit, t('Save')); + $this->assertNoRaw( + "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->assertNoRaw( + "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.' + ); + $this->assertRaw( + t('This post is unpublished and will be published @publish_time.', + array('@publish_time' => date($date_format, strtotime('tomorrow', REQUEST_TIME) + $this->offset_seconds)) + ), + '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 scheduler form fields. + $this->clickLink(t('Edit')); + $this->assertFieldByName( + $publish_time_field, + date($field_format, strtotime('tomorrow', REQUEST_TIME) + $this->offset_seconds), + 'The correct default time offset has been added to the date field when scheduling content for publication.' + ); + + $this->assertFieldByName( + $unpublish_time_field, + date($field_format, strtotime('tomorrow +1 day', REQUEST_TIME) + $this->offset_seconds), + 'The correct default time offset has been added to the date field when scheduling content for unpublication.' + ); } + }