Commit ded3e4a9 authored by Robert Rollins's avatar Robert Rollins

Implemented RRULEs, EXDATEs, and RDATEs.

parent 71d5d149
......@@ -113,8 +113,7 @@ function hook_date_ical_icalcreator_component_alter(&$component, &$context) {
}
/**
* Alter an individual feeds object (the source data for a single field)
* that was parsed from an iCal feed.
* Alter the post-parse data for a single field from an iCal feed.
*
* @param $value
* A string or object representing one parsed property of an iCal component.
......@@ -133,3 +132,7 @@ function hook_date_ical_feeds_object_alter(&$value, &$context) {
// ...
}
}
object for the start time.
// ...
}
}
......@@ -157,9 +157,6 @@ function date_ical_libraries_info() {
return $libraries;
}
////////////////////////////////////////////////////////////////////
// Additions from the Parser iCal module.
////////////////////////////////////////////////////////////////////
/**
* Implementation of hook_ctools_plugin_api().
*/
......@@ -209,3 +206,143 @@ function date_ical_feeds_plugins() {
return $info;
}
/**
* Implements hook_feeds_processor_targets_alter().
*
* Adds the "Field Name: Repeat Rule" target to Date Repeat Fields.
*
* @see FeedsNodeProcessor::getMappingTargets().
*/
function date_ical_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if (in_array($info['type'], array('date', 'datestamp', 'datetime')) && isset($info['settings']['repeat']) && $info['settings']['repeat']) {
$targets[$name . ':rrule'] = array(
'name' => t('@name: Repeat Rule', array('@name' => $instance['label'])),
'callback' => 'date_ical_feeds_set_rrule',
'description' => t('The repeat rule for the @name field.', array('@name' => $instance['label'])),
'real_target' => $name,
);
}
}
}
/**
* Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
*
* @param $source
* The FeedsSource object.
* @param $entity
* The node that's being built from the iCal element that's being parsed.
* @param $target
* The machine name of the field into which this RRULE shall be parsed,
* with ":rrule" appended to the end.
* @param $feed_element
* The RRULE string (with optional EXDATEs and RDATEs separated by \n).
*/
function date_ical_feeds_set_rrule($source, $entity, $target, $feed_element) {
// Add the RRULE value to the field in $entity.
list($field_name, $trash) = explode(':', $target, 2);
module_load_include('inc', 'date_api', 'date_api_ical');
$info = field_info_field($field_name);
foreach ($entity->{$field_name} as $lang => $field_array) {
// Add the multiple date values that Date Repeat Field uses to represent recurring dates.
$values = date_ical_build_repeating_dates($feed_element, NULL, $info, $field_array[0]);
foreach ($values as $key => $value) {
$entity->{$field_name}[$lang][$key] = $value;
}
}
}
/**
* 99% copy-pasta from date_repeat_field.module's date_repeat_build_dates() function.
* The only change is that we assume COUNT=52 on indefinitely repeating RRULEs, rather than
* giving up completely.
*/
function date_ical_build_repeating_dates($rrule = NULL, $rrule_values = NULL, $field, $item) {
module_load_include('inc', 'date_api', 'date_api_ical');
$field_name = $field['field_name'];
if (empty($rrule)) {
$rrule = date_api_ical_build_rrule($rrule_values);
}
elseif (empty($rrule_values)) {
$rrule_values = date_ical_parse_rrule(NULL, $rrule);
}
// By the time we get here, the start and end dates have been
// adjusted back to UTC, but we want localtime dates to do
// things like '+1 Tuesday', so adjust back to localtime.
$timezone = date_get_timezone($field['settings']['tz_handling'], $item['timezone']);
$timezone_db = date_get_timezone_db($field['settings']['tz_handling']);
$start = new DateObject($item['value'], $timezone_db, date_type_format($field['type']));
$start->limitGranularity($field['settings']['granularity']);
if ($timezone != $timezone_db) {
date_timezone_set($start, timezone_open($timezone));
}
if (!empty($item['value2']) && $item['value2'] != $item['value']) {
$end = new DateObject($item['value2'], date_get_timezone_db($field['settings']['tz_handling']), date_type_format($field['type']));
$end->limitGranularity($field['settings']['granularity']);
date_timezone_set($end, timezone_open($timezone));
}
else {
$end = $start;
}
$duration = $start->difference($end);
$start_datetime = date_format($start, DATE_FORMAT_DATETIME);
if (!empty($rrule_values['UNTIL']['datetime'])) {
$end = date_ical_date($rrule_values['UNTIL'], $timezone);
$end_datetime = date_format($end, DATE_FORMAT_DATETIME);
}
elseif (!empty($rrule_values['COUNT'])) {
$end_datetime = NULL;
}
else {
// No UNTIL and no COUNT means this is an indefinitely repeating RRULE, which Date Repeat Field doesn't support.
// The best we can do is pretend it has a repeat count of 52 (52 weeks in a year, most repeats are weekly)
// by inserting a COUNT=52 param into the string, right after 'RRULE:'.
$rrule = substr_replace($rrule, 'COUNT=52;', 6, 0);
}
// Split the RRULE into RRULE, EXDATE, and RDATE parts.
$parts = date_repeat_split_rrule($rrule);
$parsed_exceptions = (array) $parts[1];
$exceptions = array();
foreach ($parsed_exceptions as $exception) {
$date = date_ical_date($exception, $timezone);
$exceptions[] = date_format($date, 'Y-m-d');
}
$parsed_rdates = (array) $parts[2];
$additions = array();
foreach ($parsed_rdates as $rdate) {
$date = date_ical_date($rdate, $timezone);
$additions[] = date_format($date, 'Y-m-d');
}
$dates = date_repeat_calc($rrule, $start_datetime, $end_datetime, $exceptions, $timezone, $additions);
$value = array();
foreach ($dates as $delta => $date) {
// date_repeat_calc always returns DATE_DATETIME dates, which is
// not necessarily $field['type'] dates.
// Convert returned dates back to db timezone before storing.
$date_start = new DateObject($date, $timezone, DATE_FORMAT_DATETIME);
$date_start->limitGranularity($field['settings']['granularity']);
date_timezone_set($date_start, timezone_open($timezone_db));
$date_end = clone($date_start);
date_modify($date_end, '+' . $duration . ' seconds');
$value[$delta] = array(
'value' => date_format($date_start, date_type_format($field['type'])),
'value2' => date_format($date_end, date_type_format($field['type'])),
'offset' => date_offset_get($date_start),
'offset2' => date_offset_get($date_end),
'timezone' => $timezone,
'rrule' => $rrule,
);
}
return $value;
}
......@@ -28,7 +28,8 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
),
'dtstart' => array(
'name' => 'Date start',
'description' => 'Start time for the feed item. If also using the "Date end" source, this MUST come before it in the mapping, due to the way some iCal feeds are formatted.',
'description' => 'Start time for the feed item.
If also using the "Date end" source, this MUST come before it in the mapping, due to the way iCal feeds are formatted.',
'date_ical_parse_handler' => 'formatDateTime',
),
'dtend' => array(
......@@ -36,6 +37,13 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
'description' => 'End time for the feed item.',
'date_ical_parse_handler' => 'formatDateTime',
),
'rrule' => array(
'name' => 'Repeat rule',
'description' => 'Describes when and how often this calendar component should repeat.
The date field for the target node must be configrred to support repeating dates, using the Date Repeat Field module (a submodule of Date).
When using this source field, it MUST come after Date start (and Date end, if used) in the mapping.',
'date_ical_parse_handler' => 'formatRrule',
),
'uid' => array(
'name' => 'UID',
'description' => 'UID of feed item',
......@@ -159,12 +167,12 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
*
* @todo is \n \N handling correct?
*/
public function formatText($property_key, $property, DateIcalComponentInterface $item, FeedsParserResult $result, FeedsSource $source) {
public function formatText($property_key, $property, DateIcalIcalcreatorComponent $item, FeedsParserResult $result, FeedsSource $source) {
$context = get_defined_vars();
$text = $property['value'];
$text = str_replace(array("\\n", "\\N"), "\n", $text);
// Allow modules to alter the text before it's mapped to the destination.
// Allow modules to alter the text before it's mapped to the target.
drupal_alter('date_ical_feeds_object', $text, $context);
return $text;
......@@ -175,14 +183,14 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
*
* @return string.
*/
public function formatParamText($property_key, $property, DateIcalComponentInterface $item, DateIcalParserResult $result, FeedsSource $source) {
public function formatParamText($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
$context = get_defined_vars();
$position = strpos($property_key, ':');
$key = substr($property_key, 0, $position);
$attribute = strtoupper(substr($property_key, ++$position));
$text = $property['params'][$attribute];
// Allow modules to alter the param text before it's mapped to the destination.
// Allow modules to alter the param text before it's mapped to the target.
drupal_alter('date_ical_feeds_object', $text, $context);
return $text;
......@@ -193,7 +201,7 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
*
* @return FeedsDateTime
*/
public function formatDateTime($property_key, $property, DateIcalComponentInterface $item, DateIcalParserResult $result, FeedsSource $source) {
public function formatDateTime($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
$context = get_defined_vars();
$date_array = $property['value'];
......@@ -227,7 +235,7 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
$date_array = $start_array;
}
}
// The order in which the source -> destination mapping is set up matters here: dtstart has to come before dtend.
// The order in which the source -> target mapping is set up matters here: dtstart has to come before dtend.
// This has been noted in the description for this source field.
if ($property_key == 'dtstart') {
if ($duration = $item->getProperty('duration')) {
......@@ -273,7 +281,7 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
}
// If this iCal object has no DTEND, but it does have a DURATION, emulate DTEND as DTSTART + DURATION.
// Again, this mechanism requires that dtstart be parsed before dtend in the source->destination mapping.
// Again, this mechanism requires that dtstart be parsed before dtend in the source->target mapping.
if ($property_key == 'dtstart' && !$item->getProperty('dtend') && ($duration = $item->getProperty('duration'))) {
$end_array = iCalUtilityFunctions::_duration2date($date_array, $duration['value']);
$item->setProperty('dtend', $end_array);
......@@ -282,7 +290,7 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
$parsed_datetime = new FeedsDateTime($date_string, $tz);
}
// Allow modules to alter the FeedsDateTime object before it's mapped to the destination.
// Allow modules to alter the FeedsDateTime object before it's mapped to the target.
drupal_alter('date_ical_feeds_object', $parsed_datetime, $context);
return $parsed_datetime;
......@@ -293,15 +301,50 @@ class DateIcalIcalcreatorParser extends DateIcalFeedsParser {
*
* @return array of free tags strings.
*/
public function formatCategories($property_key, $property, DateIcalComponentInterface $item, DateIcalParserResult $result, FeedsSource $source) {
public function formatCategories($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
$context = get_defined_vars();
// Allow modules to alter the categories before they're mapped to the destination.
// Allow modules to alter the categories before they're mapped to the target.
drupal_alter('date_ical_feeds_object', $property['value'], $context);
if (!empty($property['value'])) {
return is_array($property['value']) ? $property['value'] : array($property['value']);
}
}
/**
* Format RRULEs, which specify when and how often the event is repeated.
*
* @return An RRULE string, with any EXDATE and RDATE values appended, separated by \n.
* This is to make the RRULE compatible with date_repeat_split_rrule().
*/
public function formatRrule($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
$context = get_defined_vars();
// Allow modules to alter the RRULE before it's mapped to the target.
drupal_alter('date_ical_feeds_object', $property, $context);
$item->setRrule($property);
/*
// I'm not really sure it makes sense to let users alter these, and since you can have more than one of each,
// it's probbaly not worth the effort to program it correctly. But here's my attempts so far, if anyone wants
// to finish it.
$rdate = $item->getProperty('rdate');
if (!empty($rdate)) {
$context['property_key'] = 'rdate';
drupal_alter('date_ical_feeds_object', $rdate, $context);
$item->setRdate($rdate);
}
$exdate = $item->getProperty('exdate');
if (!empty($rdate)) {
$context['property_key'] = 'exdate';
drupal_alter('date_ical_feeds_object', $exdate, $context);
$item->setExdate($exdate);
}
*/
$rrule = $item->getRrule();
$exdate = $item->getExdate();
$rdate = $item->getRdate();
return trim("$rrule\n$exdate\n$rdate");
}
/**
* Load the iCalcreator library.
......@@ -364,4 +407,31 @@ class DateIcalIcalcreatorComponent implements DateIcalComponentInterface {
// Call the setProperty() method on $this->component with the arguments: $name + $args.
call_user_func_array(array($this->component, "setProperty"), array($name) + $args);
}
// Special set functions, because using component->setRrule() and similar always add an *additional*
// RRULE/RDATE/EXDATE, rather than changing the existing one. This may be a bug in iCalcreator, or
// I may just be calling the functions wrong.
public function setRrule($rrule) {
return $this->component->rrule[0] = $rrule;
}
public function getRrule() {
return trim($this->component->createRrule());
}
public function setRdate($rdate) {
return $this->component->rdate[0] = $rdate;
}
public function getRdate() {
return trim($this->component->createRdate());
}
public function setExdate($exdate) {
return $this->component->exdate[0] = $exdate;
}
public function getExdate() {
return trim($this->component->createExdate());
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment