Skip to content
Snippets Groups Projects
Commit 079da329 authored by Alex Barth's avatar Alex Barth
Browse files

#623448: David Goode, alex_b, et al.: Date mapper.

parent b0d6307f
No related branches found
No related tags found
No related merge requests found
......@@ -3,6 +3,7 @@
Feeds 6.x 1.0 XXXXXXX, 20XX-XX-XX
---------------------------------
- #623448: David Goode, alex_b, et al.: Date mapper.
- #624088: mongolito404, David Goode, alex_b: Imagefield/filefield mapper,
formalize feed elements.
- #584034: aaroncouch, mongolito404: Views integration.
......
......@@ -439,6 +439,9 @@ function _parser_common_syndication_RSS20_parse($feed_XML) {
* The timestamp of the string or the current time if can't be parsed
*/
function _parser_common_syndication_parse_date($date_str) {
// PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
$date_str = str_replace("GMT-", "-", $date_str);
$date_str = str_replace("GMT+", "+", $date_str);
$parsed_date = strtotime($date_str);
if ($parsed_date === FALSE || $parsed_date == -1) {
$parsed_date = _parser_common_syndication_parse_w3cdtf($date_str);
......
<?php
// $Id$
/**
* @file
* On behalf implementation of Feeds mapping API for date
*/
/**
* Implementation of hook_feeds_node_processor_targets_alter().
*
* @see FeedsNodeProcessor::getMappingTargets().
*
* @todo Only provides "end date" target if field allows it.
*/
function date_feeds_node_processor_targets_alter(&$targets, $content_type) {
$info = content_types($content_type);
if (isset($info['fields']) && count($info['fields'])) {
foreach ($info['fields'] as $field_name => $field) {
if (in_array($field['type'], array('date', 'datestamp', 'datetime'))) {
$name = isset($field['widget']['label']) ? $field['widget']['label'] : $field_name;
$targets[$field_name .':start'] = array(
'name' => $name .': Start',
'callback' => 'date_feeds_set_target',
'description' => t('The start date for the !name field. Also use if mapping both start and end.', array('!name' => $name)),
);
$targets[$field_name .':end'] = array(
'name' => $name .': End',
'callback' => 'date_feeds_set_target',
'description' => t('The end date for the @name field.', array('@name' => $name)),
);
}
}
}
}
/**
* Implementation of hook_feeds_set_target().
*
* @param $node
* The target node.
* @param $field_name
* The name of field on the target node to map to.
* @param $feed_element
* The value to be mapped. Should be either a (flexible) date string
* or a FeedsDateTimeElement object.
*
* @todo Support array of values for dates.
*/
function date_feeds_set_target($node, $target, $feed_element) {
list($field_name, $sub_field) = split(':', $target);
if (!($feed_element instanceof FeedsDateTimeElement)) {
if (is_array($feed_element)) {
$feed_element = $feed_element[0];
}
if ($sub_field == 'end') {
$feed_element = new FeedsDateTimeElement(NULL, $feed_element);
}
else {
$feed_element = new FeedsDateTimeElement($feed_element, NULL);
}
}
$feed_element->buildDateField($node, $field_name);
}
\ No newline at end of file
<?php
// $Id$
/**
* Abstract class, defines interface for parsers.
*/
abstract class FeedsParser extends FeedsPlugin {
/**
* Parse content fetched by fetcher.
*
* Extending classes must implement this method.
*
* @param $batch
* FeedsImportBatch returned by fetcher.
* @param FeedsSource $source
* Source information.
*/
public abstract function parse(FeedsImportBatch $batch, FeedsSource $source);
/**
* Clear all caches for results for given source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers can choose to only clear
* caches pertaining to this source.
*/
public function clear(FeedsSource $source) {}
/**
* Declare the possible mapping sources that this parser produces.
*
* @return
* An array of mapping sources, or FALSE if the sources can be defined by
* typing a value in a text field.
*
* Example:
* array(
* 'title' => t('Title'),
* 'created' => t('Published date'),
* 'url' => t('Feed item URL'),
* 'guid' => t('Feed item GUID'),
* )
*/
public function getMappingSources() {
return FALSE;
}
/**
* Get an element identified by $element_key of the given item.
* The element key corresponds to the values in the array returned by
* FeedsParser::getMappingSources().
*/
public function getSourceElement($item, $element_key) {
return isset($item[$element_key]) ? $item[$element_key] : '';
}
}
/**
* Defines an element of a parsed result. Such an element can be a simple type,
* a complex type (derived from FeedsElement) or an array of either.
......@@ -43,56 +98,309 @@ class FeedsElement {
}
/**
* Abstract class, defines interface for parsers.
* Defines a date element of a parsed result (including ranges, repeat).
*/
abstract class FeedsParser extends FeedsPlugin {
class FeedsDateTimeElement extends FeedsElement {
// Start date and end date.
public $start;
public $end;
/**
* Parse content fetched by fetcher.
* Constructor.
*
* Extending classes must implement this method.
* @param $start
* A FeedsDateTime object or a date as accepted by FeedsDateTime.
* @param $end
* A FeedsDateTime object or a date as accepted by FeedsDateTime.
* @param $tz
* A PHP DateTimeZone object.
*/
public function __construct($start = NULL, $end = NULL, $tz = NULL) {
$this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
$this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
}
/**
* Override FeedsElement::getValue().
*/
public function getValue() {
return $this->start;
}
/**
* Implementation of toString magic php method.
*/
public function __toString() {
$val = $this->getValue();
if ($val) {
return $val->format('U');
}
return '';
}
/**
* Merge this field with another. Most stuff goes down when merging the two
* sub-dates.
*
* @param $batch
* FeedsImportBatch returned by fetcher.
* @param FeedsSource $source
* Source information.
* @see FeedsDateTime
*/
public abstract function parse(FeedsImportBatch $batch, FeedsSource $source);
public function merge(FeedsDateTimeElement $other) {
$this2 = clone $this;
if ($this->start && $other->start) {
$this2->start = $this->start->merge($other->start);
}
elseif ($other->start) {
$this2->start = clone $other->start;
}
elseif ($this->start) {
$this2->start = clone $this->start;
}
if ($this->end && $other->end) {
$this2->end = $this->end->merge($other->end);
}
elseif ($other->end) {
$this2->end = clone $other->end;
}
elseif ($this->end) {
$this2->end = clone $this->end;
}
return $this2;
}
/**
* Clear all caches for results for given source.
* Helper method for buildDateField(). Build a FeedsDateTimeElement object
* from a standard formatted node.
*/
protected static function readDateField($node, $field_name) {
$field = content_fields($field_name);
$ret = new FeedsDateTimeElement();
if ($node->{$field_name}[0]['date'] instanceof FeedsDateTime) {
$ret->start = $node->{$field_name}[0]['date'];
}
if ($node->{$field_name}[0]['date2'] instanceof FeedsDateTime) {
$ret->end = $node->{$field_name}[0]['date2'];
}
return $ret;
}
/**
* Build a node's date CCK field from our object.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers can choose to only clear
* caches pertaining to this source.
* @param $node
* The node to build the date field on.
* @param $field_name
* The name of the field to build.
*/
public function clear(FeedsSource $source) {}
public function buildDateField($node, $field_name) {
$field = content_fields($field_name);
$oldfield = FeedsDateTimeElement::readDateField($node, $field_name);
// Merge with any preexisting objects on the field; we take precedence.
$oldfield = $this->merge($oldfield);
$use_start = $oldfield->start;
$use_end = $oldfield->end;
// Set timezone if not already in the FeedsDateTime object
$to_tz = date_get_timezone($field['tz_handling'], date_default_timezone_name());
$temp = new FeedsDateTime($to_tz, NULL);
$db_tz = '';
if ($use_start) {
$use_start = $use_start->merge($temp);
if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
$use_start->setTimezone(new DateTimeZone("UTC"));
}
$db_tz = date_get_timezone_db($field['tz_handling'], $use_start->getTimezone()->getName());
}
if ($use_end) {
$use_end = $use_end->merge($temp);
if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
$use_end->setTimezone(new DateTimeZone("UTC"));
}
if (!$db_tz) {
$db_tz = date_get_timezone_db($field['tz_handling'], $use_end->getTimezone()->getName());
}
}
if (!$db_tz) {
return;
}
$db_tz = new DateTimeZone($db_tz);
if (!isset($node->{$field_name})) {
$node->{$field_name} = array();
}
if ($use_start) {
$node->{$field_name}[0]['timezone'] = $use_start->getTimezone()->getName();
$node->{$field_name}[0]['offset'] = $use_start->getOffset();
$use_start->setTimezone($db_tz);
$node->{$field_name}[0]['date'] = $use_start;
/**
* @todo the date_type_format line could be simplified based upon a patch
* DO issue #259308 could affect this, follow up on at some point.
* Without this, all granularity info is lost.
* $use_start->format(date_type_format($field['type'], $use_start->granularity));
*/
$node->{$field_name}[0]['value'] = $use_start->format(date_type_format($field['type']));
}
if ($use_end) {
// Don't ever use end to set timezone (for now)
$node->{$field_name}[0]['offset2'] = $use_end->getOffset();
$use_end->setTimezone($db_tz);
$node->{$field_name}[0]['date2'] = $use_end;
$node->{$field_name}[0]['value2'] = $use_end->format(date_type_format($field['type']));
}
}
}
/**
* Extend PHP DateTime class with granularity handling, merge functionality and
* slightly more flexible initialization parameters.
*
* This class is a Drupal independent extension of the >= PHP 5.2 DateTime
* class.
*
* @see FeedsDateTimeElement class
*/
class FeedsDateTime extends DateTime {
public $granularity = array();
protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
/**
* Declare the possible mapping sources that this parser produces.
* Overridden constructor.
*
* @return
* An array of mapping sources, or FALSE if the sources can be defined by
* typing a value in a text field.
* @param $time
* time string, flexible format including timestamp.
* @param $tz
* PHP DateTimeZone object, NULL allowed
*/
public function __construct($time = '', $tz = NULL) {
if (is_numeric($time)) {
// Assume timestamp.
$time = "@". $time;
}
// PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
$time = str_replace("GMT-", "-", $time);
$time = str_replace("GMT+", "+", $time);
parent::__construct($time, $tz ? $tz : new DateTimeZone("UTC"));
$this->setGranularityFromTime($time, $tz);
if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
// This tz was given as just an offset, which causes problems
$this->setTimezone(new DateTimeZone("UTC"));
}
}
/**
* This function will keep this object's values by default.
*/
public function merge(FeedsDateTime $other) {
$other_tz = $other->getTimezone();
$this_tz = $this->getTimezone();
// Figure out which timezone to use for combination.
$use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
$this2 = clone $this;
$this2->setTimezone($use_tz);
$other->setTimezone($use_tz);
$val = $this2->toArray();
$otherval = $other->toArray();
foreach (self::$allgranularity as $g) {
if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
// The other class has a property we don't; steal it.
$this2->addGranularity($g);
$val[$g] = $otherval[$g];
}
}
$other->setTimezone($other_tz);
$this2->setDate($val['year'], $val['month'], $val['day']);
$this2->setTime($val['hour'], $val['minute'], $val['second']);
return $this2;
}
/**
* Overrides default DateTime function. Only changes output values if
* actually had time granularity. This should be used as a "converter" for
* output, to switch tzs.
*
* Example:
* array(
* 'title' => t('Title'),
* 'created' => t('Published date'),
* 'url' => t('Feed item URL'),
* 'guid' => t('Feed item GUID'),
* )
* In order to set a timezone for a datetime that doesn't have such
* granularity, merge() it with one that does.
*/
public function getMappingSources() {
return FALSE;
public function setTimezone(DateTimeZone $tz, $force = FALSE) {
// PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
// http://bugs.php.net/bug.php?id=45038
if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
$tz = new DateTimeZone($tz->getName());
}
if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
// this has no time or timezone granularity, so timezone doesn't mean much
// We set the timezone using the method, which will change the day/hour, but then we switch back
$arr = $this->toArray();
parent::setTimezone($tz);
$this->setDate($arr['year'], $arr['month'], $arr['day']);
$this->setTime($arr['hour'], $arr['minute'], $arr['second']);
return;
}
parent::setTimezone($tz);
}
/**
* Get an element identified by $element_key of the given item.
* The element key corresponds to the values in the array returned by
* FeedsParser::getMappingSources().
* Safely adds a granularity entry to the array.
*/
public function getSourceElement($item, $element_key) {
return isset($item[$element_key]) ? $item[$element_key] : '';
public function addGranularity($g) {
$this->granularity[] = $g;
$this->granularity = array_unique($this->granularity);
}
}
\ No newline at end of file
/**
* Removes a granularity entry from the array.
*/
public function removeGranularity($g) {
if ($key = array_search($g, $this->granularity)) {
unset($this->granularity[$key]);
}
}
/**
* Checks granularity array for a given entry.
*/
public function hasGranularity($g) {
return in_array($g, $this->granularity);
}
/**
* Returns whether this object has time set. Used primarily for timezone
* conversion and fomratting.
*
* @todo currently very simplistic, but effective, see usage
*/
public function hasTime() {
return $this->hasGranularity('hour');
}
/**
* Protected function to find the granularity given by the arguments to the
* constructor.
*/
protected function setGranularityFromTime($time, $tz) {
$this->granularity = array();
$temp = date_parse($time);
// This PHP method currently doesn't have resolution down to seconds, so if there is some time, all will be set.
foreach (self::$allgranularity AS $g) {
if (is_numeric($temp[$g]) || ($g == 'zone' && $temp['zone_type'] > 0)) {
$this->granularity[] = $g;
}
}
if ($tz) {
$this->addGranularity('zone');
}
}
/**
* Helper to return all standard date parts in an array.
*/
protected function toArray() {
return array('year' => $this->format('Y'), 'month' => $this->format('m'), 'day' => $this->format('d'), 'hour' => $this->format('H'), 'minute' => $this->format('i'), 'second' => $this->format('s'), 'zone' => $this->format('e'));
}
}
This diff is collapsed.
<?php
// $Id$
/**
* @file
* Test case for CCK date field mapper mappers/content.inc.
*/
require_once(drupal_get_path('module', 'feeds') . '/tests/feeds_mapper_test.inc');
/**
* Class for testing Feeds <em>content</em> 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: 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 RSS', 'daterss');
$this->setSettings('daterss', NULL, array('content_type' => '', 'import_period' => FEEDS_SCHEDULE_NEVER,));
$this->setPlugin('daterss', 'FeedsFileFetcher');
$this->setPlugin('daterss', 'FeedsSyndicationParser');
$this->setSettings('daterss', 'FeedsNodeProcessor', array('content_type' => $typename));
$this->addMappings('daterss', array(
array(
'source' => 'title',
'target' => 'title',
),
array(
'source' => 'description',
'target' => 'body',
),
array(
'source' => 'timestamp',
'target' => 'field_date:start',
),
array(
'source' => 'timestamp',
'target' => 'field_datestamp:start',
),
));
// Import CSV file.
$this->importFile('daterss', $this->absolutePath() .'/tests/feeds/googlenewstz.rss2');
$this->assertText('Created 6 '. $typename .' nodes.');
// Check the imported nodes.
$values = array(
'01/06/2010 - 19:26',
'01/06/2010 - 10:21',
'01/06/2010 - 13:42',
'01/06/2010 - 06:05',
'01/06/2010 - 11:26',
'01/07/2010 - 00:26',
);
for ($i = 1; $i <= 6; $i++) {
$this->drupalGet("node/$i/edit");
$this->assertCCKFieldValue('date', $values[$i-1]);
$this->assertCCKFieldValue('datestamp', $values[$i-1]);
}
}
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);
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment