Have the Calendar iCal feed pass along the timeZone of events. Fixes bug #12030.

Squashed commit of the following:

commit ce957db5311c7fb11c7d780b9f63c1b0adc17cda
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Mon Jan 31 19:00:23 2011 -0800

    Changelog notice for the ical bugfix with timezones.

commit 24a3bc1c74b20ca6ba689641fa0187b659dc3c5e
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Mon Jan 31 15:32:50 2011 -0800

    Transfer the time zone as one of the WebGUI specific items for the Event.

commit 36f87539ba8b090a5e5c9f1879cd40b6f6e69afc
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Sun Jan 30 21:18:12 2011 -0800

    Refactor the code so that the Event adds itself to a calendar object, giving it access to add other things to the calendar, like a timezone definition.

commit f401966bd53b51950f1402a5a19b92b13dcc6b06
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Sun Jan 30 19:42:49 2011 -0800

    Document which version of Data::ICal that we're now using.

commit 689522e30b26fc445da5b59b81789c0d72e157d4
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Sat Jan 29 14:27:25 2011 -0800

    Fix a typo in the Event preventing menuTitle from being sent out.  Fix issue with the menuTitle being set twice.  Update the test to fix long string handling.

commit 59043eee1536e60d92f23313a33dc54da6f15122
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Fri Jan 28 19:48:16 2011 -0800

    Move iCal generation over to Data::ICal.  Give each event the ability to return itself as an Data::ICal::Entry::Event object.

commit eec448b38c101b599c0fb695f442012e311385fb
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Fri Jan 28 19:47:14 2011 -0800

    Document new testing module.  It't only required if you're doing testing.

commit 3d1e82229423834330dcdd31c0a81df986df0f32
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Fri Jan 28 17:05:09 2011 -0800

    Fix setting the title in the iCal feed.

commit 458f4d2e0e1faa1cc6dcb7107040d06b51b97d36
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Fri Jan 28 13:37:52 2011 -0800

    Rewrite of CalendarUpdateFeeds to use Data::ICal

commit 1a271ebc78bbe8b21a26f7a0dba61d1462922f30
Author: Colin Kuskie <colink@perldreamer.com>
Date:   Fri Jan 28 10:49:27 2011 -0800

    Begin building an ICal object for parsing the ical feeds.
This commit is contained in:
Colin Kuskie 2011-01-31 19:05:04 -08:00
parent 93646ad8e6
commit 8ae16ba37c
8 changed files with 110 additions and 161 deletions

View file

@ -1,4 +1,5 @@
7.10.9 7.10.9
- fixed #12030: Calendar Feed Time Zone Issue
7.10.8 7.10.8
- rfe #12016 for the top story as well - rfe #12016 for the top story as well

View file

@ -7,6 +7,11 @@ upgrading from one version to the next, or even between multiple
versions. Be sure to heed the warnings contained herein as they will versions. Be sure to heed the warnings contained herein as they will
save you many hours of grief. save you many hours of grief.
7.10.9
--------------------------------------------------------------------
* WebGUI now depends on Data::ICal for making and reading iCal feeds
for the Calendar.
7.10.4 7.10.4
-------------------------------------------------------------------- --------------------------------------------------------------------
* WebGUI now depends on Monkey::Patch for doing sanely scoped * WebGUI now depends on Monkey::Patch for doing sanely scoped

View file

@ -28,6 +28,7 @@ use WebGUI::Storage;
use Test::Deep::NoTest qw(eq_deeply); use Test::Deep::NoTest qw(eq_deeply);
use DateTime::Event::ICal; use DateTime::Event::ICal;
use DateTime::Set; use DateTime::Set;
use Data::ICal::Entry::Event;
use base 'WebGUI::Asset'; use base 'WebGUI::Asset';
@ -74,6 +75,54 @@ sub addRevision {
return $newRev; return $newRev;
} }
####################################################################
=head2 add_to_calendar ($iCal)
Build a Data::ICal::Entry::Event object that contains the information for this
event and add it to the Data::ICal calendar
=head3 $iCal
A Data::ICal object, representing the top-level calendar instance.
=cut
sub add_to_calendar {
my $self = shift;
my $session = $self->session;
my $calendar = shift;
my $event = Data::ICal::Entry::Event->new();
$event->add_properties(
'last-modified' => WebGUI::DateTime->new($session, $event->get("revisionDate"))->toIcal,
created => WebGUI::DateTime->new($session, $event->get("creationDate"))->toIcal,
sequence => $self->get('iCalSequenceNumber'),
summary => $self->get('title'),
description => $self->get('description'),
location => $self->get('location'),
uid => $self->get('feedUid')
? $self->get('feedUid')
: $self->get('assetId') . '@'. $session->config->get("sitename")->[0],
);
##WebGUI Specific fields
foreach my $prop (qw/groupIdView groupIdEdit url menuTitle timeZone/) {
$event->add_property( 'x-webgui-'.lc($prop) => $self->get($prop));
}
my $eventStart = $self->getIcalStart;
my $start_parameters = {};
if (! $eventStart =~ /T/) {
$start_parameters->{VALUE} = 'DATE';
}
$event->add_property(dtstart => [ $eventStart, $start_parameters ]);
my $eventEnd = $self->getIcalEnd;
my $end_parameters = {};
if (! $eventEnd =~ /T/) {
$end_parameters->{VALUE} = 'DATE';
}
$event->add_property(dtend => [ $eventEnd, $end_parameters ]);
$calendar->add_entry($event);
}
{ {
my %dayNamesToICal = ( my %dayNamesToICal = (

View file

@ -19,6 +19,7 @@ use WebGUI::International;
use WebGUI::Search; use WebGUI::Search;
use WebGUI::Form; use WebGUI::Form;
use WebGUI::HTML; use WebGUI::HTML;
use WebGUI::ICal;
use WebGUI::DateTime; use WebGUI::DateTime;
use Class::C3; use Class::C3;
@ -1736,97 +1737,20 @@ sub www_ical {
$dt_end = $dt_start->clone->add( seconds => $self->get('icalInterval') ); $dt_end = $dt_start->clone->add( seconds => $self->get('icalInterval') );
} }
my $ical = WebGUI::ICal->new();
# Get all the events we're going to display # Get all the events we're going to display
my @events = $self->getEventsIn($dt_start->toMysql,$dt_end->toMysql); my @events = $self->getEventsIn($dt_start->toMysql,$dt_end->toMysql);
my $ical = qq{BEGIN:VCALENDAR\r\n}
. qq{PRODID:WebGUI }.$WebGUI::VERSION."-".$WebGUI::STATUS.qq{\r\n}
. qq{VERSION:2.0\r\n};
# VEVENT:
EVENT: for my $event (@events) { EVENT: for my $event (@events) {
next EVENT unless $event->canView(); next EVENT unless $event->canView();
$ical .= qq{BEGIN:VEVENT\r\n}; $event->add_to_calendar($ical);
### UID
# Use feed's UID to prevent over-propagation
if ($event->get("feedUid")) {
$ical .= qq{UID:}.$event->get("feedUid")."\r\n";
}
# Create a UID for feeds native to this calendar
else {
my $domain = $session->config->get("sitename")->[0];
$ical .= qq{UID:}.$event->get("assetId").'@'.$domain."\r\n";
}
# LAST-MODIFIED (revisionDate)
$ical .= qq{LAST-MODIFIED:}
. WebGUI::DateTime->new($self->session, $event->get("revisionDate"))->toIcal
. "\r\n";
# CREATED (creationDate)
$ical .= qq{CREATED:}
. WebGUI::DateTime->new($self->session, $event->get("creationDate"))->toIcal
. "\r\n";
# SEQUENCE
my $sequenceNumber = $event->get("iCalSequenceNumber");
if (defined $sequenceNumber) {
$ical .= qq{SEQUENCE:}
. $event->get("iCalSequenceNumber")
. "\r\n";
}
# DTSTART
my $eventStart = $event->getIcalStart;
$ical .= 'DTSTART';
if ($eventStart !~ /T/) {
$ical .= ';VALUE=DATE';
}
$ical .= ":$eventStart\r\n";
# DTEND
my $eventEnd = $event->getIcalEnd;
$ical .= 'DTEND';
if ($eventEnd !~ /T/) {
$ical .= ';VALUE=DATE';
}
$ical .= ":$eventEnd\r\n";
# Summary (the title)
# Wrapped at 75 columns
$ical .= $self->wrapIcal("SUMMARY:".$event->get("title"))."\r\n";
# Description (the text)
# Wrapped at 75 columns
$ical .= $self->wrapIcal("DESCRIPTION:".$event->get("description"))."\r\n";
# Location (the text)
# Wrapped at 75 columns
$ical .= $self->wrapIcal("LOCATION:".$event->get("location"))."\r\n";
# X-WEBGUI lines
if ($event->get("groupIdView")) {
$ical .= "X-WEBGUI-GROUPIDVIEW:".$event->get("groupIdView")."\r\n";
}
if ($event->get("groupIdEdit")) {
$ical .= "X-WEBGUI-GROUPIDEDIT:".$event->get("groupIdEdit")."\r\n";
}
$ical .= "X-WEBGUI-URL:".$event->get("url")."\r\n";
$ical .= "X-WEBGUI-MENUTITLE:".$event->get("menuTitle")."\r\n";
$ical .= qq{END:VEVENT\r\n};
} }
# ENDVEVENT
$ical .= qq{END:VCALENDAR\r\n};
# Set mime of text/icalendar # Set mime of text/icalendar
#$self->session->http->setMimeType("text/plain"); #$self->session->http->setMimeType("text/plain");
$self->session->http->setFilename("feed.ics","text/calendar"); $self->session->http->setFilename("feed.ics","text/calendar");
return $ical; return $ical->as_string;
} }
#---------------------------------------------------------------------------- #----------------------------------------------------------------------------

10
lib/WebGUI/ICal.pm Normal file
View file

@ -0,0 +1,10 @@
package WebGUI::ICal;
use WebGUI;
use parent qw/Data::ICal/;
sub product_id {
return 'WebGUI '. $WebGUI::VERSION . '-' . $WebGUI::STATUS;
}
1;

View file

@ -23,6 +23,7 @@ use WebGUI::Asset::Event;
use WebGUI::DateTime; use WebGUI::DateTime;
use DateTime::TimeZone; use DateTime::TimeZone;
use Data::Dumper; use Data::Dumper;
use Data::ICal;
use LWP::UserAgent; use LWP::UserAgent;
use JSON (); use JSON ();
@ -142,88 +143,52 @@ sub execute {
next FEED; next FEED;
} }
my $data = $response->content; my $data = $response->content;
# If doesn't start with BEGIN:VCALENDAR then error my $cal = Data::ICal->new( data => $data );
unless ($data =~ /^BEGIN:VCALENDAR/i) { if (!$cal) {
# Update the result and last updated fields # Update the result and last updated fields
$feed->{lastResult} = "Not an iCalendar feed"; $feed->{lastResult} = "Error parsing iCal feed";
$feed->{lastUpdated} = $dt; $feed->{lastUpdated} = $dt;
$calendar->setFeed($feed->{feedId}, $feed); $calendar->setFeed($feed->{feedId}, $feed);
next FEED; #next FEED;
} }
my $active = 0; # Parser on/off
my %current_event = ();
my %events;
my $line_number = 0;
$data =~ s/[ \t]?[\r\n]+[ \t]+/ /msg; #Process line continuations
LINE: for my $line (split /[\r\n]+/,$data) {
chomp $line;
$line_number++;
next unless $line =~ /\w/;
#warn "LINE $line_number: $line\n";
if ($line =~ /^BEGIN:VEVENT$/i) {
$active = 1;
next LINE;
}
elsif ($line =~ /^END:VEVENT$/i) {
$active = 0;
# Flush event
my $uid = lc $current_event{uid}[1];
delete $current_event{uid};
$events{$uid} = {%current_event};
$session->log->info( "Found event $uid from feed " . $feed->{feedId} );
%current_event = ();
next LINE;
}
else {
# Flush old entry
# KEY;ATTRIBUTE=VALUE;ATTRIBUTE=VALUE:KEYVALUE
my ($key_attrs,$value) = split /:/,$line,2;
my @attrs = $key_attrs ? (split /;/, $key_attrs) : ();
my $key = shift @attrs;
my %attrs;
while (my $attribute = shift @attrs) {
my ($attr_key, $attr_value) = split /=/, $attribute, 2;
$attrs{lc $attr_key} = $attr_value;
}
$current_event{lc $key} = [\%attrs,$value];
}
}
my $feedData = $feedList->{$feed->{feedId}} = { my $feedData = $feedList->{$feed->{feedId}} = {
added => 0, added => 0,
updated => 0, updated => 0,
errored => 0, errored => 0,
assetId => $calendar->getId, assetId => $calendar->getId,
}; };
EVENT: for my $id (keys %events) { EVENT: foreach my $entry (@{ $cal->entries }) {
next EVENT unless $entry->ical_entry_type eq 'VEVENT';
#use Data::Dumper; #use Data::Dumper;
#warn "EVENT: $id; ".Dumper $events{$id}; #warn "EVENT: $id; ".Dumper $events{$id};
my $event_properties = $entry->properties;
# Prepare event data # Prepare event data
my $properties = { my $properties = {
feedUid => $id,
feedId => $feed->{feedId}, feedId => $feed->{feedId},
description => _unwrapIcalText($events{$id}->{description}->[1]),
title => _unwrapIcalText($events{$id}->{summary}->[1]),
location => _unwrapIcalText($events{$id}->{location}->[1]),
menuTitle => substr($events{$id}->{summary}->[1],0,15),
className => 'WebGUI::Asset::Event', className => 'WebGUI::Asset::Event',
isHidden => 1, isHidden => 1,
}; };
PROPERTY: foreach my $property (qw/uid description summary location/) {
next property unless exists $event_properties->{$property};
$properties->{$property} = $event_properties->{$property}->[0]->value;
}
##Fixup
$properties->{title} = delete $properties->{summary};
$properties->{feedUid} = delete $properties->{uid};
# Prepare the date # Prepare the date
my $dtstart = $events{$id}->{dtstart}->[1]; my $dtstart = $event_properties->{dtstart}->[0]->value;
if ($dtstart =~ /T/) { if ($dtstart =~ /T/) {
my ($date, $time) = split /T/, $dtstart; my ($date, $time) = split /T/, $dtstart;
my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/;
my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/;
my $tz = $events{$id}->{dtstart}->[0]->{tzid}; my $tz = '';
if ($event_properties->{dtstart}->[0]->properties->{tzid}) {
$tz = $event_properties->{dtstart}->[0]->properties->{tzid};
}
if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) {
$tz = "UTC"; $tz = "UTC";
} }
@ -253,14 +218,14 @@ sub execute {
next EVENT; next EVENT;
} }
my $dtend = $events{$id}->{dtend}->[1]; my $dtend = exists $event_properties->{dtend} ? $event_properties->{dtend}->[0]->value : undef;
my $duration = $events{$id}->{duration}->[1]; my $duration = exists $event_properties->{duration} ? $event_properties->{duration}->[0]->value : undef;
if ($dtend =~ /T/) { if ($dtend =~ /T/) {
my ($date, $time) = split /T/, $dtend; my ($date, $time) = split /T/, $dtend;
my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/;
my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/;
my $tz = $events{$id}->{dtend}->[0]->{tzid}; my $tz = '';
if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) {
$tz = "UTC"; $tz = "UTC";
} }
@ -330,28 +295,15 @@ sub execute {
} }
# If there are X-WebGUI-* fields # If there are X-WebGUI-* fields
for my $key (grep /^x-webgui-/, keys %{$events{$id}}) { PROPERTY: foreach my $key (qw/groupIdEdit groupIdView url menuTitle timeZone/) {
my $property_name = $key; my $property_name = 'x-webgui-'.lc $key;
$property_name =~ s/^x-webgui-//; next PROPERTY unless exists $event_properties->{$property_name};
$property_name = lc $property_name; $properties->{$key} = $event_properties->{$property_name}->[0]->value;
if ($property_name eq "groupidedit") {
$properties->{groupIdEdit} = $events{$id}->{$key}->[1];
}
elsif ($property_name eq "groupidview") {
$properties->{groupIdView} = $events{$id}->{$key}->[1];
}
elsif ($property_name eq "url") {
$properties->{url} = $events{$id}->{$key}->[1];
}
elsif ($property_name eq "menutitle") {
$properties->{menuTitle} = $events{$id}->{$key}->[1];
}
} }
my $recur; my $recur;
if ($events{$id}->{rrule}) { if (exists $event_properties->{rrule}) {
$recur = _icalToRecur($session, $properties->{startDate}, $events{$id}->{rrule}->[1]); $recur = _icalToRecur($session, $properties->{startDate}, $event_properties->{rrule}->[0]->value);
} }
# save events for later # save events for later

