Commit b2c6c1cf authored by Robert Rollins's avatar Robert Rollins
Browse files

Date iCal 3.0 is finished!

This commit adds the migration code that converts all Feeds Importers which used
the DateIcalIcalcreatorParser plugin to use DateiCalFeedsImporter instead. The
old classes have been removed.
parent 690df398
Date iCal
This module allows users to create iCal feeds in Views, and import iCal feeds
from other sites using the Feeds module. Any entity can act as the source of
This module allows users to export iCal feeds with Views, and import iCal feeds
from other sites with the Feeds module. Any entity can act as the source of
events for an iCal feed, as long as that entity contains a Date field. Date
iCal creates a new iCal "view mode" for all entities, which is used to format
the Description field of the events in the iCal feed.
the Description field of the events in the iCal feed (when using the iCal
Entity plugin).
For an easier-to-read HTML version of these instructions, please go to
http://www.drupal.org/project/date_ical and click the "Read documentation" link
......@@ -13,7 +14,8 @@ in the Resources section of the right sidebar.
INSTALLATION
Date iCal has several required dependencies, and an optional one:
- The Views, Entity API, Libraries API (version 2.0), and Date modules are required.
- The Views (version 3.5+), Entity API, Libraries API (version 2.0), and Date
modules are required.
- The iCalcreator library is required.
- PHP 5.3 is required by the iCalcreator library to properly handle timezone
data. Date iCal *might* work with PHP 5.2, but that configuration is
......@@ -36,15 +38,17 @@ Performance and click the "Clear all caches" button. This is necessary because
libraries are cached, and you may see confusing behavior from Date iCal if the
iCalcreator library gets cached at a bad time.
To confirm that iCalcreator is installed correctly, log in to your Drupal site
and navigate to the admin/reports/status page. If the row titled "Date iCal" is
green, Date iCal is ready to go. If it's red, the iCalcreator library is not
properly installed. If it's missing, you'll need to enable Date iCal and then
come back to this page.
EXPORTING AN ICAL FEED USING Views
There are two plugins that export iCal feeds. You can use either one, though
the iCal Fields plugin is a bit more versatile.
HOW TO CREATE AN ICAL FEED (using the iCal Entities plugin)
HOW TO EXPORT AN ICAL FEED USING THE iCal Entities PLUGIN
1. Go to the Manage Display page for the content type you want to export in an
iCal feed. On the "Default" tab, check the box for "iCal" in the section
......@@ -89,7 +93,7 @@ HOW TO CREATE AN ICAL FEED (using the iCal Entities plugin)
download a .ics file with the events, instead of loading the events directly
into a calendar app.
HOW TO CREATE AN ICAL FEED (using the iCal Fields plugin)
HOW TO EXPORT AN ICAL FEED USING THE iCal Fields PLUGIN
1-6.These steps are the same as above.
7. Add views fields for each piece of information that you want to populate your
iCal feed with. A Date field is required, and fields that will act as the
......@@ -101,9 +105,9 @@ HOW TO CREATE AN ICAL FEED (using the iCal Fields plugin)
10+ These steps are the same as above.
HOW TO IMPORT AN ICAL FEED FROM ANOTHER SITE
- Install the Feeds module, which acts as the framework around Date iCal's
calendar import functionality.
IMPORTING ICAL FEEDS FROM ANOTHER SITE USING Feeds
- Install the Feeds module, which is the framework upon which Date iCal's
import functionality is built.
- Login to your Drupal site and navigate to the admin/structure/feeds page.
- Click the "Add importer" link, and give it a name and description.
- Clicking "Create" will bring you to the general Feeds importer settings page.
......@@ -122,19 +126,16 @@ HOW TO IMPORT AN ICAL FEED FROM ANOTHER SITE
- Now click the "Mapping" link at the bottom of the left sidebar. This page is
where you'll define how iCal event properties get mapped into your nodes'
fields. Expand the "Legend" for a detailed description of each source and
target field. Sources are the attributes available in iCal event objects.
Targets are the fields in your nodes.
target field. Sources are the attributes available in iCal event objects,
and Targets are the fields in your nodes.
- Most of this setup is going to be dependant upon how your content type's
fields are configured, but there are some universal requirements:
1) You must map the "UID" source to the "GUID" target. Then, after clicking
1) You MUST map the "UID" source to the "GUID" target. Then, after clicking
"Add", click the gear-shaped button that appears in the new table row,
and check the "Unique" checkbox. Then click "Update", and then before
you add any more mappings, click "Save" at the bottom of the page.
2) If you're going to map both the "Date start" and "Date end" sources, you
MUST ensure that the "Date start" mapping is above the "Date end" mapping
in the table. This is because of an implementation detail in the parser.
3) It's a good idea to map the "Summary" source to the "Title" target, and
the "Description" source to whatever field is the "body" of the node.
3) It's a good idea to map the "Summary/Title" source to the "Title" target,
and the "Description" source to whatever field is the "body" of the node.
- Once you've completed all the mappings, click the "Save" button on the
bottom left side of the page.
- Now you can import the iCal feed into nodes by going to the /import page of
......@@ -146,12 +147,15 @@ HOW TO IMPORT AN ICAL FEED FROM ANOTHER SITE
you do, you've successfully set up your iCal importer. If you get some other
message, you'll need to tweak the importer's settings.
Remember, you have to map the UID source to the GUID target, and make it
unique, or your imports won't work!
Additional Notes:
The Feeds plugin was originally written by ekes, for the "iCal feed parser"
module (http://www.drupal.org/project/parser_ical). It was modified and
improved for Date iCal by coredumperror.
improved for Date iCal by coredumperror. In Date iCal 3.0, the plugin was
re-written from scratch to conform to the Feeds APIs.
At this time, Date iCal supports outputting iCal calendars only through Views.
To put an "Add to calendar" button on individual event nodes, try the
......@@ -159,6 +163,9 @@ To put an "Add to calendar" button on individual event nodes, try the
the instructions created by the estimable nmc at:
http://nmc-codes.blogspot.ca/2012/11/creating-ical-feed-for-single-node-in.html
The Feeds Tamper module is quite useful for manipulating the data in imported
iCal feeds.
Developers who wish to implement more powerful manipulation of event data can
read the date_ical.api.php file to learn about the various alter hooks that
date_ical exposes.
name = Date iCal
description = Enables users to export iCal feeds using Views, and import iCal feeds using Feeds.
description = Enables export of iCal feeds using Views, and import of iCal feeds using Feeds.
package = Date/Time
php = 5.3
core = 7.x
......@@ -10,7 +10,7 @@ dependencies[] = libraries (>=7.x-2.0)
dependencies[] = date
dependencies[] = date_api
; Date iCal doesn't actually depend on Feeds, but it's iCal import functionality won't be usable without it.
; Date iCal doesn't actually depend on Feeds, but its iCal import functionality won't be usable without it.
;dependencies[] = feeds
; Includes for iCal feed export using Views
......@@ -20,5 +20,3 @@ files[] = includes/date_ical_plugin_style_ical_feed.inc
; Includes for iCal feed import using Feeds
files[] = includes/DateIcalFeedsParser.inc
files[] = includes/DateIcalFeedsParserOld.inc
files[] = includes/DateIcalIcalcreatorParser.inc
......@@ -16,7 +16,8 @@ function date_ical_requirements($phase) {
switch ($phase) {
case 'runtime':
if (($library = libraries_detect('iCalcreator')) && !empty($library['installed'])) {
$library = libraries_detect('iCalcreator');
if ($library && !empty($library['installed'])) {
$requirements['date_ical'] = array(
'title' => $t('Date iCal'),
'value' => $t('iCalcreator library is installed, version: @version found', array('@version' => $library['version'])),
......@@ -27,7 +28,9 @@ function date_ical_requirements($phase) {
$requirements['date_ical'] = array(
'title' => $t('Date iCal'),
'value' => $t('iCalcreator library could not be found, check the installation instructions for the Date iCal module.'),
'description' => $t('The error message was: @error<br>!error_message', array('@error' => $library['error'], '!error_message' => $library['error message'])),
'description' => $t('The error message was: @error<br>!error_message',
array('@error' => $library['error'], '!error_message' => $library['error message'])
),
'severity' => REQUIREMENT_ERROR,
);
}
......@@ -43,3 +46,59 @@ function date_ical_requirements($phase) {
function date_ical_enable() {
cache_clear_all('plugins:feeds:plugins', 'cache');
}
/*****************************************************************************
* UPDATE HOOKS
****************************************************************************/
/**
* Migrates all iCal feed importers for from Date iCal 2.x to 3.0.
* <br>
* PLEASE NOTE: If any of your importers are defined by Features, you'll need
* to update their feature(s) to match.
*/
function date_ical_update_7300() {
// Rebuild the registry immediately, so that we don't get fatal errors when
// Drupal attempts to instantiate the old feeds plugin classes.
registry_rebuild();
cache_clear_all('plugins:feeds:plugins', 'cache');
// Update all the importers which used DateIcalIcalcreatorParser to use
// DateiCalFeedsParser instead.
$importer_data = ctools_export_load_object('feeds_importer', 'all');
foreach ($importer_data as $key => $value) {
$importer = feeds_importer($key);
$config = $importer->getConfig();
if ($config['parser']['plugin_key'] == 'DateIcalIcalcreatorParser') {
$importer->setPlugin('DateiCalFeedsParser');
// Update the mappings: the source keys are now capitalized.
$processor = $importer->processor;
$processor_config = $processor->getconfig();
foreach ($processor_config['mappings'] as &$mapping) {
$mapping['source'] = strtoupper($mapping['source']);
}
$processor->setConfig($processor_config);
$importer->save();
// When this importer object got created, a warning was issued about its
// parser plugin being missing. We corrected that warning above, so we
// should clear it out to avoid potential confusion.
$messages = drupal_get_messages('warning');
foreach($messages['warning'] as $warning) {
// Calling drupal_get_messages() removed *every* warning from the
// message queue, so we need to re-issue any warnings that aren't
// about missing Feeds plugins.
if (strpos($warning, 'Missing Feeds plugin') === FALSE) {
drupal_set_message($warning, 'warning');
}
}
$t = get_t();
$importer_link = l($key, "admin/structure/feeds/$key");
drupal_set_message($t('Date iCal updated the parser plugin for !importer.
If that importer is defined by a feature, you will need to update that feature to match.', array('!importer' => $importer_link))
);
}
}
}
......@@ -7,22 +7,23 @@
/**
* The version number of the current release. This is inserted into the PRODID
* value of the iCal feeds created by Date iCal.
* value of the iCal feeds created by Date iCal. It's primarily used for
* debugging.
*/
define('DATE_ICAL_VERSION', '3.0-dev');
/**
* Exception for when the date field for a row in the ical_fields row plugin is blank.
* Exception class for generic exceptions thrown by this module.
*/
class BlankDateFieldException extends Exception { }
class DateIcalException extends Exception {}
/**
* Generic DateIcalFeedsParser exceptions.
* Exception for when the date field for a row in the ical_fields row plugin is blank.
*/
class DateIcalException extends Exception {}
class BlankDateFieldException extends DateIcalException {}
/**
* DateIcalFeedsParser failed to parse some part of iCal.
* Exception thrown when the Feeds parser plugin fails.
*/
class DateIcalParseException extends DateIcalException {}
......@@ -50,26 +51,6 @@ function date_ical_views_api() {
function date_ical_feeds_plugins() {
$path = drupal_get_path('module', 'date_ical') . '/includes';
$info = array();
$info['DateIcalFeedsParserOld'] = array(
'hidden' => TRUE,
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'DateIcalFeedsParserOld',
'file' => 'DateIcalFeedsParserOld.inc',
'path' => $path,
),
);
$info['DateIcalIcalcreatorParser'] = array(
'name' => 'iCal parser (old)',
'description' => t('Use the iCalcreator library to parse iCal feeds.'),
'help' => 'Parse iCal feeds.',
'handler' => array(
'parent' => 'DateIcalFeedsParserOld',
'class' => 'DateIcalIcalcreatorParser',
'file' => 'DateIcalIcalcreatorParser.inc',
'path' => $path,
),
);
$info['DateiCalFeedsParser'] = array(
'name' => 'iCal parser',
'description' => t('Parse iCal feeds.'),
......
<?php
/**
* @file
* Basic classes.
*/
/**
* Parent class for Feeds integration.
*/
abstract class DateIcalFeedsParserOld extends FeedsParser {
/**
* The output sources the parser offers.
*
* array(
* 'feeds_output_key' => array(
* 'name' => 'Human readable name of output source.',
* 'description' => 'Longer description of source.',
* 'date_ical_parse_handler' => 'Method callback for parsing source before handing to feeds.',
* ),
* );
*/
static protected $sources;
/**
* Implementation of FeedsParser::getMappingSources().
*/
public function getMappingSources() {
// Quirky work around.
// Want to have the sources as a property of the class,
// but can't declare them with t().
$sources = $this::$sources;
foreach ($sources as &$source) {
foreach ($source as $key => &$value) {
if ($key == 'name' || $key == 'description') {
$value = t($value);
}
}
}
// Call parent::getMappingSources() to trigger additional target creation.
return $sources + parent::getMappingSources();
}
/**
* Override FeedsParser::getSourceElement().
*/
public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $property_key) {
// Allow parent method to handle any mappings based on the parent node.
if (substr($property_key, 0, 7) == 'parent:') {
return parent::getSourceElement($source, $result, $property_key);
}
// Otherwise retrieve the current DateIcalComponent from the DateIcalParserResult.
if ($item = $result->currentItem()) {
if ($position = strpos($property_key, ':')) {
$key = substr($property_key, 0, $position);
$attribute = substr($property_key, ++$position);
}
else {
$key = $property_key;
}
// and use listed handler to get source output
$handler = $this::$sources[$property_key]['date_ical_parse_handler'];
$property = $item->getProperty($key);
if (empty($property)) {
// $property will be empty if the mapping is set up to parse optional source
// components (e.g. RRULE), and this particular VEVENT doesn't have one.
return '';
}
else {
return $this->$handler($property_key, $property, $item, $result, $source);
}
}
}
}
interface DateIcalComponentInterface {
public function getComponentType();
public function setProperty($name, $args);
public function getProperty($name);
}
class DateIcalParserResult extends FeedsParserResult {
// Feed extension timezone (X-WR-TIMEZONE)
public $timezone;
// Feed RFC 5545 timezones; we can't use these at
// present, only PHP timezonedb tz will be actually recognized.
// So this is more here as reminder/explanation.
public $timezones;
}
<?php
/**
* @file
* Classes implementing Date iCal's iCalcreator-based parser functionality.
*
* @TODO: Figure out how to incorporate VVENUE information into the parser.
*/
class DateIcalIcalcreatorParser extends DateIcalFeedsParserOld {
/**
* Output sources this parser offers.
*
* Includes additional field for the handler for output.
*
* NOTE: The name and description strings would be t()'d, but PHP doesn't allow that:
* http://stackoverflow.com/questions/3960323/why-dont-php-attributes-allow-functions
*
* @see DateIcalFeedsParser::getMappingSources().
* @see DateIcalFeedsParser::getSourceElement().
*/
static protected $sources = array(
'summary' => array(
'name' => 'Summary',
'description' => 'A short summary, or title, for the calendar component.',
'date_ical_parse_handler' => 'formatText',
),
'description' => array(
'name' => 'Description',
'description' => 'A more complete description of the calendar component than that provided by the "summary" property.',
'date_ical_parse_handler' => 'formatText',
),
'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 iCal feeds are formatted.',
'date_ical_parse_handler' => 'formatDateTime',
),
'dtend' => array(
'name' => 'Date end',
'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',
'date_ical_parse_handler' => 'formatText',
),
'url' => array(
'name' => 'URL',
'description' => 'URL for the feed item.',
'date_ical_parse_handler' => 'formatText',
),
'location' => array(
'name' => 'Location text',
'description' => 'Text of the location property of the feed item.',
'date_ical_parse_handler' => 'formatText',
),
'location:altrep' => array(
'name' => 'Location alternate representation',
'description' => 'Additional location information, usually a URL to a page with more info.',
'date_ical_parse_handler' => 'formatParamText',
),
'categories' => array(
'name' => 'Categories',
'description' => 'Catagories of the feed item.',
'date_ical_parse_handler' => 'formatCategories',
),
);
/**
* Load and run parser implementation of FeedsParser::parse().
*
* @params - change these to generic required paramters.
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
self::loadLibrary();
// Read the iCal feed into memory.
$ical_feed_contents = $fetcher_result->getRaw();
// Create the calendar object and parse the feed.
$calendar = new vcalendar();
if (!$calendar->parse($ical_feed_contents)) {
$url = $source->config[$source->importer->config['fetcher']['plugin_key']]['source'];
throw new DateIcalParseException(t('Error parsing iCal feed: %url', array('%url' => $url)));
}
// Allow modules to alter the vcalendar object before we interpret its properties.
$context = array(
'source' => $source,
'fetcher_result' => $fetcher_result,
);
drupal_alter('date_ical_icalcreator_calendar', $calendar, $context);
//
// Set a result object.
//
$result = new DateIcalParserResult();
// FeedsResult properties
$xcalname = $calendar->getProperty('X-WR-CALNAME');
$result->title = !empty($xcalname) ? $xcalname[1] : '';
$xcaldesc = $calendar->getProperty('X-WR-CALDESC');
$result->description = !empty($xcaldesc) ? $xcaldesc[1] : '';
$result->link = NULL;
// Additional DateIcalParserResult properties
$xtimezone = $calendar->getProperty('X-WR-TIMEZONE');
if (!empty($xtimezone)) {
try {
$tz = new DateTimeZone($xtimezone[1]);
$result->timezone = $tz;
}
catch (Exception $e) {
$source->log('parse', 'Invalid X-WR-TIMEZONE: %error', array('%error' => $e->getMessage()), WATCHDOG_NOTICE);
}
}
$timezones = array();
while ($raw_component = $calendar->getComponent('VTIMEZONE')) {
$timezones[$raw_component->getProperty('tzid')] = new DateIcalIcalcreatorComponent($raw_component);
}
$result->timezones = $timezones;
// Separate the individual feed items into DateIcalIcalcreatorComponents.
$components = array();
$component_types = array('vevent', 'vtodo', 'vjournal', 'vfreebusy', 'valarm');
foreach ($component_types as $component_type) {
while ($raw_component = $calendar->getComponent($component_type)) {
$component = new DateIcalIcalcreatorComponent($raw_component);
// Allow modules to alter the DateIcalIcalcreatorComponent before we
// parse it into Feeds-readable data.
$context = array(
'calendar' => $calendar,
'source' => $source,
'fetcher_result' => $fetcher_result,
'parser_result' => $result,
);
drupal_alter('date_ical_icalcreator_component', $component, $context);
$components[] = $component;
}
}
$result->items = $components;
return $result;
}
/******
* Source output formatters.
*
* TODO: Could be in a class of their own?
**/
/**
* Format text fields.
*
* @todo is \n \N handling correct?
*/
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 target.
drupal_alter('date_ical_feeds_object', $text, $context);
return $text;
}
/**
* Format Text Parameter
*
* @return string.
*/
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 target.
drupal_alter('date_ical_feeds_object', $text, $context);
return $text;
}
/**
* Format date fields.
*
* @return FeedsDateTime
*/
public function formatDateTime($property_key, $property, DateIcalIcalcreatorComponent $item, DateIcalParserResult $result, FeedsSource $source) {
$context = get_defined_vars();
$date_array = $property['value'];
// It's frustrating that iCalcreator gives us date data in a different
// format than what it expects us to give back.
if (isset($property['params']['TZID'])) {
$date_array['tz'] = $property['params']['TZID'];
}
if (isset($property['params']['VALUE']) && $property['params']['VALUE'] == 'DATE') {
/**
* DATE values are All Day events, with no time-of-day.
* They can span over multiple days.
* FeedsDateTime sets the granularity correctly.
* However the granularity is not used yet.
* All Day events handling is not finalized at the time of writing.
* http://drupal.org/node/874322 To Date & All Day Date Handling
*/
if ($property_key == 'dtend') {
$start_prop = $item->getProperty('dtstart');
$start_array = $start_prop['value'];
if (isset($start_prop['params']['TZID'])) {
$start_array['tz'] = $start_prop['params']['TZID'];
}
// On single-day All Day events (the start date + 1 = the end date), just ignore the DTEND
// if the Date All Day module is installed. Otherwise it'll be displayed strangely.
if ($start_array['year'] == $date_array['year']
&& $start_array['month'] == $date_array['month']
&& $start_array['day'] + 1 == $date_array['day']
&& module_exists('date_all_day')) {
return;
}
elseif (module_exists('date_all_day')) {
// When we add the time of 00:00:00 to our date explicitly
// the date that gets parsed for DTEND is 1 day ahead.
// so taking a day away before that (if date_all_day is found)
$date_array['day'] -= 1;
}
}
// 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')) {
$end_array = iCalUtilityFunctions::_duration2date($date_array, $duration['value']);
$item->setProperty('dtend', $end_array);
}
elseif (!$item->getProperty('dtend')) {
// For cases where a VEVENT component specifies a DTSTART property with a DATE value type
// but no DTEND nor DURATION property, the event's duration is taken to be one day.
$end_array = $date_array;
$end_array['day'] += 1;
$item->setProperty('dtend', $end_array);
}
}
// FeedsDateTime's implementation of setTimezone() assumes that dates with no time element should just ignore
// timezone changes, so I had to add the 00:00:00 explicitly, even though it shouldn't be necessary.