From b55266e821a447e1289ad261935bd95b59caf112 Mon Sep 17 00:00:00 2001 From: Doug Bell Date: Wed, 6 Dec 2006 08:02:42 +0000 Subject: [PATCH] Tentative final Calendar beta for testing. --- docs/changelog/7.x.x.txt | 3 + docs/upgrades/upgrade_7.2.3-7.3.0.pl | 7 +- lib/WebGUI/Asset/Event.pm | 8 + lib/WebGUI/Asset/Wobject/Calendar.pm | 306 +++++++++++++--- lib/WebGUI/DateTime.pm | 1 + .../Workflow/Activity/CalendarUpdateFeeds.pm | 341 ++++++++++++++++++ lib/WebGUI/i18n/English/Asset_Calendar.pm | 8 + 7 files changed, 627 insertions(+), 47 deletions(-) create mode 100755 lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 598406d0f..57483294b 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -40,6 +40,9 @@ - WebGUI::TabForm->addTab now returns the WebGUI::HTMLForm created. - WebGUI::AssetLineage::getLineage can now limit the number of records returned - fix: IP addresses for adminModeSubnets not using X-Forwarded-For properly + - The Events Calendar is now the new Calendar with some fun new features. + All your existing Events Calendars will be migrated automatically. + *** PLEASE READ THE GOTCHAS *** 7.2.3 - fix: minor bug with new template vars in Auth::createAccount diff --git a/docs/upgrades/upgrade_7.2.3-7.3.0.pl b/docs/upgrades/upgrade_7.2.3-7.3.0.pl index ee8e54c17..258dd95c9 100644 --- a/docs/upgrades/upgrade_7.2.3-7.3.0.pl +++ b/docs/upgrades/upgrade_7.2.3-7.3.0.pl @@ -23,9 +23,9 @@ addWikiAssets($session); deleteOldFiles($session); addFileFieldsToDataForm($session); makeRSSFromParentAlwaysHidden($session); -#addNewCalendar($session); -#migrateCalendars($session); -#removeOldCalendar($session); +addNewCalendar($session); +migrateCalendars($session); +removeOldCalendar($session); finish($session); # this line required #------------------------------------------------- @@ -121,6 +121,7 @@ CREATE TABLE `Event` ( `assetId` varchar(22) NOT NULL, `revisionDate` bigint(20) unsigned NOT NULL, `feedId` varchar(22) default NULL, + `feedUid` varchar(255) default NULL, `startDate` date default NULL, `endDate` date default NULL, `userDefined1` text, diff --git a/lib/WebGUI/Asset/Event.pm b/lib/WebGUI/Asset/Event.pm index 9ccfcb2bf..a7d9bb867 100644 --- a/lib/WebGUI/Asset/Event.pm +++ b/lib/WebGUI/Asset/Event.pm @@ -97,6 +97,14 @@ sub definition { fieldType => "Text", defaultValue => undef, }, + 'feedId' => { + fieldType => "Text", + defaultValue => undef, + }, + 'feedUid' => { + fieldType => "Text", + defaultValue => undef, + }, ); diff --git a/lib/WebGUI/Asset/Wobject/Calendar.pm b/lib/WebGUI/Asset/Wobject/Calendar.pm index a61a5a55b..757a6212b 100644 --- a/lib/WebGUI/Asset/Wobject/Calendar.pm +++ b/lib/WebGUI/Asset/Wobject/Calendar.pm @@ -27,6 +27,7 @@ use WebGUI::DateTime; use base 'WebGUI::Asset::Wobject'; use DateTime; +use JSON; =head1 Name @@ -348,7 +349,7 @@ sub duplicate my @events = $self->getLineage(["descendents"], { returnObjects => 1, - includeOnlyClasses => 'WebGUI::Asset::Event', + includeOnlyClasses => ['WebGUI::Asset::Event'], }); @@ -368,7 +369,10 @@ sub duplicate =head2 getEditForm -Adds an additional tab for feeds. +Adds an additional tab for feeds. + +TODO: Abstract the Javascript enough to export into extras/yui-webgui for use +in other areas. =cut @@ -379,10 +383,146 @@ sub getEditForm my $form = $self->SUPER::getEditForm; my $i18n = WebGUI::International->new($session,"Asset_Calendar"); - my $tab = $form->addTab("feeds",$i18n->get("feeds")); + my $tab = $form->addTab("feeds",$i18n->get("feeds")); + $tab->raw(""); + + + $tab->raw(<<'ENDJS'); + +ENDJS + + + $tab->raw(<<'ENDHTML'); + + + + + + + + + + + + +
 Feed URLStatusLast Updated 