View file

@ -67,6 +67,7 @@ checkModule("HTTP::Headers", 1.61 );
checkModule("Test::More", 0.82, 2 ); checkModule("Test::More", 0.82, 2 );
checkModule("Test::MockObject", 1.02, 2 ); checkModule("Test::MockObject", 1.02, 2 );
checkModule("Test::Deep", 0.095, ); checkModule("Test::Deep", 0.095, );
checkModule("Test::LongString", 0.13, 2 );
checkModule("Test::Exception", 0.27, 2 ); checkModule("Test::Exception", 0.27, 2 );
checkModule("Test::Differences", 0.5, 2 ); checkModule("Test::Differences", 0.5, 2 );
checkModule("Test::Class", 0.31, 2 ); checkModule("Test::Class", 0.31, 2 );
@ -149,6 +150,7 @@ checkModule('IO::Socket::SSL', );
checkModule('Net::Twitter', "3.13006" ); checkModule('Net::Twitter', "3.13006" );
checkModule('PerlIO::eol', "0.14" ); checkModule('PerlIO::eol', "0.14" );
checkModule('Monkey::Patch', '0.03' ); checkModule('Monkey::Patch', '0.03' );
checkModule('Data::ICal', '0.16' );
failAndExit("Required modules are missing, running no more checks.") if $missingModule; failAndExit("Required modules are missing, running no more checks.") if $missingModule;

