diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 63d8f72ae..7502d4855 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -1,4 +1,5 @@ 7.7.19 + - fixed #10833: Calendar feeds not versioned, not duplicated - fixed #10831: Graphing tab in Poll is not i18n'ed - fixed #10829: Extra field in Poll Property tab - refactored out JSON collateral module, to use with any module in WebGUI. diff --git a/docs/upgrades/upgrade_7.7.18-7.7.19.pl b/docs/upgrades/upgrade_7.7.18-7.7.19.pl index 9ebee09fb..906b3a8db 100644 --- a/docs/upgrades/upgrade_7.7.18-7.7.19.pl +++ b/docs/upgrades/upgrade_7.7.18-7.7.19.pl @@ -22,6 +22,8 @@ use Getopt::Long; use WebGUI::Session; use WebGUI::Storage; use WebGUI::Asset; +use WebGUI::Asset::Wobject::Calendar; +use JSON; my $toVersion = '7.7.19'; @@ -33,6 +35,7 @@ my $session = start(); # this line required # upgrade functions go here addInboxSmsNotificationTemplateIdSetting($session); upgradeJSONDatabaseFields($session); +moveCalendarFeedsToJSON($session); finish($session); # this line required #---------------------------------------------------------------------------- @@ -92,6 +95,30 @@ sub upgradeJSONDatabaseFields { print "DONE!\n" unless $quiet; } +sub moveCalendarFeedsToJSON { + my $session = shift; + print "\tMoveing Calendar feeds from database collateral to JSON... " unless $quiet; + $session->db->write(q|ALTER TABLE Calendar ADD COLUMN icalFeeds LONGTEXT|); + my $getCalendar = WebGUI::Asset::Wobject::Calendar->getIsa($session); + while (my $calendar = $getCalendar->()) { + my $feeds = $session->db->buildHashRefOfHashRefs( + "select * from Calendar_feeds where assetId=?", + [$calendar->getId], + "feedId" + ); + foreach my $feedParams (values %{ $feeds }) { + delete $feedParams->{assetId}; + $calendar->addFeed($feedParams); + } + ##Copy the JSON across all the revisions of this Calendar. + my $jsonFeeds = $session->db->quickScalar('select icalFeeds from Calendar where assetId=? and revisionDate=?', [ $calendar->getId, $calendar->get('revisionDate')]); + $session->db->write('update Calendar set icalFeeds=? where assetId=?', [$jsonFeeds, $calendar->getId]); + } + $session->db->write(q|DROP TABLE Calendar_feeds|); + + print "DONE!\n" unless $quiet; +} + # -------------- DO NOT EDIT BELOW THIS LINE -------------------------------- #---------------------------------------------------------------------------- diff --git a/lib/WebGUI/Asset/Wobject/Calendar.pm b/lib/WebGUI/Asset/Wobject/Calendar.pm index 6d455c00f..d2114243f 100644 --- a/lib/WebGUI/Asset/Wobject/Calendar.pm +++ b/lib/WebGUI/Asset/Wobject/Calendar.pm @@ -20,8 +20,9 @@ use WebGUI::Search; use WebGUI::Form; use WebGUI::HTML; use WebGUI::DateTime; +use Class::C3; -use base 'WebGUI::Asset::Wobject'; +use base qw/WebGUI::Asset::Wobject WebGUI::JSONCollateral/; use DateTime; use JSON; @@ -255,6 +256,14 @@ sub definition { unitsAvailable => [ qw( days weeks months years ) ], }, + icalFeeds => { + fieldType => "textarea", + defaultValue => [], + serialize => 1, + noFormPost => 1, + tab => "display", + }, + icalInterval => { fieldType => "interval", defaultValue => $session->datetime->intervalToSeconds( 3, 'months' ), @@ -263,7 +272,7 @@ sub definition { hoverHelp => $i18n->get('editForm icalInterval description'), unitsAvailable => [ qw( days weeks months years ) ], }, - + workflowIdCommit => { fieldType => "workflow", defaultValue => $session->setting->get('defaultVersionTagWorkflow'), @@ -309,6 +318,45 @@ sub addChild { #---------------------------------------------------------------------------- +=head2 addFeed ( $feedParams ) + +Adds a new Feed to this calendar. This is a wrapper around WebGUI::JSONCollateral's setJSONCollateral +method. + +=head3 $feedParams + +A hashref of parameters that describe the feed. + +=head4 feedId + +GUID for this feed. + +=head4 url + +URL for this feed. + +=head4 lastUpdated + +The date this feed was added, or edited last. + +=head4 lastResult + +The results of what happened the last time this feed was accessed to pull iCal. + +=head4 feedType + +What kind of feed this is. + +=cut + +sub addFeed { + my $self = shift; + my $feedParams = shift; + return $self->setJSONCollateral('icalFeeds', 'feedId', 'new', $feedParams); +} + +#---------------------------------------------------------------------------- + =head2 appendTemplateVarsDateTime( var, datetime [, prefix ] ) Append template vars from the given datetime. C is a hash reference. @@ -460,6 +508,25 @@ sub createSubscriptionGroup { #---------------------------------------------------------------------------- +=head2 deleteFeed ( $feedId ) + +Deletes a Feed from this calendar. This is a wrapper around WebGUI::JSONCollateral's deleteJSONCollateral +method. + +=head3 $feedId + +GUID of the feed to delete. + +=cut + +sub deleteFeed { + my $self = shift; + my $feedId = shift; + return $self->deleteJSONCollateral('icalFeeds', 'feedId', $feedId); +} + +#---------------------------------------------------------------------------- + =head2 getEditForm Adds an additional tab for feeds. @@ -613,9 +680,9 @@ ENDHTML # Add the existing feeds my $feeds = $self->getFeeds(); $tab->raw(''); @@ -779,9 +846,28 @@ sub getEventVars { #---------------------------------------------------------------------------- +=head2 getFeed ( $feedId ) + +Gets the data structure for one particular feed from this Calendar. This is a wrapper +for getJSONCollateral. + +=head3 $feedId + +The GUID of the feed to fetch. + +=cut + +sub getFeed { + my $self = shift; + my $feedId = shift; + return $self->getJSONCollateral('icalFeeds', 'feedId', $feedId); +} + +#---------------------------------------------------------------------------- + =head2 getFeeds ( ) -Gets a hashref of hashrefs of all the feeds attached to this calendar. +Gets an arrayref of hashrefs of all the feeds attached to this calendar. TODO: Format lastUpdated into the user's time zone @@ -789,12 +875,7 @@ TODO: Format lastUpdated into the user's time zone sub getFeeds { my $self = shift; - - return $self->session->db->buildHashRefOfHashRefs( - "select * from Calendar_feeds where assetId=?", - [$self->get("assetId")], - "feedId" - ); + return $self->get('icalFeeds'); } #---------------------------------------------------------------------------- @@ -950,30 +1031,23 @@ sub processPropertiesFromFormPost { $feeds{$feedId}++; } my @feedsFromForm = keys %feeds; - + # Delete old feeds that are not in @feeds - my @oldFeeds - = $session->db->buildArray( - "select feedId from Calendar_feeds where assetId=?", - [$self->get("assetId")] - ); + my @oldFeeds = map { $_->{feedId} } @{ $self->getFeeds }; for my $feedId (@oldFeeds) { - if (!grep /^$feedId$/, @feedsFromForm) { - $session->db->write( - "delete from Calendar_feeds where feedId=? and assetId=?", - [$feedId,$self->get("assetId")] - ); + if (!isIn($feedId, @feedsFromForm)) { + $self->deleteFeed($feedId); } } - + # Create new feeds for my $feedId (grep /^new(\d+)/, @feedsFromForm) { - $session->db->setRow("Calendar_feeds","feedId",{ - feedId => "new", - assetId => $self->get("assetId"), + $self->addFeed({ url => $form->param("feeds-".$feedId), feedType => "ical", + lastUpdated => 'never', + lastResult => '', }); } @@ -983,18 +1057,26 @@ sub processPropertiesFromFormPost { #---------------------------------------------------------------------------- -=head2 purge ( ) +=head2 setFeed ( $feedId, $feedParams ) -Handle Asset specific purge tasks. +Adds a new Feed to this calendar. This is a wrapper around WebGUI::JSONCollateral's setJSONCollateral +method. -Delete iCal feeds for this Calendar. +=head3 $feedId + +The GUID of the feed to update. + +=head3 $feedParams + +See L for a list of parameters. =cut -sub purge { - my $self = shift; - $self->session->db->write('delete from Calendar_feeds where assetId=?',[$self->get('assetId')]); - $self->SUPER::purge; +sub setFeed { + my $self = shift; + my $feedId = shift; + my $feedParams = shift; + return $self->setJSONCollateral('icalFeeds', 'feedId', $feedId, $feedParams); } #---------------------------------------------------------------------------- diff --git a/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm b/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm index 05bf788b3..b9cef3ed4 100644 --- a/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm +++ b/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm @@ -22,6 +22,7 @@ use WebGUI::Asset::Wobject::Calendar; use WebGUI::Asset::Event; use WebGUI::DateTime; use DateTime::TimeZone; +use Data::Dumper; use LWP::UserAgent; use JSON (); @@ -77,12 +78,12 @@ See WebGUI::Workflow::Activity::execute() for details. sub execute { my $self = shift; my $session = $self->session; - + my $object = shift; my $instance = shift; my $previousUser = $session->user; $session->user({userId => 3}); - + ### TODO: If we take more than a minute, return WAITING so that some # other activity can run my $startTime = time(); @@ -92,284 +93,286 @@ sub execute { my $feedList; if ($instance->getScratch('events')) { $eventList = JSON::from_json($instance->getScratch('events')); - $feedList = JSON::from_json($instance->getScratch('feeds')); + $feedList = JSON::from_json($instance->getScratch('feeds')); } else { my $ua = LWP::UserAgent->new(agent => "WebGUI"); - my $sth = $self->session->db->read("select * from Calendar_feeds"); + my $getCalendar = WebGUI::Asset::Wobject::Calendar->getIsa($session); - FEED: while (my $feed = $sth->hashRef) { - my $calendar = WebGUI::Asset->newByDynamicClass($self->session,$feed->{assetId}); - if (!defined $calendar) { - $self->session->errorHandler->error("Calendar object failed to instanciate. Did you commit the calendar wobject?"); - next FEED; + CALENDAR: while (my $calendar = $getCalendar->()) { + my $calendarTitle = $calendar->getTitle; + my $calendarId = $calendar->getId; + if ( $calendar->get( "state" ) ne "published" ) { + $session->log->info( "Calendar $calendarTitle ($calendarId) is not state='published', skipping..." ); + next CALENDAR; } - elsif ( $calendar->get( "state" ) ne "published" ) { - $self->session->errorHandler->info( "Calendar is not state='published', skipping..." ); - next FEED; + elsif (! scalar @{ $calendar->getFeeds } ) { + $session->log->info( "Calendar $calendarTitle ($calendarId) has no feeds, skipping..." ); + next CALENDAR; } - + + $session->log->info( "Calendar $calendarTitle ($calendarId) has feeds, fetching..." ); #!!! KLUDGE - If the feed is on the same server, set a scratch value # I do not know how dangerous this is, so THIS MUST CHANGE! # Preferably: Spectre would add a userSession to the database, # and send the appropriate cookie with the request. - my $sitename = $self->session->config->get("sitename")->[0]; - if ($feed->{url} =~ m{http://[^/]*$sitename}) - { - $feed->{url} .= ( $feed->{url} =~ /[?]/ ? ";" : "?" ) . "adminId=".$session->getId; - $self->session->db->write("REPLACE INTO userSessionScratch (sessionId,name,value) VALUES (?,?,?)", - [$session->getId,$feed->{assetId},"SPECTRE"]); - } - #/KLUDGE - - ## Somebody point me to a DECENT iCalendar parser... - # Text::vFile perhaps? - - # Get the feed - my $response = $ua->get($feed->{url}); - - if (!$response->is_success) { - # Update the result and last updated fields - $self->session->db->write("update Calendar_feeds set lastResult=?,lastUpdated=? where feedId=?", - [($response->message || $response->content),$dt,$feed->{feedId}]); - next FEED; - } - - my $data = $response->content; - # If doesn't start with BEGIN:VCALENDAR then error - unless ($data =~ /^BEGIN:VCALENDAR/i) { - # Update the result and last updated fields - $self->session->db->write( - "update Calendar_feeds set lastResult=?,lastUpdated=? where feedId=?", - ["Not an iCalendar feed",$dt,$feed->{feedId}]); - 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; + my $sitename = $session->config->get("sitename")->[0]; + FEED: foreach my $feed (@{ $calendar->getFeeds }) { + if ($feed->{url} =~ m{http://[^/]*$sitename}) { + $feed->{url} .= ( $feed->{url} =~ /[?]/ ? ";" : "?" ) . "adminId=".$session->getId; + $session->db->write("REPLACE INTO userSessionScratch (sessionId,name,value) VALUES (?,?,?)", + [$session->getId,$calendar->getId,"SPECTRE"]); } - elsif ($line =~ /^END:VEVENT$/i) { - $active = 0; - # Flush event - my $uid = lc $current_event{uid}[1]; - delete $current_event{uid}; - $events{$uid} = {%current_event}; - $self->session->log->info( "Found event $uid from feed " . $feed->{feedId} ); - %current_event = (); - next LINE; + #/KLUDGE + + ## Somebody point me to a DECENT iCalendar parser... + # Text::vFile perhaps? + + # Get the feed + $session->log->info( "Trying Calendar feed ".$feed->{url}." for $calendarTitle" ); + my $response = $ua->get($feed->{url}); + + if (!$response->is_success) { + # Update the result and last updated fields + $feed->{lastResult} = $response->message || $response->content; + $feed->{lastUpdated} = $dt; + $calendar->setFeed($feed->{feedId}, $feed); + $session->log->info( "Calendar feed ".$feed->{url}." for $calendarTitle failed" ); + next FEED; } - 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; + + my $data = $response->content; + # If doesn't start with BEGIN:VCALENDAR then error + unless ($data =~ /^BEGIN:VCALENDAR/i) { + # Update the result and last updated fields + $feed->{lastResult} = "Not an iCalendar feed"; + $feed->{lastUpdated} = $dt; + $calendar->setFeed($feed->{feedId}, $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]; } - - $current_event{lc $key} = [\%attrs,$value]; } - } - - my $feedData = $feedList->{$feed->{feedId}} = { - added => 0, - updated => 0, - errored => 0, - assetId => $feed->{assetId}, - }; - EVENT: for my $id (keys %events) { - #use Data::Dumper; - #warn "EVENT: $id; ".Dumper $events{$id}; - - # Prepare event data - my $properties = { - feedUid => $id, - 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', - isHidden => 1, + + my $feedData = $feedList->{$feed->{feedId}} = { + added => 0, + updated => 0, + errored => 0, + assetId => $calendar->getId, }; - - # Prepare the date - my $dtstart = $events{$id}->{dtstart}->[1]; - if ($dtstart =~ /T/) { - my ($date, $time) = split /T/, $dtstart; - - my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; - my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; - my $tz = $events{$id}->{dtstart}->[0]->{tzid}; - if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { - $tz = "UTC"; - } - - ($properties->{startDate}, $properties->{startTime}) = - split / /, WebGUI::DateTime->new( - year => $year, - month => $month, - day => $day, - hour => $hour, - minute => $minute, - second => $second, - time_zone => $tz, - )->toMysql; - $properties->{timeZone} = $tz; - } - elsif ($dtstart =~ /(\d{4})(\d{2})(\d{2})/) { - my ($year, $month, $day) = $dtstart =~ /(\d{4})(\d{2})(\d{2})/; - $properties->{startDate} = join "-",$year,$month,$day; - } - elsif ($dtstart) { - $session->errorHandler->warn( - "Workflow::Activity::CalendarUpdateFeeds" - . " -- '$dtstart' does not appear to be a valid date" - ); - $feedData->{errored}++; - next EVENT; - } - - my $dtend = $events{$id}->{dtend}->[1]; - my $duration = $events{$id}->{duration}->[1]; - if ($dtend =~ /T/) { - my ($date, $time) = split /T/, $dtend; - - my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; - my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; - my $tz = $events{$id}->{dtend}->[0]->{tzid}; - if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { - $tz = "UTC"; - } - - ($properties->{endDate}, $properties->{endTime}) = - split / /, WebGUI::DateTime->new( - year => $year, - month => $month, - day => $day, - hour => $hour, - minute => $minute, - second => $second, - time_zone => $tz, - )->toMysql; - $properties->{timeZone} = $tz; - } - elsif ($dtend =~ /(\d{4})(\d{2})(\d{2})/) { - my ($year, $month, $day) = $dtend =~ /(\d{4})(\d{2})(\d{2})/; - - my $endDateLet = WebGUI::DateTime->new( year => $year, month => $month, day => $day); - $endDateLet->subtract( days => 1 ); - $properties->{endDate} = $endDateLet->toDatabaseDate; - } - # If we can't parse it, forget the whole event - elsif ($dtend) { - $session->errorHandler->warn( - "Workflow::Activity::CalendarUpdateFeeds" - . " -- '$dtend' does not appear to be a valid date" - ); - $feedData->{errored}++; - next EVENT; - } - # No dtend, but we have duration! - elsif ($duration) { - my ($days, $hours, $minutes, $seconds) - = $duration =~ m{ - P - (?:(\d+)D)? # Days - T - (?:(\d+)H)? # Hours - (?:(\d+)M)? # Minutes - (?:(\d+)S)? # Seconds - }ix; - my $startDate = $properties->{startDate}; - # Fill in bogus value to get a WebGUI::DateTime object, - # we'll figure out what we actually need later - my $startTime = $properties->{startTime} || "00:00:00"; - my $datetime = WebGUI::DateTime->new($session,$startDate." ".$startTime); + EVENT: for my $id (keys %events) { + #use Data::Dumper; + #warn "EVENT: $id; ".Dumper $events{$id}; - $datetime->add( - days => $days || 0, - hours => $hours || 0, - minutes => $minutes || 0, - seconds => $seconds || 0, - ); + # Prepare event data + my $properties = { + feedUid => $id, + 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', + isHidden => 1, + }; - $properties->{endDate} = $datetime->toDatabaseDate; - # If it not an all-day event, set the end time too - if ($properties->{startTime}) { - $properties->{endTime} = $datetime->toDatabaseTime; - } - } - # No dtend, no duration, just copy the start - else { - $properties->{endDate} = $properties->{startDate}; - $properties->{endTime} = $properties->{startTime}; - } - - # If there are X-WebGUI-* fields - for my $key (grep /^x-webgui-/, keys %{$events{$id}}) { - my $property_name = $key; - $property_name =~ s/^x-webgui-//; - $property_name = lc $property_name; - - 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]; - } - } + # Prepare the date + my $dtstart = $events{$id}->{dtstart}->[1]; + if ($dtstart =~ /T/) { + my ($date, $time) = split /T/, $dtstart; - my $recur; - if ($events{$id}->{rrule}) { - $recur = _icalToRecur($session, $properties->{startDate}, $events{$id}->{rrule}->[1]); + my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; + my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; + my $tz = $events{$id}->{dtstart}->[0]->{tzid}; + if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { + $tz = "UTC"; + } + + ($properties->{startDate}, $properties->{startTime}) = + split / /, WebGUI::DateTime->new( + year => $year, + month => $month, + day => $day, + hour => $hour, + minute => $minute, + second => $second, + time_zone => $tz, + )->toMysql; + $properties->{timeZone} = $tz; + } + elsif ($dtstart =~ /(\d{4})(\d{2})(\d{2})/) { + my ($year, $month, $day) = $dtstart =~ /(\d{4})(\d{2})(\d{2})/; + $properties->{startDate} = join "-",$year,$month,$day; + } + elsif ($dtstart) { + $session->log->warn( + "Workflow::Activity::CalendarUpdateFeeds" + . " -- '$dtstart' does not appear to be a valid date" + ); + $feedData->{errored}++; + next EVENT; + } + + my $dtend = $events{$id}->{dtend}->[1]; + my $duration = $events{$id}->{duration}->[1]; + if ($dtend =~ /T/) { + my ($date, $time) = split /T/, $dtend; + + my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; + my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; + my $tz = $events{$id}->{dtend}->[0]->{tzid}; + if (!$tz || !DateTime::TimeZone->is_valid_name($tz)) { + $tz = "UTC"; + } + + ($properties->{endDate}, $properties->{endTime}) = + split / /, WebGUI::DateTime->new( + year => $year, + month => $month, + day => $day, + hour => $hour, + minute => $minute, + second => $second, + time_zone => $tz, + )->toMysql; + $properties->{timeZone} = $tz; + } + elsif ($dtend =~ /(\d{4})(\d{2})(\d{2})/) { + my ($year, $month, $day) = $dtend =~ /(\d{4})(\d{2})(\d{2})/; + + my $endDateLet = WebGUI::DateTime->new( year => $year, month => $month, day => $day); + $endDateLet->subtract( days => 1 ); + $properties->{endDate} = $endDateLet->toDatabaseDate; + } + # If we can't parse it, forget the whole event + elsif ($dtend) { + $session->log->warn( + "Workflow::Activity::CalendarUpdateFeeds" + . " -- '$dtend' does not appear to be a valid date" + ); + $feedData->{errored}++; + next EVENT; + } + # No dtend, but we have duration! + elsif ($duration) { + my ($days, $hours, $minutes, $seconds) + = $duration =~ m{ + P + (?:(\d+)D)? # Days + T + (?:(\d+)H)? # Hours + (?:(\d+)M)? # Minutes + (?:(\d+)S)? # Seconds + }ix; + my $startDate = $properties->{startDate}; + # Fill in bogus value to get a WebGUI::DateTime object, + # we'll figure out what we actually need later + my $startTime = $properties->{startTime} || "00:00:00"; + my $datetime = WebGUI::DateTime->new($session,$startDate." ".$startTime); + + $datetime->add( + days => $days || 0, + hours => $hours || 0, + minutes => $minutes || 0, + seconds => $seconds || 0, + ); + + $properties->{endDate} = $datetime->toDatabaseDate; + # If it not an all-day event, set the end time too + if ($properties->{startTime}) { + $properties->{endTime} = $datetime->toDatabaseTime; + } + } + # No dtend, no duration, just copy the start + else { + $properties->{endDate} = $properties->{startDate}; + $properties->{endTime} = $properties->{startTime}; + } + + # If there are X-WebGUI-* fields + for my $key (grep /^x-webgui-/, keys %{$events{$id}}) { + my $property_name = $key; + $property_name =~ s/^x-webgui-//; + $property_name = lc $property_name; + + 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; + if ($events{$id}->{rrule}) { + $recur = _icalToRecur($session, $properties->{startDate}, $events{$id}->{rrule}->[1]); + } + + # save events for later + push @$eventList, { + properties => $properties, + recur => $recur, + }; } - - # save events for later - push @$eventList, { - properties => $properties, - recur => $recur, - }; } } } - my $currentVersionTag = WebGUI::VersionTag->getWorking($self->session, 1); + my $currentVersionTag = WebGUI::VersionTag->getWorking($session, 1); if ($currentVersionTag) { $currentVersionTag->clearWorking; } my $ttl = $self->getTTL; - $self->session->log->info( "Have to add " . scalar( @$eventList ) . " events..." ); - while (@$eventList) { + $session->log->info( "Have to add " . scalar( @$eventList ) . " events..." ); + while (@{ $eventList }) { if ($startTime + $ttl < time()) { $instance->setScratch('events', JSON::to_json($eventList)); - $instance->setScratch('feeds', JSON::to_json($feedList)); - my $newVersionTag = WebGUI::VersionTag->getWorking($self->session, 1); + $instance->setScratch('feeds', JSON::to_json($feedList)); + my $newVersionTag = WebGUI::VersionTag->getWorking($session, 1); if ($newVersionTag) { $newVersionTag->requestCommit; } @@ -379,28 +382,28 @@ sub execute { $session->user({user => $previousUser}); return $self->WAITING(1); } - my $eventData = shift @$eventList; - my $recur = $eventData->{recur}; + my $eventData = shift @$eventList; + my $recur = $eventData->{recur}; my $properties = $eventData->{properties}; - my $id = $properties->{feedUid}; - my $feed = $feedList->{$properties->{feedId}}; + my $id = $properties->{feedUid}; + my $feed = $feedList->{$properties->{feedId}}; # Update event - my $assetId = $self->session->db->quickScalar("select assetId from Event where feedUid=?",[$id]); - + my $assetId = $session->db->quickScalar("select assetId from Event where feedUid=?",[$id]); + # If this event already exists, update if ($assetId) { - $self->session->log->info( "Updating existing asset $assetId" ); - my $event = WebGUI::Asset->newByDynamicClass($self->session,$assetId); - + $session->log->info( "Updating existing asset $assetId" ); + my $event = WebGUI::Asset->newByDynamicClass($session,$assetId); + if ($event) { $event->update($properties); $feed->{updated}++; } } else { - $self->session->log->info( "Creating new Event!" ); - my $calendar = WebGUI::Asset->newByDynamicClass($self->session,$feed->{assetId}); + $session->log->info( "Creating new Event!" ); + my $calendar = WebGUI::Asset->newByDynamicClass($session,$feed->{assetId}); my $event = $calendar->addChild($properties, undef, undef, { skipAutoCommitWorkflows => 1}); $feed->{added}++; if ($recur) { @@ -408,12 +411,12 @@ sub execute { $event->generateRecurringEvents; } } - + # TODO: Only update if last-updated field is # greater than the event's lastUpdated property - $self->session->log->info( scalar @$eventList . " events left to load" ); + $session->log->info( scalar @$eventList . " events left to load" ); } - my $newVersionTag = WebGUI::VersionTag->getWorking($self->session, 1); + my $newVersionTag = WebGUI::VersionTag->getWorking($session, 1); if ($newVersionTag) { $newVersionTag->requestCommit; } @@ -422,8 +425,11 @@ sub execute { } for my $feedId (keys %$feedList) { my $feed = $feedList->{$feedId}; - $self->session->db->write("update Calendar_feeds set lastResult=?,lastUpdated=? where feedId=?", - ["Success! $feed->{added} added, $feed->{updated} updated, $feed->{errored} parsing errors",$dt,$feedId]); + my $calendar = WebGUI::Asset->newByDynamicClass($session, $feed->{assetId}); + my $feedData = $calendar->getFeed($feedId); + $feedData->{lastResult} = "Success! $feed->{added} added, $feed->{updated} updated, $feed->{errored} parsing errors"; + $feedData->{lastUpdated} = $dt; + $calendar->setFeed($feedId, $feedData); } $instance->deleteScratch('events'); $instance->deleteScratch('feeds'); @@ -472,7 +478,7 @@ sub _icalToRecur { every => $ical->{interval} || 1, }; my $type = lc $ical->{"freq"}; - + if ($type eq "daily") { $recur->{recurType} = 'daily'; } @@ -500,7 +506,7 @@ sub _icalToRecur { map { $icalMonths->{lc $1} } split(',', $ical->{bymonth}) ]; } - + if ($ical->{count}) { $recur->{endAfter} = $ical->{count}; } @@ -510,14 +516,14 @@ sub _icalToRecur { else { $recur->{endDate} = $date->clone->add(years => 2)->toDatabaseDate; } - + return $recur; } sub _icalToMySQL { my $dt = shift; my ($date, $time) = split /t/, $dt; - + my ($year, $month, $day) = $date =~ /(\d{4})(\d{2})(\d{2})/; my ($hour, $minute, $second) = $time =~ /(\d{2})(\d{2})(\d{2})/; return split / /, WebGUI::DateTime->new( diff --git a/t/Workflow/Activity/CalendarUpdateFeeds.t b/t/Workflow/Activity/CalendarUpdateFeeds.t index 877d9596f..7a9591738 100644 --- a/t/Workflow/Activity/CalendarUpdateFeeds.t +++ b/t/Workflow/Activity/CalendarUpdateFeeds.t @@ -20,6 +20,7 @@ use WebGUI::Asset::Wobject::Calendar; use Test::More; use Test::Deep; +use Data::Dumper; if (!$ENV{WEBGUI_LIVE}) { plan skip_all => 'No website available'; @@ -40,11 +41,10 @@ my $receiver = $home->addChild({ title => 'Receiving Calendar', }); -$session->db->setRow('Calendar_feeds', 'feedId', { - feedId => 'new', - assetId => $receiver->getId, +$receiver->addFeed({ url => $session->url->getSiteURL.$session->url->gateway($sender->getUrl('func=ical')), feedType => 'ical', + lastUpdated => 'never', }); my $dt = WebGUI::DateTime->new($session, time());