+ENDHTML + # Add the existing feeds + my $feeds = $self->getFeeds(); + $tab->raw(''); + + + $tab->raw(""); return $form; } @@ -481,6 +621,31 @@ sub getEventsIn +#################################################################### + +=head2 getFeeds ( ) + +Gets a hashref of hashrefs of all the feeds attached to this calendar. + +TODO: Format lastUpdated into the user's time zone + +=cut + +sub getFeeds +{ + my $self = shift; + + return $self->session->db->buildHashRefOfHashRefs( + "select * from Calendar_feeds where assetId=?", + [$self->get("assetId")], + "feedId" + ); +} + + + + + #################################################################### =head2 getFirstEvent ( ) @@ -540,7 +705,7 @@ sub prepareView my $view = ucfirst lc $self->session->form->param("type") || ucfirst $self->get("defaultView") || "Month"; - $self->session->errorHandler->warn("Prepare view ".$view." with template ".$self->get("templateId".$view)); + #$self->session->errorHandler->warn("Prepare view ".$view." with template ".$self->get("templateId".$view)); my $template = WebGUI::Asset::Template->new($self->session, $self->get("templateId".$view)); $template->prepare; @@ -566,9 +731,9 @@ Adds / removes feeds from the feed trough. sub processPropertiesFromFormPost { - my $self = shift; - - # The super does most of the real work + my $self = shift; + my $session = $self->session; + my $form = $self->session->form; $self->SUPER::processPropertiesFromFormPost; @@ -578,6 +743,33 @@ sub processPropertiesFromFormPost } + ### Get feeds from the form + # Workaround WebGUI::Session::Form->param bug + my %feeds; + $feeds{$_}++ + for map { s/^feeds-//; $_; } grep /^feeds-/,($form->param()); + my @feeds = keys %feeds; + + # Delete old feeds that are not in @feeds + for my $feedId ($session->db->buildArray("select feedId from Calendar_feeds where assetId=?",[$self->get("assetId")])) + { + unless (grep /^$feedId$/, @feeds) + { + $session->db->write("delete from Calendar_feeds where feedId=? and assetId=?",[$feedId,$self->get("assetId")]); + } + } + + + # Create new feeds + for my $feedId (grep /^new(\d+)/, @feeds) + { + $session->db->setRow("Calendar_feeds","feedId",{ + feedId => "new", + assetId => $self->get("assetId"), + url => $form->param("feeds-".$feedId), + feedType => "ical", + }); + } } @@ -616,26 +808,26 @@ sub view # Set defaults if necessary unless ($params->{start}) { - if ($self->get("defaultDate") eq "first") - { + #if ($self->get("defaultDate") eq "first") + #{ #!! TODO: Get the first event's date # select startDate from Events # join assetLineage # order by startDate ASC, revisionDate DESC # limit 1 - } - elsif ($self->get("defaultDate") eq "last") - { + #} + #elsif ($self->get("defaultDate") eq "last") + #{ #!! TODO: Get the last event's date # select startDate from Events # join assetLineage # order by startDate DESC, revisionDate DESC # limit 1 - } - else - { + #} + #else + #{ $params->{start} = WebGUI::DateTime->from_epoch(epoch => time(), time_zone => $session->user->profileField("timeZone"))->toMysql; - } + #} } $params->{type} ||= $self->get("defaultView") || "Month"; @@ -1172,34 +1364,51 @@ sub www_ical #!!! Events from what time period should we show? Default perpage? # By default show the events for a month - my $type = $form->param("type") || $self->get("defaultView") || "month"; + my $type = $form->param("type") || lc($self->get("defaultView")) || "month"; my $start = $form->param("start"); my $end = $form->param("end"); + + + #!!! KLUDGE: + # An "adminId" may be passed as a parameter in order to facilitate + # calls between calendars on the same server getting administrator + # privileges + # I do not know how dangerous this could possibly be, so THIS MUST + # CHANGE + my $adminId = $form->param("adminId"); + if ($adminId + && ($self->session->db->quickArray("SELECT value FROM userSessionScratch WHERE sessionId=? and name=?",[$adminId,$self->get("assetId")]))[0] eq "SPECTRE") + { + $self->session->user({userId => 3}); + } + #/KLUDGE + + my $dt_start; unless ($start) { - if ($self->get("defaultDate") eq "first") - { + #if ($self->get("defaultDate") eq "first") + #{ #!! TODO: Get the first event's date # select startDate from Events # join assetLineage # order by startDate ASC, revisionDate DESC # limit 1 - } - elsif ($self->get("defaultDate") eq "last") - { + #} + #elsif ($self->get("defaultDate") eq "last") + #{ #!! TODO: Get the last event's date # select startDate from Events # join assetLineage # order by startDate DESC, revisionDate DESC # limit 1 - } - else - { + #} + #else + #{ $dt_start = WebGUI::DateTime->from_epoch(epoch => time(), time_zone => $session->user->profileField("timeZone")); - } + #} } else { @@ -1211,18 +1420,18 @@ sub www_ical my $dt_end; unless ($end) { - if ($type eq "month") - { + #if ($type eq "month") + #{ $dt_end = $dt_start->clone->add(months => 1); - } - elsif ($type eq "week") - { - $dt_end = $dt_start->clone->add(weeks => 1); - } - elsif ($type eq "day") - { - $dt_end = $dt_start->clone->add(days => 1); - } + #} + #elsif ($type eq "week") + #{ + # $dt_end = $dt_start->clone->add(weeks => 1); + #} + #elsif ($type eq "day") + #{ + # $dt_end = $dt_start->clone->add(days => 1); + #} } else { @@ -1246,32 +1455,41 @@ sub www_ical # Currently we only need # UID + # TODO: Use feedUid if one exists my $domain = $session->config->get("sitename")->[0]; - $ical .= qq{UID:}.$event->get("assetId").'@'.$domain."\n"; + $ical .= qq{UID:}.$event->get("assetId").'@'.$domain."\x0D\x0A"; # LAST-MODIFIED (revisionDate) $ical .= qq{LAST-MODIFIED:} . WebGUI::DateTime->new($event->get("revisionDate"))->toIcal - . "\n"; + . "\x0D\x0A"; # CREATED (creationDate) $ical .= qq{CREATED:} . WebGUI::DateTime->new($event->get("creationDate"))->toIcal - . "\n"; + . "\x0D\x0A"; # DTSTART - $ical .= qq{DTSTART:}.$event->getIcalStart."\n"; + $ical .= qq{DTSTART:}.$event->getIcalStart."\x0D\x0A"; # DTEND - $ical .= qq{DTEND:}.$event->getIcalEnd."\n"; + $ical .= qq{DTEND:}.$event->getIcalEnd."\x0D\x0A"; # Summary (the title) # Wrapped at 75 columns - $ical .= $self->wrapIcal("SUMMARY:".$event->get("title"))."\n"; + $ical .= $self->wrapIcal("SUMMARY:".$event->get("title"))."\x0D\x0A"; # Description (the text) # Wrapped at 75 columns - $ical .= $self->wrapIcal("DESCRIPTION:".$event->get("description"))."\n"; + $ical .= $self->wrapIcal("DESCRIPTION:".$event->get("description"))."\x0D\x0A"; + + + + # X-WEBGUI lines + $ical .= "X-WEBGUI-GROUPIDVIEW:".$event->get("groupIdView")."\x0D\x0A"; + $ical .= "X-WEBGUI-GROUPIDEDIT:".$event->get("groupIdEdit")."\x0D\x0A"; + $ical .= "X-WEBGUI-URL:".$event->get("url")."\x0D\x0A"; + $ical .= qq{END:VEVENT\n}; } diff --git a/lib/WebGUI/DateTime.pm b/lib/WebGUI/DateTime.pm index 19d034aa8..e920aacea 100755 --- a/lib/WebGUI/DateTime.pm +++ b/lib/WebGUI/DateTime.pm @@ -47,6 +47,7 @@ dealing with time zones. =cut + ####################################################################### =head2 new ( string ) diff --git a/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm b/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm new file mode 100755 index 000000000..408f08ebb --- /dev/null +++ b/lib/WebGUI/Workflow/Activity/CalendarUpdateFeeds.pm @@ -0,0 +1,341 @@ +package WebGUI::Workflow::Activity::CalendarUpdateFeeds; + + +=head1 LEGAL + + ------------------------------------------------------------------- + WebGUI is Copyright 2001-2006 Plain Black Corporation. + ------------------------------------------------------------------- + Please read the legal notices (docs/legal.txt) and the license + (docs/license.txt) that came with this distribution before using + this software. + ------------------------------------------------------------------- + http://www.plainblack.com info@plainblack.com + ------------------------------------------------------------------- + +=cut + +use strict; +use warnings; +use base 'WebGUI::Workflow::Activity'; + +use WebGUI::Asset::Wobject::Calendar; +use WebGUI::Asset::Event; +use WebGUI::DateTime; + +use LWP::UserAgent; + + +=head1 NAME + +Package WebGUI::Workflow::Activity::CalendarUpdateFeeds; + +=head1 DESCRIPTION + +Imports calendar events from Calendar feeds. + +=head1 SYNOPSIS + +See WebGUI::Workflow::Activity for details on how to use any activity. + +=head1 METHODS + +These methods are available from this class: + +=cut + + +#------------------------------------------------------------------- + +=head2 definition ( session, definition ) + +See WebGUI::Workflow::Activity::defintion() for details. + +=cut + +sub definition { + my $class = shift; + my $session = shift; + my $definition = shift; + my $i18n = WebGUI::International->new($session, "Asset_Calendar"); + push(@{$definition}, { + name=>$i18n->get("workflow updateFeeds"), + properties=> { } + }); + return $class->SUPER::definition($session,$definition); +} + + +#------------------------------------------------------------------- + +=head2 execute ( ) + +See WebGUI::Workflow::Activity::execute() for details. + +=cut + +sub execute { + my $self = shift; + $self->session->user({userId => 3}); + + + my $ua = LWP::UserAgent->new(agent => "WebGUI"); + my $dt = WebGUI::DateTime->new(time)->toMysql; + + my $sth = $self->session->db->prepare("select * from Calendar_feeds"); + $sth->execute(); + + + FEED:while (my $feed = $sth->hashRef) + { + #!!! 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}) + { + my $sessionId = $self->session->id->generate; + $feed->{url} .= ";adminId=".$sessionId; + $self->session->db->write("INSERT INTO userSessionScratch (sessionId,name,value) VALUES (?,?,?)", + [$sessionId,$feed->{assetId},"SPECTRE"]); + } + #/KLUDGE + #warn "FEED URL: ".$feed->{url} ."\n"; + + + + ## Somebody point me to a DECENT iCalendar parser... + # Text::vFile perhaps? + + # Get the feed + my $response = $ua->get($feed->{url}); + + if ($response->is_success) + { + 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 $current_entry = ""; + my %events; + my $line_number = 0; + 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; + } + elsif ($line =~ /^END:VEVENT$/i) + { + $active = 0; + # Flush event + my $uid = lc $current_event{uid}[1]; + delete $current_event{uid}; + $events{$uid} = {%current_event}; + %current_event = (); + } + elsif ($line =~ /^ /) + { + # Add to entry data + $current_entry .= substr $line, 1; + } + else + { + # Flush old entry + # KEY;ATTRIBUTE=VALUE;ATTRIBUTE=VALUE:KEYVALUE + my ($key_attrs,$value) = split /:/,$current_entry,2; + + my @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; + } + + # Unescape value + + + $current_event{lc $key} = [\%attrs,$value]; + + # Start new entry + $current_entry = $line; + } + } + + my $added = 0; + my $updated = 0; + 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 => $events{$id}->{description}->[1], + title => $events{$id}->{summary}->[1], + menuTitle => substr($events{$id}->{summary}->[1],0,15), + className => 'WebGUI::Asset::Event', + isHidden => 1, + }; + + # 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})/; + + ($properties->{startDate}, $properties->{startTime}) = + split / /, WebGUI::DateTime( + year => $year, + month => $month, + day => $day, + hour => $hour, + minute => $minute, + second => $second, + time_zone => "UTC", + )->toMysql; + } + 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; + } + + my $dtend = $events{$id}->{dtend}->[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})/; + + ($properties->{endDate}, $properties->{endTime}) = + split / /, WebGUI::DateTime( + year => $year, + month => $month, + day => $day, + hour => $hour, + minute => $minute, + second => $second, + time_zone => "UTC", + )->toMysql; + } + elsif ($dtend =~ /(\d{4})(\d{2})(\d{2})/) + { + my ($year, $month, $day) = $dtend =~ /(\d{4})(\d{2})(\d{2})/; + + $properties->{endDate} = join "-",$year,$month,$day; + } + + + + # If there are X-WebGUI-* fields + for my $key (grep /^X-WEBGUI-/, keys %{$events{$id}}) + { + my $property_name = $key; + $property_name =~ s/^X-WEBGUI-//; + + if (lc $property_name eq "groupidedit") + { + $properties->{groupIdEdit} = $events{$id}->{$key}->[1]; + } + elsif (lc $property_name eq "groupidview") + { + $properties->{groupIdView} = $events{$id}->{$key}->[1]; + } + elsif (lc $property_name eq "url") + { + $properties->{url} = $events{$id}->{$key}->[1]; + } + } + + + # Update event + my ($assetId) = $self->session->db->quickArray("select assetId from Event where feedUid=?",[$id]); + + # If this event already exists, update + if ($assetId) + { + #warn "Updating $assetId\n"; + + my $event = WebGUI::Asset->newByDynamicClass($self->session,$assetId); + + if ($event) + { + $event->update($properties); + $event->requestCommit; + $updated++; + } + } + else + { + my $calendar = WebGUI::Asset->newByDynamicClass($self->session,$feed->{assetId}); + my $event = $calendar->addChild($properties); + $event->requestCommit; + $added++; + } + + # TODO: Only update if last-updated field is + # greater than the event's lastUpdated property + } + + # Update the result and last updated fields + $self->session->db->write("update Calendar_feeds set lastResult=?,lastUpdated=? where feedId=?", + ["Success! $added added, $updated updated",$dt,$feed->{feedId}]); + } + else + { + # Update the result and last updated fields + $self->session->db->write("update Calendar_feeds set lastResult=?,lastUpdated=? where feedId=?", + [$response->message,$dt,$feed->{feedId}]); + } + } + + $sth->finish; + + return $self->COMPLETE; +} + + +=head1 BUGS + +We should probably be using some sort of parser for the iCalendar files. I did +not have time to make a decent observation but the following were observed and +rejected + + Data::ICal - Best one I saw. Rejected because I've run out of time + Text::vFile + Net::ICal + iCal::Parser - Bad data structure + Tie::iCal + +=cut + +1; + + diff --git a/lib/WebGUI/i18n/English/Asset_Calendar.pm b/lib/WebGUI/i18n/English/Asset_Calendar.pm index 681b09d74..fdb96e47c 100755 --- a/lib/WebGUI/i18n/English/Asset_Calendar.pm +++ b/lib/WebGUI/i18n/English/Asset_Calendar.pm @@ -263,6 +263,14 @@ our $I18N = { +#################### WORKFLOW ACTIVITIES #################### + 'workflow updateFeeds' => { + message => q{Update Calendar Feeds}, + lastUpdated => 0, + context => q{The name of the CalendarUpdateFeeds workflow activity}, + }, + + #################### ASSET NAME #################### 'assetName' => { message => q{Calendar},