Index: libraries/common_syndication_parser.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/libraries/common_syndication_parser.inc,v retrieving revision 1.4 diff -u -r1.4 common_syndication_parser.inc --- libraries/common_syndication_parser.inc 21 Dec 2009 00:16:22 -0000 1.4 +++ libraries/common_syndication_parser.inc 6 Jan 2010 18:47:59 -0000 @@ -417,9 +417,11 @@ $item['author'] = $original_author; if (isset($news['pubDate'])) { $item['timestamp'] = _parser_common_syndication_parse_date($news['pubDate']); + $item['timestamp_raw'] = $news['pubDate']; } else { $item['timestamp'] = time(); + $item['timestamp_raw'] = time(); } $item['url'] = trim($original_url); $item['guid'] = $guid; Index: mappers/date.inc =================================================================== RCS file: mappers/date.inc diff -N mappers/date.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ mappers/date.inc 6 Jan 2010 18:47:59 -0000 @@ -0,0 +1,430 @@ + $field) { + // Check to see if the field type is a date (CCK). + if (in_array($field['type'], array('date', 'datestamp', 'datetime'))) { + // Set the name for the source label to be the label of the field or the field name if no label exists + $name = isset($field['widget']['label']) ? $field['widget']['label'] : $field_name; + + foreach($sub_fields as $sub_field_name => $sub_field){ + $targets[$field_name.'#'.$sub_field_name] = array( + 'name' => $name . ': ' . $sub_field[0], + 'callback' => $sub_field[1] ? $sub_field[1] : 'date_feeds_set_target_part', + 'description' => t($sub_field[2] ? $sub_field[2] : 'The !subname for the !name field when set from a UNIX timestamp source.', array('!date_time' => $description, '!name' => $name, '!subname' => $sub_field[0])), + ); + } + } + } + } +} + +/** + * Create a date from a simple string date value. + */ +function date_feeds_set_target_datetime(&$node, $target, $feed_element) { + if (empty($feed_element)) { + return; + } + list($field_name, $sub_field) = split('#', $target); + $field = content_fields($field_name); + + $field_name = $field['field_name']; + $timezone = date_default_timezone_name(); + $to_tz = date_get_timezone($field['tz_handling'], $timezone); + + if (is_array($feed_element)) { + $feed_element = $feed_element[0]; + } + // Pull offset information out of datetime and reset date to + // the known value, UTC. The offset is not useful here. + preg_match('/(([\+\-])(\d{2})?:?(\d{2})?)$/', $feed_element, $matches); + if (!empty($matches)) { + $datetime = trim(str_replace($matches[1], '', $feed_element)); + $value = date_create($datetime, timezone_open('UTC')); + $direction = $matches[2]; + $hours = $matches[3]; + $minutes = $matches[4]; + if ($hours == '00' && $minutes == '00') { + // This is UTC, no adjustment needed. + } + else { + // Bring the date back to UTC. + $direction = ($direction == '+' ? '-' : '+'); + date_modify($value, $direction . trim($hours) .' hours'); + date_modify($value, $direction . trim($minutes) .' minutes'); + } + // Reset it to the appropriate local timezone. + date_timezone_set($value, timezone_open($to_tz)); + } + elseif (stripos($feed_element, "Z") != strlen($feed_element)) { + $value = date_create($feed_element, timezone_open($to_tz)); + } + else { + // A date without a timezone or a non-ISO date, assume local timezone. + $needle = ';'; + $pos = strripos($feed_element, $needle); + $feed_element = substr($feed_element, 0, $pos); + $value = date_create($feed_element, timezone_open($to_tz)); + } + + $date = new feeds_date_constructor(); + $date->construct($field); + $date->set_value('timezone', $timezone); + $date->set_value($sub_field, date_format($value, DATE_FORMAT_DATETIME)); + $date->build(); + + if (!isset($node->{$field_name})) { + $node->$field_name = array($date->value); + } + else { + if ($sub_field == 'start_datetime') { + $node->{$field_name}[0]['date'] = $date->value['date']; + $node->{$field_name}[0]['value'] = $date->value['value']; + } + elseif ($sub_field == 'end_datetime') { + $node->{$field_name}[0]['date2'] = $date->value['date2']; + $node->{$field_name}[0]['value2'] = $date->value['value2']; + } + } + return $node; +} + +/** + * Create a date from a unix timestamp. + */ +function date_feeds_set_target_timestamp(&$node, $target, $feed_element) { + if (empty($feed_element)) { + return; + } + list($field_name, $sub_field) = split('#', $target); + $field = content_fields($field_name); + + $field_name = $field['field_name']; + $timezone = date_default_timezone_name(); + $to_tz = date_get_timezone($field['tz_handling'], $timezone); + + // A date without a timezone, assume local timezone. + $value = date_create("@$feed_element", timezone_open('UTC')); + date_timezone_set($value, timezone_open($to_tz)); + + $date = new feeds_date_constructor(); + $date->construct($field); + $date->set_value('timezone', $timezone); + $date->set_value($sub_field, date_format($value, DATE_FORMAT_DATETIME)); + $date->build(); + + if (!isset($node->{$field_name})) { + $node->$field_name = array($date->value); + } + else { + if ($sub_field == 'start_stamp') { + $node->{$field_name}[0]['date'] = $date->value['date']; + $node->{$field_name}[0]['value'] = $date->value['value']; + } + elseif ($sub_field == 'end_stamp') { + $node->{$field_name}[0]['date2'] = $date->value['date2']; + $node->{$field_name}[0]['value2'] = $date->value['value2']; + } + } + return $node; +} + +/** + * Create a date from an iCal feed array. + */ +function date_feeds_set_target_ical(&$node, $target, $feed_element) { + if (empty($feed_element)) { + return; + } + list($field_name, $sub_field) = split('#', $target); + $field = content_fields($field_name); + // There are lots of available elements in the feed, so do some checking. + + + include_once(drupal_get_path('module', 'date_api') .'/date_api_ical.inc'); + + // If the feed element contains the top-level VEVENT, + // pick out the part we need, the DATE array. + if (array_key_exists('DATE', $feed_element)) { + $feed_element = $feed_element['DATE']; + } + // If the feed element contains only the DTSTART or DTEND sub-array, + // we don't know which one we have, not enough information to process. + // If it is any other part of the VEVENT array, or the date is empty + // we don't have any date information and there is nothing to do. + if (!array_key_exists('DTSTART', $feed_element)) { + return; + } + elseif (empty($feed_element['DTSTART']['datetime'])) { + return; + } + + $field_name = $field['field_name']; + + $timezone = $feed_element['DTSTART']['tz']; + if (empty($timezone)) $timezone = date_default_timezone_name(); + + $date = new feeds_date_constructor(); + $date->construct($field); + $date->set_value('timezone', $timezone); + if (strlen($feed_element['DTSTART']['datetime'] < 11)) { + $date->set_value('start_date', $feed_element['DTSTART']['datetime']); + } + else { + $date->set_value('start_datetime', $feed_element['DTSTART']['datetime']); + } + if (!empty($feed_element['DTEND']) && !empty($feed_element['DTEND']['datetime'])) { + if (strlen($feed_element['DTEND']['datetime'] < 11)) { + $date->set_value('end_date', $feed_element['DTEND']['datetime']); + } + else { + $date->set_value('end_datetime', $feed_element['DTEND']['datetime']); + } + } + $date->set_value('all_day', $feed_element['DTSTART']['all_day']); + $date->build(); + $node->$field_name = array($date->value); + + if (array_key_exists('RRULE', $feed_element) && !empty($feed_element['RRULE']) && module_exists('date_repeat')) { + + include_once('./'. drupal_get_path('module', 'date_repeat') .'/date_repeat_calc.inc'); + include_once('./'. drupal_get_path('module', 'date') .'/date_repeat.inc'); + + // Explode the RRULE into parts so we can analyze it. + $rrule = $feed_element['RRULE']['DATA'] . (!empty($feed_element['EXDATE']) ? "/n". $feed_element['EXDATE'] : ""); + $form_values = date_ical_parse_rrule($field, $rrule); + + // Make sure we don't end up with thousands of values with RRULES + // that have no UNTIL or COUNT. + // TODO: could be adjusted or made configurable later. + $max = date_now(); + $max_repeats = 52; + date_modify($max, '+5 years'); + $until = date_format($max, 'Y-m-d H:i:s'); + if (empty($form_values['COUNT']) && (empty($form_values['UNTIL']) || $until < $form_values['UNTIL']['datetime'])) { + $form_values['UNTIL'] = array('datetime' => $until, 'tz' => 'UTC'); + $form_values['COUNT'] = $max_repeats; + } + elseif (empty($form_values['COUNT'])) { + $form_values['COUNT'] = $max_repeats; + } + elseif (empty($form_values['UNTIL'])) { + $form_values['UNTIL'] = array('datetime' => $until, 'tz' => 'UTC'); + } + + // Reconstruct the RRULE with our changes and build the repeats. + $rrule = date_api_ical_build_rrule($form_values); + $node_field = $node->{$field_name}[0]; + $values = date_repeat_build_dates($rrule, $form_values, $field, $node_field); + $node->$field_name = $values; + } +} + +/** + * Create a date from a collection of date parts. + * The most complicated scenario since we don't know what parts + * we are getting or in what order. We'll create date information + * and retain it from one element to the next to gradually build + * up the date values. We use the current date as a default value + * when nothing else is known. + */ +function date_feeds_set_target_part(&$node, $target, $feed_element) { + if (empty($feed_element)) { + return; + } + list($field_name, $sub_field) = split('#', $target); + $field = content_fields($field_name); + + static $date; + + $field_name = $field['field_name']; + if (empty($node->$field_name)) { + $date = new feeds_date_constructor(); + $date->construct($field); + } + $date->set_value($sub_field, $feed_element); + $date->build(); + $node->$field_name = array($date->value); +} + +/** + * Construct a date from a collection of date parts + * that could be provided in any order, without knowing + * ahead of time exactly which parts will be set. + * + * Functions by defaulting to current date for missing values. + * Retains previous values and continuously resets them as + * more information becomes known. + * + * Call build() at any time to construct a date from the + * currently available values. + * + * @todo: This should live in date module. + * @todo: only renamed feeds_date_constructor from date_constructor to prevent error if loaded at same time as (deprecated) feed_api + */ +class feeds_date_constructor { + var $field = NULL; + var $timezone = NULL; + var $start_date = NULL; + var $end_date = NULL; + var $start_time = NULL; + var $end_time = NULL; + var $start_datetime = NULL; + var $end_datetime = NULL; + var $value = NULL; + var $format = NULL; + var $db_tz = NULL; + var $to_tz = NULL; + + function construct($field) { + $now = date_now(); + //$this->start_date = date_format($now, DATE_FORMAT_DATE); + $this->start_time = '00:00:00'; + $this->start_datetime = $this->start_date .' '. $this->start_time; + $this->timezone = date_default_timezone_name(); + $this->field = $field; + $this->format = date_type_format($field['type']); + } + + function set_value($key, $value) { + if (empty($value)) { + return; + } + switch ($key) { + case 'timezone': + $this->timezone = $value; + break; + case 'all_day': + if (!empty($value) && strtolower($value) != 'false' && $value !== FALSE) { + $this->start_time = '00:00:00'; + $this->end_time = '00:00:00'; + if (!empty($this->start_date)) { + $this->start_datetime = $this->start_date .' '. $this->start_time; + } + if (!empty($this->end_date)) { + $this->end_datetime = $this->end_date .' '. $this->end_time; + } + $this->all_day = TRUE; + } + break; + case 'start_date': + $this->start_date = $value; + $this->start_datetime = $this->start_date .' '. $this->start_time; + break; + case 'start_time': + $this->start_time = $value; + $this->start_datetime = $this->start_date .' '. $this->start_time; + break; + case 'end_date': + $this->end_date = $value; + if (empty($this->end_time)) { + $this->end_time = $this->start_time; + } + $this->end_datetime = $this->end_date .' '. $this->end_time; + break; + case 'end_time': + $this->end_time = $value; + if (empty($this->end_date)) { + $this->end_date = $this->start_date; + } + $this->end_datetime = $this->end_date .' '. $this->end_time; + break; + case 'start_datetime': + $this->start_datetime = $value; + $this->start_date = NULL; + $this->start_time = NULL; + break; + case 'end_datetime': + $this->end_datetime = $value; + $this->end_date = NULL; + $this->end_time = NULL; + break; + } + } + + function set_timezone() { + $this->db_tz = date_get_timezone_db($this->field['tz_handling'], $this->timezone); + $this->to_tz = date_get_timezone($this->field['tz_handling'], $this->timezone); + } + + /** + * Create date objects with the known values. + */ + function build() { + $this->set_timezone(); + if (empty($this->start_datetime) && !empty($this->start_date)) { + $this->start_datetime = trim($this->start_date .' '. $this->start_time); + } + if (empty($this->end_date) && empty($this->end_datetime)) { + $this->end_date = $this->start_date; + } + if (empty($this->end_time) && empty($this->end_datetime)) { + $this->end_time = $this->start_time; + } + if (empty($this->end_datetime) && (!empty($this->end_date) || !empty($this->end_time))) { + $this->end_datetime = trim($this->end_date .' '. $this->end_time); + } + elseif (empty($this->end_datetime)) { + $this->end_datetime = $this->start_datetime; + } + + // We don't know exactly what format the provided date used, + // so use the native date_create() where available, which does a + // pretty good job of interpreting non-standard date values. Where the + // native date_create() is not available (PHP 4, PHP 5.0 and PHP 5.1) + // use strtotime() as the best available interpreter of irregularly + // formated date values. + if (version_compare(PHP_VERSION, '5.2', '<')) { + $timestamp = strtotime($this->start_datetime); + $this->value['date'] = date_create("@$timestamp"); + $timestamp = strtotime($this->end_datetime); + $this->value['date2'] = date_create("@$timestamp"); + } + else { + $this->value['date'] = date_create($this->start_datetime, timezone_open($this->to_tz)); + $this->value['date2'] = date_create($this->end_datetime, timezone_open($this->to_tz)); + } + + if (isset($this->value['date'])) { + $this->value['offset'] = date_offset_get($this->value['date']); + date_timezone_set($this->value['date'], timezone_open($this->db_tz)); + $this->value['value'] = date_format($this->value['date'], $this->format); + } + if (isset($this->value['date2'])) { + $this->value['offset2'] = date_offset_get($this->value['date2']); + date_timezone_set($this->value['date2'], timezone_open($this->db_tz)); + $this->value['value2'] = date_format($this->value['date2'], $this->format); + } + $this->value['timezone'] = $this->to_tz; + } +} Index: plugins/FeedsSyndicationParser.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/feeds/plugins/FeedsSyndicationParser.inc,v retrieving revision 1.10 diff -u -r1.10 FeedsSyndicationParser.inc --- plugins/FeedsSyndicationParser.inc 20 Dec 2009 23:54:44 -0000 1.10 +++ plugins/FeedsSyndicationParser.inc 6 Jan 2010 18:47:59 -0000 @@ -45,6 +45,10 @@ 'name' => t('Published date'), 'description' => t('Published date as UNIX time GMT of the feed item.'), ), + 'timestamp_raw' => array( + 'name' => t('Published date (Raw)'), + 'description' => t('Published date as a raw string.'), + ), 'url' => array( 'name' => t('Item URL (link)'), 'description' => t('URL of the feed item.'), Index: tests/feeds_mapper_date.test =================================================================== RCS file: tests/feeds_mapper_date.test diff -N tests/feeds_mapper_date.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ tests/feeds_mapper_date.test 6 Jan 2010 18:47:59 -0000 @@ -0,0 +1,107 @@ +content mapper. + * + * @todo: Add test method iCal + * @todo: Add test method for end date + */ +class FeedsMapperDateTestCase extends FeedsMapperTestCase { + + public static function getInfo() { + return array( + 'name' => t('Mapper: CCK Date'), + 'description' => t('Test Feeds Mapper support for CCK date fields'), + 'group' => t('Feeds'), + ); + } + + /** + * Set up the test. + */ + public function setUp() { + // Call parent setup with the required module. + parent::setUp('feeds', 'feeds_ui', 'ctools', 'content', 'date_api', 'date'); + + // Create user and login. + $this->drupalLogin($this->drupalCreateUser( + array( + 'administer content types', + 'administer feeds', + 'administer nodes', + 'administer site configuration', + ) + )); + } + + /** + * Basic test loading a single entry CSV file. + */ + public function test() { + + // Create content type. + $typename = $this->createContentType(NULL, array( + 'date' => 'date', + 'datestamp' => 'datestamp', + 'datetime' => 'datetime', + )); + + // Create and configure importer. + $this->createFeedConfiguration('Date CSV', 'csv'); + $this->setSettings('csv', NULL, array('content_type' => '','import_period' => FEEDS_SCHEDULE_NEVER,)); + $this->setPlugin('csv', 'FeedsFileFetcher'); + $this->setPlugin('csv', 'FeedsCSVParser'); + $this->setSettings('csv', 'FeedsNodeProcessor', array('content_type' => $typename)); + $this->addMappings('csv', array( + array( + 'source' => 'format', + 'target' => 'title', + ), + array( + 'source' => 'value', + 'target' => 'body', + ), + array( + 'source' => 'value', + 'target' => 'field_date#', + ), + array( + 'source' => 'value', + 'target' => 'field_datetime#', + ), + array( + 'source' => 'value', + 'target' => 'field_datestamp#', + ), + )); + + // Import CSV file. + $this->importFile('csv', $this->absolutePath() .'/tests/feeds/date.csv'); + $this->assertText('Created 13 '. $typename .' nodes.'); + + // Check the imported nodes. + $date = '12/01/2009 - 12:41'; + for($i = 1; $i <= 13; $i++) { + $this->drupalGet("node/$i/edit"); + $this->assertCCKFieldValue('date', $date); + $this->assertCCKFieldValue('datetime', $date); + $this->assertCCKFieldValue('datestamp', $date); + } + } + +protected function getFormFieldsNames($field_name, $index) { + if(in_array($field_name, array('date', 'datetime', 'datestamp'))) { + return array("field_{$field_name}[{$index}][value][date]"); + } else { + return parent::getFormFieldsNames($field_name, $index); + } + } +}