diff --git a/simplenews_scheduler.install b/simplenews_scheduler.install index b1b3171..63de9e5 100644 --- a/simplenews_scheduler.install +++ b/simplenews_scheduler.install @@ -24,6 +24,12 @@ function simplenews_scheduler_schema() { 'not null' => TRUE, 'default' => 0, ), + 'next_run' => array( + 'description' => 'The future timestamp the next scheduled newsletter is due to be sent.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), 'activated' => array( 'description' => 'Whether the schedule is active.', 'type' => 'int', @@ -131,3 +137,40 @@ function simplenews_scheduler_update_7000() { db_add_field('simplenews_scheduler', 'title', $field); } } + +/** + * Add the next_run field to the scheduler table and populate it. + */ +function simplenews_scheduler_update_7001() { + // Only act if the field doesn't exist yet: this accounts for the possibility + // it's been added in a 62xx update. + if (!db_field_exists('simplenews_scheduler', 'next_run')) { + // Add the field. + $field = array( + 'description' => 'The future timestamp the next scheduled newsletter is due to be sent.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'initial' => 0, + ); + db_add_field('simplenews_scheduler', 'next_run', $field); + + // Populate the new field with each schedule's next run time. + // Retrieve all records into an associative array keyed by nid. + $schedules = db_query("SELECT * FROM {simplenews_scheduler}")->fetchAllAssoc('nid'); + foreach ($schedules as $nid => $schedule) { + // Clone the object to make a fake one to pass to the API. + $schedule_copy = clone($schedule); + // Remove last_run from the object to force the next_run calculation to + // work from the start_date. This ensures that any error in previous + // edition dates due to bugs with month length is ignored. + // @see http://drupal.org/node/1364784 + unset($schedule_copy->last_run) + + // Get the next run time relative to the request time. + $schedule->next_run = simplenews_scheduler_calculate_next_run_time($schedule_copy, REQUEST_TIME); + + drupal_write_record('simplenews_scheduler', $schedule, 'nid'); + } + } +} diff --git a/simplenews_scheduler.module b/simplenews_scheduler.module index 43dca03..b921e29 100644 --- a/simplenews_scheduler.module +++ b/simplenews_scheduler.module @@ -330,13 +330,17 @@ function simplenews_scheduler_cron() { // Create a new edition. $eid = _simplenews_scheduler_new_edition($newsletter_parent_data->nid, $edition_time); + // Set the next run time for the edition. + $newsletter_parent_data->next_run = simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $now_time); + drupal_write_record('simplenews_scheduler', $newsletter_parent_data, 'nid'); + // Send it. _simplenews_scheduler_send_new_edition($edition_time, $newsletter_parent_data, $eid); } } /** - * Calculates the next edition time. + * Calculates time for the current edition about to be created. * * Because the scheduler runs according to last_run timestamp and the cron * does not run exactly at the scheduled timestamp, this correction fixes @@ -359,6 +363,46 @@ function simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $n } /** + * Calculates time for the next edition to be sent. + * + * This is set in the {simplenews_scheduler} table when a new edition is run, + * for subsequent cron runs to query against. + * + * The time is strictly in the future; that is, if the $now_time is a valid + * edition time, a schedule interval is added to it. This is to allow for cron + * runs that need to calculate the next run time at the time of the current + * edition being sent. + * + * @param $newsletter_parent_data + * A row of data from {simplenews_scheduler}, as returned by + * simplenews_scheduler_get_newsletters_due(). + * @param $now_time + * The time of the operation. + * + * @return + * The calculcated run time for the next future edition. + */ +function simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $now_time) { + // Make an offset string of the format '+1 month'. + $offset_string = "+{$newsletter_parent_data->interval_frequency} {$newsletter_parent_data->send_interval}"; + + // Set a pointer we'll advance and the increment. + if ($newsletter_parent_data->last_run) { + $pointer_time = $newsletter_parent_data->last_run; + } + else { + $pointer_time = $newsletter_parent_data->start_date; + } + + // Add as many offsets until we get into the future. + while ($pointer_time <= $now_time) { + // Add increment to the pointer time. + $pointer_time = strtotime($offset_string, $pointer_time); + } + return $pointer_time; +} + +/** * Get the newsletters that need to have new editions sent. * * This is a helper function for hook_cron that has the current date abstracted @@ -373,52 +417,37 @@ function simplenews_scheduler_calculate_edition_time($newsletter_parent_data, $n * {simplenews_scheduler} table, keyed by newsletter nid. */ function simplenews_scheduler_get_newsletters_due($timestamp) { - // Set the default intervals for scheduling. - $intervals['hour'] = 3600; - $intervals['day'] = 86400; - $intervals['week'] = $intervals['day'] * 7; - $intervals['month'] = $intervals['day'] * date_days_in_month(date('Y'), date('m')); - - $newsletters = array(); - foreach ($intervals as $interval => $seconds) { - // Check daily items that need to be sent. - $sql = "SELECT * FROM {simplenews_scheduler} "; - $sql .= "WHERE activated = :active "; - $sql .= "AND :now - last_run > :interval "; - $sql .= "AND send_interval = :frequency "; - $sql .= "AND start_date <= :now "; - $sql .= "AND (stop_date > :now OR stop_date = 0)"; - - $result = db_query($sql, array(':active' => 1, ':now' => $timestamp, ':interval' => $seconds, ':frequency' => $interval)); - - foreach ($result as $newsletter_parent_data) { - // The node id of the parent node. - $pid = $newsletter_parent_data->nid; - - // Check upon if sending should stop with a given edition number. - $stop = $newsletter_parent_data->stop_type; - $stop_edition = $newsletter_parent_data->stop_edition; - - $edition_count = db_query('SELECT COUNT(*) FROM {simplenews_scheduler_editions} WHERE pid = :pid', array(':pid' => $pid))->fetchField(); - // Don't create new edition if the edition number would exceed the given maximum value. - if ($stop == 2 && $edition_count >= $stop_edition) { - continue; - } + // Get all newsletters that need to be sent. + $result = db_query("SELECT * FROM {simplenews_scheduler} WHERE activated = 1 AND next_run <= :now AND (stop_date > :now OR stop_date = 0)", array(':now' => $timestamp)); - // does this newsletter have something to evaluate to check running condition? - if (strlen($newsletter_parent_data->php_eval)) { - $eval_result = eval($newsletter_parent_data->php_eval); - if (!$eval_result) { - continue; - } - } + foreach ($result as $newsletter_parent_data) { + // The node id of the parent node. + $pid = $newsletter_parent_data->nid; - // Add the number of seconds the newsletter's interval represents. - $newsletter_parent_data->seconds = $seconds; + // Check upon if sending should stop with a given edition number. + $stop = $newsletter_parent_data->stop_type; + $stop_edition = $newsletter_parent_data->stop_edition; - $newsletters[$pid] = $newsletter_parent_data; + $edition_count = db_query('SELECT COUNT(*) FROM {simplenews_scheduler_editions} WHERE pid = :pid', array(':pid' => $pid))->fetchField(); + // Don't create new edition if the edition number would exceed the given maximum value. + if ($stop == 2 && $edition_count >= $stop_edition) { + continue; } + + // does this newsletter have something to evaluate to check running condition? + if (strlen($newsletter_parent_data->php_eval)) { + $eval_result = eval($newsletter_parent_data->php_eval); + if (!$eval_result) { + continue; + } + } + + // Add the number of seconds the newsletter's interval represents. + $newsletter_parent_data->seconds = $seconds; + + $newsletters[$pid] = $newsletter_parent_data; } + return $newsletters; } diff --git a/tests/simplenews_scheduler.test b/tests/simplenews_scheduler.test index 3aa55a3..1d0d04d 100644 --- a/tests/simplenews_scheduler.test +++ b/tests/simplenews_scheduler.test @@ -115,3 +115,140 @@ class SimpleNewsSchedulerNodeCreationTest extends DrupalWebTestCase { $this->assertEqual(1, count($mails), t('Newsletter mail has been sent.')); } } + + +/** + * Unit testing for monthly newsletter next run times. + */ +class SimpleNewsSchedulerNextRunTimeTest extends DrupalUnitTestCase { + + /** + * Provides information about this test. + */ + public static function getInfo() { + return array( + 'name' => 'Next run time: monthly', + 'description' => 'Testing edition times for newsletters due every month and every 2 months.', + 'group' => 'Simplenews Scheduler', + ); + } + + /** + * Declares the module dependencies. + */ + function setUp() { + parent::setUp('simplenews', 'token', 'date', 'simplenews_scheduler'); + } + + /** + * Test a frequency of 1 month. + */ + function testNextRunTimeOneMonth() { + // The start date of the edition. + $this->edition_day = '05'; + $start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00"); + + // Fake newsletter parent data: sets the interval, start date, and frequency. + $newsletter_parent_data = (object) array( + 'nid' => 1, + 'last_run' => 0, + 'activated' => '1', + 'send_interval' => 'month', + 'interval_frequency' => '1', + 'start_date' => $start_date->getTimestamp(), + 'stop_type' => '0', + 'stop_date' => '0', + 'stop_edition' => '0', + 'php_eval' => '', + 'title' => '[node:title] for [current-date:long]', + ); + + // Number of days to run for. + $days = 370; + // Index of the days we've done so far. + $added_days = 0; + // Iterate over days. + while ($added_days <= $days) { + // Create today's date at noon and get the timestamp. + $date = clone($start_date); + $date->add(new DateInterval("P{$added_days}D")); + $timestamp_noon = $date->getTimestamp(); + + // Get the next run time from the API function we're testing. + $next_run_time = simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $timestamp_noon); + //debug($edition_time); + + $this->assertTrue($timestamp_noon < $next_run_time, t('Next run time of !next-run is in the future relative to current time of !now.', array( + '!next-run' => date("Y-n-d H:i:s", $next_run_time), + '!now' => date("Y-n-d H:i:s", $timestamp_noon), + ))); + + $interval = $newsletter_parent_data->interval_frequency * 31 * 24 * 60 * 60; + $this->assertTrue($next_run_time - $timestamp_noon <= $interval, t('Next run timestamp is less than or exactly one month in the future.')); + + // Create a date object from the timestamp. The '@' makes the constructor + // consider the string as a timestamp. + $next_run_date = new DateTime("@$next_run_time"); + $d = date_format($next_run_date, 'd'); + $this->assertEqual($next_run_date->format('d'), $this->edition_day, t('Next run timestamp is on same day of the month as the start date.')); + + $added_days++; + } // while days + } + + /** + * Test a frequency of 2 months. + */ + function testNextRunTimeTwoMonths() { + // The start date of the edition. + $this->edition_day = '05'; + $start_date = new DateTime("2012-01-{$this->edition_day} 12:00:00"); + + // Fake newsletter parent data: sets the interval, start date, and frequency. + $newsletter_parent_data = (object) array( + 'nid' => 1, + 'last_run' => 0, + 'activated' => '1', + 'send_interval' => 'month', + 'interval_frequency' => '2', + 'start_date' => $start_date->getTimestamp(), + 'stop_type' => '0', + 'stop_date' => '0', + 'stop_edition' => '0', + 'php_eval' => '', + 'title' => '[node:title] for [current-date:long]', + ); + + // Number of days to run for. + $days = 370; + // Index of the days we've done so far. + $added_days = 0; + // Iterate over days. + while ($added_days <= $days) { + // Create today's date at noon and get the timestamp. + $date = clone($start_date); + $date->add(new DateInterval("P{$added_days}D")); + $timestamp_noon = $date->getTimestamp(); + + // Get the next run time from the API function we're testing. + $next_run_time = simplenews_scheduler_calculate_next_run_time($newsletter_parent_data, $timestamp_noon); + //debug($edition_time); + + $this->assertTrue($timestamp_noon < $next_run_time, t('Next run time of !next-run is in the future relative to current time of !now.', array( + '!next-run' => date("Y-n-d H:i:s", $next_run_time), + '!now' => date("Y-n-d H:i:s", $timestamp_noon), + ))); + + $interval = $newsletter_parent_data->interval_frequency * 31 * 24 * 60 * 60; + $this->assertTrue($next_run_time - $timestamp_noon <= $interval, t('Next run timestamp is less than or exactly two months in the future.')); + + // Create a date object from the timestamp. The '@' makes the constructor + // consider the string as a timestamp. + $next_run_date = new DateTime("@$next_run_time"); + $d = date_format($next_run_date, 'd'); + $this->assertEqual($next_run_date->format('d'), $this->edition_day, t('Next run timestamp is on same day of the month as the start date.')); + + $added_days++; + } // while days + } +}