View file

@ -20,12 +20,13 @@ use WebGUI::Asset::Wobject::Calendar;
use Test::More; use Test::More;
use Test::Deep; use Test::Deep;
use Test::LongString;
use Data::Dumper; use Data::Dumper;
plan skip_all => 'set WEBGUI_LIVE to enable this test' plan skip_all => 'set WEBGUI_LIVE to enable this test'
unless $ENV{WEBGUI_LIVE}; unless $ENV{WEBGUI_LIVE};
plan tests => 14; # increment this value for each test you create plan tests => 19; # increment this value for each test you create
my $session = WebGUI::Test->session; my $session = WebGUI::Test->session;
@ -105,7 +106,12 @@ is($anniversary->get('menuTitle'), $party->get('menuTitle'), '... menuTitl
is($anniversary->get('groupIdView'), $party->get('groupIdView'), '... groupIdView'); is($anniversary->get('groupIdView'), $party->get('groupIdView'), '... groupIdView');
is($anniversary->get('groupIdEdit'), $party->get('groupIdEdit'), '... groupIdEdit'); is($anniversary->get('groupIdEdit'), $party->get('groupIdEdit'), '... groupIdEdit');
is($anniversary->get('url'), $party->get('url').'2', '... url (accounting for duplicate)'); is($anniversary->get('url'), $party->get('url').'2', '... url (accounting for duplicate)');
is($anniversary->get('description'), $party->get('description'), '... description, checks for line wrapping'); is($anniversary->get('timeZone'), $party->get('timeZone'), '... timeZone');
is($anniversary->get('startDate'), $party->get('startDate'), '... startDate');
is($anniversary->get('startTime'), $party->get('startTime'), '... startTime');
is($anniversary->get('endDate'), $party->get('endDate'), '... endDate');
is($anniversary->get('endTime'), $party->get('endTime'), '... endTime');
is_string($anniversary->get('description'), $party->get('description'), '... description, checks for line wrapping');
$party->update({description => "one line\nsecond line"}); $party->update({description => "one line\nsecond line"});