add RTx::Calendar 0.07
authorivan <ivan>
Wed, 19 May 2010 02:32:01 +0000 (02:32 +0000)
committerivan <ivan>
Wed, 19 May 2010 02:32:01 +0000 (02:32 +0000)
22 files changed:
rt/etc/RT_Config.pm
rt/etc/RT_Config.pm.in
rt/lib/RTx/Calendar.pm [new file with mode: 0644]
rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head [new file with mode: 0644]
rt/share/html/Callbacks/RTx-Calendar/Ticket/Elements/Tabs/Default [new file with mode: 0644]
rt/share/html/Callbacks/RTx-Calendar/User/Elements/Tabs/Default [new file with mode: 0644]
rt/share/html/Elements/CalendarEvent [new file with mode: 0644]
rt/share/html/Elements/MyCalendar [new file with mode: 0644]
rt/share/html/NoAuth/Calendar/dhandler [new file with mode: 0644]
rt/share/html/NoAuth/css/calendar.css [new file with mode: 0644]
rt/share/html/NoAuth/images/created.png [new file with mode: 0644]
rt/share/html/NoAuth/images/created_due.png [new file with mode: 0644]
rt/share/html/NoAuth/images/due.png [new file with mode: 0644]
rt/share/html/NoAuth/images/reminder.png [new file with mode: 0644]
rt/share/html/NoAuth/images/resolved.png [new file with mode: 0644]
rt/share/html/NoAuth/images/started.png [new file with mode: 0644]
rt/share/html/NoAuth/images/starts.png [new file with mode: 0644]
rt/share/html/NoAuth/images/starts_due.png [new file with mode: 0644]
rt/share/html/NoAuth/images/updated.png [new file with mode: 0644]
rt/share/html/Prefs/Calendar.html [new file with mode: 0644]
rt/share/html/Prefs/Elements/CalendarFeed [new file with mode: 0644]
rt/share/html/Search/Calendar.html [new file with mode: 0644]

index b36a607..a976fb3 100644 (file)
@@ -1346,7 +1346,7 @@ customized homepage ("RT at a glance").
 
 =cut
 
 
 =cut
 
-Set($HomepageComponents, [qw(QuickCreate Quicksearch MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards)]);
+Set($HomepageComponents, [qw(QuickCreate Quicksearch MyCalendar MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards)]);
 
 =item C<@MasonParameters>
 
 
 =item C<@MasonParameters>
 
@@ -1778,7 +1778,7 @@ C<Set(@Plugins, (qw(Extension::QuickDelete RT::FM)));>
 
 =cut
 
 
 =cut
 
-Set(@Plugins, ());
+Set(@Plugins, (qw(RTx::Calendar)));
 
 =back
 
 
 =back
 
index 03ee5d9..564682b 100644 (file)
@@ -1346,7 +1346,7 @@ customized homepage ("RT at a glance").
 
 =cut
 
 
 =cut
 
-Set($HomepageComponents, [qw(QuickCreate Quicksearch MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards)]);
+Set($HomepageComponents, [qw(QuickCreate Quicksearch MyCalendar MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards)]);
 
 =item C<@MasonParameters>
 
 
 =item C<@MasonParameters>
 
@@ -1778,7 +1778,7 @@ C<Set(@Plugins, (qw(Extension::QuickDelete RT::FM)));>
 
 =cut
 
 
 =cut
 
-Set(@Plugins, ());
+Set(@Plugins, (qw(RTx::Calendar)));
 
 =back
 
 
 =back
 
diff --git a/rt/lib/RTx/Calendar.pm b/rt/lib/RTx/Calendar.pm
new file mode 100644 (file)
index 0000000..515bd48
--- /dev/null
@@ -0,0 +1,230 @@
+package RTx::Calendar;
+
+use strict;
+use DateTime;
+use DateTime::Set;
+
+our $VERSION = "0.07";
+
+sub FirstMonday {
+    my ($year, $month) = (shift, shift);
+    my $set = DateTime::Set->from_recurrence(
+       next => sub { $_[0]->truncate( to => 'day' )->subtract( days => 1 ) }
+    );
+
+    my $day = DateTime->new( year => $year, month => $month );
+
+    $day = $set->next($day) while $day->day_of_week != 1;
+    $day;
+
+}
+
+sub LastSunday {
+    my ($year, $month) = (shift, shift);
+    my $set = DateTime::Set->from_recurrence(
+       next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
+    );
+
+    my $day = DateTime->last_day_of_month( year => $year, month => $month );
+
+    $day = $set->next($day) while $day->day_of_week != 7;
+    $day;
+}
+
+# we can't use RT::Date::Date because it uses gmtime
+# and we need localtime
+sub LocalDate {
+  my $ts = shift;
+  my ($d,$m,$y) = (localtime($ts))[3..5];
+  sprintf "%4d-%02d-%02d", ($y + 1900), ++$m, $d;
+}
+
+sub DatesClauses {
+    my ($Dates, $begin, $end) = @_;
+
+    my $clauses = "";
+
+    my @DateClauses = map {
+       "($_ >= '" . $begin . "' AND $_ <= '" . $end . "')"
+    } @$Dates;
+    $clauses  .= " AND " . " ( " . join(" OR ", @DateClauses) . " ) "
+       if @DateClauses;
+
+    return $clauses
+}
+
+sub FindTickets {
+    my ($CurrentUser, $Query, $Dates, $begin, $end) = @_;
+
+    $Query .= DatesClauses($Dates, $begin, $end)
+       if $begin and $end;
+
+    my $Tickets = RT::Tickets->new($CurrentUser);
+    $Tickets->FromSQL($Query);
+
+    my %Tickets;
+    my %AlreadySeen;
+
+    while ( my $Ticket = $Tickets->Next()) {
+
+       # How to find the LastContacted date ?
+       for my $Date (@$Dates) {
+           my $DateObj = $Date . "Obj";
+           push @{ $Tickets{ LocalDate($Ticket->$DateObj->Unix) } }, $Ticket
+               # if reminder, check it's refering to a ticket
+               unless ($Ticket->Type eq 'reminder' and not $Ticket->RefersTo->First)
+                   or $AlreadySeen{  LocalDate($Ticket->$DateObj->Unix) }{ $Ticket }++;
+       }
+    }
+    return %Tickets;
+}
+
+# 
+# Take a user object and return the search with Description "calendar" if it exists
+# 
+sub SearchDefaultCalendar {
+    my $CurrentUser = shift;
+    my $Description = "calendar";
+
+    # I'm quite sure the loop isn't usefull but...
+    my @Objects = $CurrentUser->UserObj;
+    for my $object (@Objects) {
+       next unless ref($object) eq 'RT::User' && $object->id == $CurrentUser->Id;
+       my @searches = $object->Attributes->Named('SavedSearch');
+       for my $search (@searches) {
+           next if ($search->SubValue('SearchType')
+                        && $search->SubValue('SearchType') ne 'Ticket');
+
+           return $search
+               if "calendar" eq $search->Description;
+       }
+    }
+}
+
+
+1;
+
+__END__
+
+=head1 NAME
+
+RTx::Calendar - Calendar for RT due tasks
+
+=head1 VERSION
+
+This document describes version 0.07 of RTx::Calendar
+
+=head1 DESCRIPTION
+
+This RT extension provides a calendar view for your tickets and your
+reminders so you see when is your next due ticket. You can find it in
+the menu Search->Calendar.
+
+There's a portlet to put on your home page (see Prefs/MyRT.html)
+
+You can also enable ics (ICal) feeds for your default calendar and all
+your private searches in Prefs/Calendar.html. Authentication is magic
+number based so that you can give those feeds to other people.
+
+You can find screenshots on
+http://gaspard.mine.nu/dotclear/index.php?tag/rtx-calendar
+
+=head1 INSTALLATION
+
+If you upgrade from 0.02, see next part before.
+
+You need to install those three modules :
+
+  * Date::ICal
+  * Data::ICal
+  * DateTime::Set
+
+Install it like a standard perl module
+
+ perl Makefile.PL
+ make
+ make install
+
+If your RT is not in the default path (/opt/rt3) you must set RTHOME
+before doing the Makefile.PL
+
+=head1 CONFIGURATION
+
+=head2 Base configuration
+
+In RT 3.8 and later, to enable calendar plugin, you must add something
+like that in your etc/RT_SiteConfig.pm :
+
+  Set(@Plugins,(qw(RTx::Calendar)));
+
+To use MyCalendar portlet you must add MyCalendar to
+$HomepageComponents in etc/RT_SiteConfig.pm like that :
+
+  Set($HomepageComponents, [qw(QuickCreate Quicksearch MyCalendar
+     MyAdminQueues MySupportQueues MyReminders RefreshHomepage)]);
+
+To enable private searches ICal feeds, you need to give
+CreateSavedSearch and LoadSavedSearch rights to your users.
+
+=head2 Display configuration
+
+You can show the owner in each day box by adding this line to your
+etc/RT_SiteConfig.pm :
+
+    Set($CalendarDisplayOwner, 1);
+
+You can change which fields show up in the popup display when you
+mouse over a date in etc/RT_SiteConfig.pm :
+
+    @CalendarPopupFields = ('Status', 'OwnerObj->Name', 'DueObj->ISO');
+
+=head2 ICAL feed configuration
+
+By default, tickets are todo and reminders event. You can change this
+by setting $RT::ICalTicketType and $RT::ICalReminderType in etc/RT_SiteConfig.pm :
+
+  Set($ICalTicketType,   "Data::ICal::Entry::Event");
+  Set($ICalReminderType ,"Data::ICal::Entry::Todo");
+
+=head1 USAGE
+
+A small help section is available in /Prefs/Calendar.html
+
+=head1 UPGRADE FROM 0.02
+
+As I've change directory structure, if you upgrade from 0.02 you need
+to delete old files manually. Go in RTHOME/share/html (by default
+/opt/rt3/share/html) and delete those files :
+
+  rm -rf Callbacks/RTx-Calendar
+  rm Tools/Calendar.html
+
+RTx-Calendar may work without this but it's not very clean.
+
+=head1 BUGS
+
+=over
+
+=item *
+compatible only with RT 3.6 for the moment. If someone need
+compatibility with 3.4 I can work on this. And I will work on 3.7
+compatibility later.
+
+=back
+
+=head1 AUTHORS
+
+Nicolas Chuche E<lt>nchuche@barna.beE<gt>
+
+Idea borrowed from redmine's calendar (Thanks Jean-Philippe).
+
+=head1 COPYRIGHT
+
+Copyright 2007 by Nicolas Chuche E<lt>nchuche@barna.beE<gt>
+
+This program is free software; you can redistribute it and/or 
+modify it under the same terms as Perl itself.
+
+See L<http://www.perl.com/perl/misc/Artistic.html>
+
+=cut
diff --git a/rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head b/rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head
new file mode 100644 (file)
index 0000000..c1f24c2
--- /dev/null
@@ -0,0 +1,2 @@
+<link rel="stylesheet" href="<%$RT::WebPath%>/NoAuth/css/calendar.css" type="text/css" media="all" />
+
diff --git a/rt/share/html/Callbacks/RTx-Calendar/Ticket/Elements/Tabs/Default b/rt/share/html/Callbacks/RTx-Calendar/Ticket/Elements/Tabs/Default
new file mode 100644 (file)
index 0000000..cb46fda
--- /dev/null
@@ -0,0 +1,19 @@
+<%init>         
+my $args;
+$args= "?" . $m->comp(
+    '/Elements/QueryString',
+    Query   => $ARGS{'Query'}   || $session{'CurrentSearchHash'}->{'Query'},
+    Format  => $ARGS{'Format'}  || $session{'CurrentSearchHash'}->{'Format'},
+    OrderBy => $ARGS{'OrderBy'} || $session{'CurrentSearchHash'}->{'OrderBy'},
+    Order   => $ARGS{'Order'}   || $session{'CurrentSearchHash'}->{'Order'},
+    Page   => $ARGS{'Page'}   || $session{'CurrentSearchHash'}->{'Page'},
+    Rows    => $ARGS{'Rows'},
+  ) if ($ARGS{'Query'} or $session{'CurrentSearchHash'}->{'Query'});
+$args ||= '';
+
+$tabs->{'zz'} = { title =>loc("Calendar"),
+                  path  => "Search/Calendar.html$args" };
+</%init>
+<%args>
+$tabs
+</%args>
diff --git a/rt/share/html/Callbacks/RTx-Calendar/User/Elements/Tabs/Default b/rt/share/html/Callbacks/RTx-Calendar/User/Elements/Tabs/Default
new file mode 100644 (file)
index 0000000..06413e2
--- /dev/null
@@ -0,0 +1,9 @@
+<%init>
+    $tabs->{'z'} = { title =>loc("Calendar"),
+                          path  => "Prefs/Calendar.html" };
+</%init>
+<%args>
+$tabs
+$current_subtab => undef
+$Searches => undef
+</%args>
diff --git a/rt/share/html/Elements/CalendarEvent b/rt/share/html/Elements/CalendarEvent
new file mode 100644 (file)
index 0000000..3a6b00b
--- /dev/null
@@ -0,0 +1,129 @@
+<%args>
+$Date => undef
+$Object => undef
+$DateTypes => undef
+</%args>
+<div class="tooltip">
+<small>
+
+% if ($IsReminder and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today) {
+     <img src="<%$RT::WebImagesURL%>/reminder.png" />
+
+% } elsif ($DateTypes->{Resolved}
+%           and RTx::Calendar::LocalDate($Object->ResolvedObj->Unix) eq $today) {
+         <img src="<%$RT::WebImagesURL%>/resolved.png" />
+
+% } elsif ($DateTypes->{Starts} and $DateTypes->{Due} 
+%           and RTx::Calendar::LocalDate($Object->StartsObj->Unix) eq $today and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today ) {
+    <img src="<%$RT::WebImagesURL%>/starts_due.png" />
+
+% } elsif ($DateTypes->{Due} and $DateTypes->{Created} 
+%           and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today and RTx::Calendar::LocalDate($Object->CreatedObj->Unix) eq $today ) {
+    <img src="<%$RT::WebImagesURL%>/created_due.png" />
+
+% } elsif ($DateTypes->{Starts}
+%           and RTx::Calendar::LocalDate($Object->StartsObj->Unix) eq $today) {
+    <img src="<%$RT::WebImagesURL%>/starts.png" />
+
+% } elsif ($DateTypes->{Due} 
+%           and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today) {
+    <img src="<%$RT::WebImagesURL%>/due.png" />
+
+% } elsif ($DateTypes->{Created}
+%           and RTx::Calendar::LocalDate($Object->CreatedObj->Unix) eq $today) {
+    <img src="<%$RT::WebImagesURL%>/created.png" />
+
+% } elsif ($DateTypes->{Started}
+%           and RTx::Calendar::LocalDate($Object->StartedObj->Unix) eq $today) {
+    <img src="<%$RT::WebImagesURL%>/started.png" />
+
+% } elsif ($DateTypes->{LastUpdated}
+%           and RTx::Calendar::LocalDate($Object->LastUpdatedObj->Unix) eq $today) {
+    <img src="<%$RT::WebImagesURL%>/updated.png" />
+
+% }
+
+       <a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
+           <% $Object->QueueObj->Name %> #<% $TicketId %>
+           <% $display_owner ? 'by ' . $Object->OwnerObj->Name : '' %>
+           <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %></a></small><br />
+       <span class="tip">
+       <a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
+           <% $Object->QueueObj->Name %> #<% $TicketId %>
+        </a>
+       :</strong> <% $subject%><br />
+       <br />
+
+%# logic taken from Ticket/Search/Results.tsv
+% foreach my $attr (@display_fields) {
+%    my $value;
+%
+%    if ($attr =~ /(.*)->ISO$/ and $Object->$1->Unix <= 0) {
+%        $value = '-';
+%    } else {
+%        my $method = '$Object->'.$attr.'()';
+%        $method =~ s/->ISO\(\)$/->ISO( Timezone => 'user' )/;
+%        $value = eval $method;
+%        if ($@) {die "<b>Check your CalendarPopupFields config in etc/RT_SiteConfig.pm</b>.<br /><br />Failed to find \"$attr\" - ". $@}; 
+%    }
+       <strong><&|/l&><% $label_of{$attr} %></&>:</strong> <% $value %><br />
+% }
+
+<br />
+       </span>
+</div>
+
+<%init>
+use RTx::Calendar;
+
+my $today = $Date->strftime("%F");
+
+my $TicketId;
+
+my $ticket;
+my $subject;
+my $IsReminder;
+
+if ($Object->Type eq 'reminder') {
+    $IsReminder = 1;
+    if ($Object->RefersTo->First) {
+       $ticket   = $Object->RefersTo->First->TargetObj;
+       $TicketId = $ticket->Id;
+       $subject = $Object->Subject . " (" . $ticket->Subject . ")";
+    }
+} else {
+    $TicketId = $Object->Id;
+    $subject = $Object->Subject;
+}
+
+my $display_owner = $RT::CalendarDisplayOwner;
+$display_owner ||= RT->Config->Get('CalendarDisplayOwner')
+    if RT->can('Config');
+
+
+# 3.6 config
+my @display_fields = @RT::CalendarPopupFields;
+# 3.8 config
+# the if condition is weird but it doesn't work with 3.8.0 without the last part
+@display_fields = RT->Config->Get('CalendarPopupFields')
+    if 0 == @display_fields and RT->can('Config') and RT->Config->Get('CalendarPopupFields');
+
+# default
+if (0 == @display_fields) {
+    @display_fields = qw(OwnerObj->Name CreatedObj->ISO StartsObj->ISO
+                        StartedObj->ISO LastUpdatedObj->ISO DueObj->ISO
+                        ResolvedObj->ISO Status Priority
+                        Requestors->MemberEmailAddressesAsString);
+}
+
+
+my %label_of;
+for my $field (@display_fields) {
+    my $label = $field;
+    $label =~ s'Obj-.(?:AsString|Name|ISO)''g;
+    $label =~ s'-\>MemberEmailAddressesAsString''g;
+    $label_of{$field} = $label;
+}
+
+</%init>
diff --git a/rt/share/html/Elements/MyCalendar b/rt/share/html/Elements/MyCalendar
new file mode 100644 (file)
index 0000000..a54ab39
--- /dev/null
@@ -0,0 +1,78 @@
+<&|/Widgets/TitleBox,
+    title => loc("Calendar"),
+    title_href => "Search/Calendar.html" &>
+
+<table class="rtxcalendar">
+<thead>
+<tr>
+% my $date = $begin->clone;
+% while ( $date <= $end ) {
+<th width="14%"><%$rtdate->GetWeekday($date->day_of_week % 7)%></th>
+% $date = $set->next($date);
+% }
+</tr>
+</thead>
+<tbody>
+<tr>
+% $date = $begin->clone;
+% while ($date <= $end) {
+<td>
+<p class="date"><%$date->day%></p>
+% for my $t (@{ $Tickets{$date->strftime("%F")} }) {
+<& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
+% }
+</td>
+% $date = $set->next($date);
+% }
+</tr>
+</tbody>
+</table>
+
+ </&>
+
+<%INIT>
+
+use RTx::Calendar;
+
+my $title = loc("Calendar");
+
+my $rtdate = RT::Date->new($session{'CurrentUser'});
+
+my @DateTypes = qw/Created Starts Started Due LastUpdated Resolved/;
+
+my $today = DateTime->today;
+
+# this line is used to debug MyCalendar
+# $today = DateTime->new(year => 2007, month => 4, day => 11);
+
+my $begin = $today->clone->subtract( days => 3);
+my $end   = $today->clone->add( days => 3);
+
+# use this to loop over days until $end
+my $set = DateTime::Set->from_recurrence(
+    next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
+);
+
+my $Query = "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
+ AND ( Owner = '" . $session{CurrentUser}->Id ."' OR Owner = 'Nobody'  )
+ AND ( Type = 'reminder' OR 'Type' = 'ticket' )";
+my $Format = "__Starts__ __Due__";
+
+if ( my $Search = RTx::Calendar::SearchDefaultCalendar($session{CurrentUser}) ) {
+  $Format = $Search->SubValue('Format');
+  $Query = $Search->SubValue('Query');
+}
+
+# we search all date types in Format string
+my @Dates = grep { $Format =~ m/__${_}(Relative)?__/ } @DateTypes;
+
+# used to display or not a date in Element/CalendarEvent
+my %DateTypes = map { $_ => 1 } @Dates;
+
+$Query .= RTx::Calendar::DatesClauses(\@Dates, $begin->strftime("%F"), $end->strftime("%F"));
+
+# print STDERR $Query, "\n";
+
+my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $Query, \@Dates);
+
+</%INIT>
diff --git a/rt/share/html/NoAuth/Calendar/dhandler b/rt/share/html/NoAuth/Calendar/dhandler
new file mode 100644 (file)
index 0000000..4b4aa63
--- /dev/null
@@ -0,0 +1,159 @@
+<%init>
+
+use Data::ICal;
+use Data::ICal::Entry::Todo;
+use Data::ICal::Entry::Event;
+use Date::ICal;
+
+$RT::ICalTicketType   ||= "Data::ICal::Entry::Todo";
+$RT::ICalReminderType ||= "Data::ICal::Entry::Event";
+
+my ($UserId, $SearchId, $MagicNumber);
+my $arg = $m->dhandler_arg;
+
+if ($arg =~ m{^(\d+)@(\d+)/(.*)$}) {
+    $UserId = $1;
+    $SearchId = $2;
+    $MagicNumber = $3;
+} elsif ($arg =~ m{^(\d+)/(.*)}) {
+    $UserId = $1;
+    $MagicNumber = $2;
+} else {
+    Abort("Corrupted URL.");
+}
+
+my $CurrentUser = new RT::CurrentUser();
+$CurrentUser->LoadById($UserId);
+my $user = $CurrentUser->Name;
+
+# if no user, abort
+unless ($CurrentUser->Id) {
+    $RT::Logger->error("No such user id $UserId from $ENV{'REMOTE_ADDR'}");
+    $m->out("RT/".$RT::VERSION ." ".404 ."\n\nno such file\n");
+    $m->abort;
+}
+
+# verify user has LoadSavedSearch right
+if ($SearchId and not $CurrentUser->HasRight( Right => 'LoadSavedSearch',
+                                              Object=> $RT::System )) {
+    $RT::Logger->error("not enough rights for user $user from $ENV{'REMOTE_ADDR'}");
+    $m->out("RT/".$RT::VERSION ." ".404 ."\n\nno such file\n");
+    $m->abort;
+}
+
+
+# if MagicNumber doesn't match the one stored in database, abort
+my $Search;
+my $ICalAttribute;
+if ($SearchId) {
+    $Search = $CurrentUser->Attributes->WithId($SearchId);
+    $ICalAttribute = $Search->FirstAttribute('ICalURL');
+} else {
+    $ICalAttribute = $CurrentUser->UserObj->FirstAttribute('ICalURL');
+}
+
+unless ($ICalAttribute) {
+    $RT::Logger->error("No such ICal feed for $user from $ENV{'REMOTE_ADDR'}");
+    $m->out("RT/".$RT::VERSION ." ".404 ."\n\nno such file\n");
+    $m->abort;
+}
+
+
+if ($MagicNumber ne $ICalAttribute->Content) {
+    $RT::Logger->error("FAILED LOGIN for $user from $ENV{'REMOTE_ADDR'}");
+    $m->out("RT/".$RT::VERSION ." ".404 ."\n\nno such file\n");
+    $m->abort;
+}
+
+my $Tickets   = RT::Tickets->new($CurrentUser);
+
+my $Query = "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
+ AND ( Owner = '" . $CurrentUser->Id ."' OR Owner = 'Nobody'  )
+ AND ( Type = 'reminder' OR 'Type' = 'ticket' )";
+
+$Query = $Search->SubValue('Query')
+    if $Search;
+
+$Query .= " AND ( Due > '1970-01-01' OR Starts > '1970-01-01' )";
+
+$Tickets->FromSQL($Query);
+
+$Tickets->OrderBy(FIELD => 'Due', ORDER => 'ASC');
+
+my $calendar = Data::ICal->new();
+
+my ($uid) = $RT::WebURL =~ m{https?://([^:]+)};
+
+while (my $Ticket = $Tickets->Next ) {
+
+    my $event;
+    if ($Ticket->Type eq 'ticket') {
+       $event = add_todo($Ticket, $uid);
+    } else {
+       $event = add_event($Ticket, $uid);
+    }
+    next unless $event;
+    $calendar->add_entry($event);
+}
+
+my $cal = $calendar->as_string;
+
+$r->content_type('text/calendar;charset=utf-8');
+$m->clear_buffer();
+$m->out($cal);
+$m->abort; 
+
+sub add_event {
+    my ($Reminder, $uid) = @_;
+
+    return unless defined $Reminder->RefersTo->First;
+    my $Ticket  = $Reminder->RefersTo->First->TargetObj;
+    
+    my %event = (
+       summary => $Reminder->Subject ? $Reminder->Subject : '',
+       url        => "${RT::WebURL}/Ticket/Display.html?id=" . $Ticket->id,
+       uid        => Date::ICal->new( epoch => time() )->ical() . "-" . $Reminder->Id . "@" . $uid,
+       categories => $Ticket->QueueObj->Name,
+       dtstart     => Date::ICal->new( epoch => $Reminder->DueObj->Unix )->ical,
+    );
+
+    my $event = $RT::ICalReminderType->new();
+    $event->add_properties(%event);
+
+    return $event;
+}
+
+sub add_todo {
+    my ($Ticket, $uid) = @_;
+
+    my %vtodo = (
+       summary    => $Ticket->Subject ? $Ticket->Subject : '',
+       dtstart    => Date::ICal->new( epoch => $Ticket->CreatedObj->Unix )->ical,
+       url        => "${RT::WebURL}/Ticket/Display.html?id=" . $Ticket->id,
+       uid        => Date::ICal->new( epoch => time() )->ical() . "-" . $Ticket->Id . "@" . $uid,
+       categories => $Ticket->QueueObj->Name,
+    );
+
+    $vtodo{due} = Date::ICal->new( epoch => $Ticket->DueObj->Unix )->ical,
+        if $Ticket->DueObj;
+
+    if ($Ticket->OwnerObj->Id != $RT::Nobody->Id and $Ticket->OwnerObj->EmailAddress) {
+       $vtodo{organizer} = "MAILTO:" . $Ticket->OwnerObj->EmailAddress;
+       $vtodo{attendee} = "MAILTO:" . $Ticket->OwnerObj->EmailAddress;
+    } elsif ($Ticket->QueueObj->CommentAddress) {
+       $vtodo{organizer} = "MAILTO:" . $Ticket->QueueObj->CommentAddress;
+       $vtodo{attendee} = "MAILTO:" . $Ticket->QueueObj->CommentAddress; 
+    }
+
+    $vtodo{priority} = $Ticket->Priority
+       if $Ticket->Priority;
+
+    my $vtodo = $RT::ICalTicketType->new();
+    $vtodo->add_properties(%vtodo);
+
+    return $vtodo;
+}
+
+
+
+</%init>
diff --git a/rt/share/html/NoAuth/css/calendar.css b/rt/share/html/NoAuth/css/calendar.css
new file mode 100644 (file)
index 0000000..e313dff
--- /dev/null
@@ -0,0 +1,40 @@
+.tooltip{position:relative;z-index:1;}
+.tooltip:hover{z-index:5;color:#000;}
+.tooltip span.tip{display: none; text-align:left;}
+
+div.tooltip:hover span.tip{
+display:block;
+position:absolute;
+top:12px; left:24px; width:350px;
+border:1px solid #555;
+background-color:#fff;
+padding: 4px;
+font-size: 0.8em;
+color:#505050;
+}
+
+.date {
+text-align: right;
+}
+
+table.rtxcalendar {
+    width:100%;
+    border-collapse: collapse;
+    border: 1px dotted #d0d0d0;
+    margin-bottom: 6px;
+}
+
+table.rtxcalendar td {
+    border: 1px solid #d7d7d7;
+    vertical-align: top;
+}
+
+table.rtxcalendar th {
+    border: 1px solid #d7d7d7;
+    background: #eee;
+}
+table.rtxcalendar tbody th {
+    border: 1px solid #d7d7d7;
+    background: #eee;
+    font-weight: normal;
+}
diff --git a/rt/share/html/NoAuth/images/created.png b/rt/share/html/NoAuth/images/created.png
new file mode 100644 (file)
index 0000000..4d5eeb9
Binary files /dev/null and b/rt/share/html/NoAuth/images/created.png differ
diff --git a/rt/share/html/NoAuth/images/created_due.png b/rt/share/html/NoAuth/images/created_due.png
new file mode 100644 (file)
index 0000000..52dfc96
Binary files /dev/null and b/rt/share/html/NoAuth/images/created_due.png differ
diff --git a/rt/share/html/NoAuth/images/due.png b/rt/share/html/NoAuth/images/due.png
new file mode 100644 (file)
index 0000000..30a3aff
Binary files /dev/null and b/rt/share/html/NoAuth/images/due.png differ
diff --git a/rt/share/html/NoAuth/images/reminder.png b/rt/share/html/NoAuth/images/reminder.png
new file mode 100644 (file)
index 0000000..4370b69
Binary files /dev/null and b/rt/share/html/NoAuth/images/reminder.png differ
diff --git a/rt/share/html/NoAuth/images/resolved.png b/rt/share/html/NoAuth/images/resolved.png
new file mode 100644 (file)
index 0000000..09db36d
Binary files /dev/null and b/rt/share/html/NoAuth/images/resolved.png differ
diff --git a/rt/share/html/NoAuth/images/started.png b/rt/share/html/NoAuth/images/started.png
new file mode 100644 (file)
index 0000000..e177add
Binary files /dev/null and b/rt/share/html/NoAuth/images/started.png differ
diff --git a/rt/share/html/NoAuth/images/starts.png b/rt/share/html/NoAuth/images/starts.png
new file mode 100644 (file)
index 0000000..88064ba
Binary files /dev/null and b/rt/share/html/NoAuth/images/starts.png differ
diff --git a/rt/share/html/NoAuth/images/starts_due.png b/rt/share/html/NoAuth/images/starts_due.png
new file mode 100644 (file)
index 0000000..16a4de4
Binary files /dev/null and b/rt/share/html/NoAuth/images/starts_due.png differ
diff --git a/rt/share/html/NoAuth/images/updated.png b/rt/share/html/NoAuth/images/updated.png
new file mode 100644 (file)
index 0000000..680e79a
Binary files /dev/null and b/rt/share/html/NoAuth/images/updated.png differ
diff --git a/rt/share/html/Prefs/Calendar.html b/rt/share/html/Prefs/Calendar.html
new file mode 100644 (file)
index 0000000..5fbdd27
--- /dev/null
@@ -0,0 +1,123 @@
+<%args>
+$ChangeURL   => undef
+$ResetURL    => undef
+$SearchType  => 'Ticket'
+$HiddenField => undef
+</%args>
+
+<& /Elements/Header, Title => $title &>
+<& /User/Elements/Tabs,
+    current_tab => 'Prefs/Calendar.html',
+    Title => $title
+&>
+
+<&| /Widgets/TitleBox, title => loc('ICal Feeds (ics)') &>
+
+<&| /Widgets/TitleBox, title => 'Help' &>
+
+<h3>displaying reminders :</h3>
+<p>If you want to have reminders in a search you need to go in the <a
+href="<%$RT::WebPath%>/Search/Edit.html"><%loc("Edit Query")%></a> tab
+of the <%loc("query builder")%> and add something like that :
+
+ <pre>
+   AND ( Type = 'ticket' OR Type = 'reminder' )
+</pre>
+</p>
+
+<h3>displaying other kind of dates :</h3>
+<p>By default RTx::Calendar display Due and Starts dates. You can
+select other kind of events you want with the <%loc("Display
+Columns")%> section in the <a
+href="<%$RT::WebPath%>/Search/Build.html"><%loc("Query
+Builder")%></a>. The following one will display the two latter and
+LastUpdated dates :
+
+<pre>
+  '&lt;small&gt;__Due__&lt;/small&gt;',
+  '&lt;small&gt;__Starts__&lt;/small&gt;',
+  '&lt;small&gt;__LastUpdated__&lt;/small&gt;'
+</pre>
+</p>
+
+<h3>changing the default query :</h3>
+<p>You can change the default Query of Calendar.html and MyCalendar
+portlet by saving a query with the name <code>calendar</code> in the
+<a href="<%$RT::WebPath%>/Search/Build.html"><%loc("Query
+Builder")%></a>.</p>
+
+</&>
+
+<& /Prefs/Elements/CalendarFeed &>
+
+% # only allow this part if 
+% if ($AllowSearch) {
+
+% my $search_count;
+
+%   # I'm quite sure the loop isn't usefull but...
+%   my @Objects = $session{CurrentUser}->UserObj;
+%   for my $object (@Objects) {
+%     next unless ref($object) eq 'RT::User' && $object->id == $session{'CurrentUser'}->Id;
+%     my @searches = $object->Attributes->Named('SavedSearch');
+%     for my $search (@searches) {
+%       next if ($search->SubValue('SearchType')
+%              && $search->SubValue('SearchType') ne $SearchType);
+%       $search_count++;
+<& /Prefs/Elements/CalendarFeed, Object => $object, Search => $search &>
+
+%     }
+%   }
+%   unless ($search_count) {
+
+<&| /Widgets/TitleBox, title => loc('Private Search ICal feeds')
+                     , title_class=> 'inverse'
+                     , color => "#993333" &>
+
+You can add private ICal feeds by saving new queries in <a
+href="<%$RT::WebPath . '/Search/Build.html'%>">the Query Builder</a>
+
+</&>
+
+%   }
+% } else {
+%#<&| /Widgets/TitleBox, title => loc('Private Search ICal feeds')
+%#                     , title_class=> 'inverse'
+%#                     , color => "#993333" &>
+%#
+%#<%loc('Private search ICal feeds disabled. To enable them, ask your admin for "[_1]" and "[_2]" rights',
+%#       loc('CreateSavedSearch'),
+%#       loc('LoadSavedSearch') )%>
+%#
+%#</&>
+% }
+
+</&>
+
+<%INIT>
+use Digest::SHA1;
+use RT::SavedSearches;
+
+my $title = loc("Calendar Prefs");
+my $AllowSearch;
+
+$AllowSearch = 1
+ if $session{'CurrentUser'}->HasRight( Right => 'LoadSavedSearch',
+                                       Object=> $RT::System );
+
+my $object;
+
+if ($HiddenField eq 'Private') {
+   $object = $session{CurrentUser}->UserObj;
+} elsif ($AllowSearch and my ($SearchId) = $HiddenField =~ m/SavedSearch\-(\d+)/) {
+    $object = $session{CurrentUser}->Attributes->WithId($SearchId);
+}
+
+if (defined $ChangeURL) {
+  my @args = $object->SetAttribute(Name => 'ICalURL', Content => Digest::SHA1::sha1_base64(time));
+} elsif (defined $ResetURL) {
+  my @args = $object->DeleteAttribute('ICalURL');
+}
+
+
+</%INIT>
diff --git a/rt/share/html/Prefs/Elements/CalendarFeed b/rt/share/html/Prefs/Elements/CalendarFeed
new file mode 100644 (file)
index 0000000..4689343
--- /dev/null
@@ -0,0 +1,68 @@
+<%args>
+$Search => undef
+$Object => undef
+$HiddenField => undef
+</%args>
+
+<&| /Widgets/TitleBox, title => $title &>
+
+% if ($FeedText) {
+<p><%$FeedText%></p>
+% } else {
+This feed will show tickets with due date find with query:<br />
+"<%$Search->SubValue('Query')%>".
+% }
+
+% if ($ICalURL) {
+<p>Your can paste this url in your calendar  : <b><a href="<%$link%>"><%$link%></a></b><p>
+<table>
+<tr>
+<td>
+<form action="<%$RT::WebPath%>/Prefs/Calendar.html" method="post">
+<input type="hidden" name="HiddenField" value="<%$HiddenField%>" />
+<input type="submit" class="button" name="ResetURL" value="<%loc('Disable Feed')%>" />
+</form>
+</td>
+<td>
+<form action="<%$RT::WebPath%>/Prefs/Calendar.html" method="post">
+<input type="hidden" name="HiddenField" value="<%$HiddenField%>" />
+<input type="submit" class="button" name="ChangeURL" value="<%loc('Change Feed URL')%>" />
+</form>
+</td>
+</tr>
+</table>
+% } else {
+
+<form action="<%$RT::WebPath%>/Prefs/Calendar.html" method="post">
+<input type="hidden" name="HiddenField" value="<%$HiddenField%>" />
+<input type="submit" class="button" name="ChangeURL" value="<%loc('Enable Feed')%>" />
+</form>
+% }
+
+</&>
+
+<%init>
+my $title;
+my $ICalURL;
+my $Id;
+my $FeedText;
+my $link;
+
+if ($Object) {
+  $title = loc('Feed for "') . ($Search->Description || loc('Unnamed search')) . '" search';
+  $HiddenField = "SavedSearch-" . $Search->Id;
+  $ICalURL = $Search->FirstAttribute('ICalURL');
+  $Id = $session{CurrentUser}->Id . "@" . $Search->Id;
+  $title .= " (disabled)" unless $ICalURL;
+} else {
+  $title = loc('Feed for default calendar');
+  $HiddenField = "Private";
+  $ICalURL = $session{CurrentUser}->UserObj->FirstAttribute('ICalURL');
+  $Id = $session{CurrentUser}->Id;
+  $FeedText = "This feed will show yours and Nobody's tasks with due date.";
+}
+
+$link = $RT::WebURL . "NoAuth/Calendar/" . $Id . "/" . $ICalURL->Content
+  if $ICalURL;
+
+</%init>
\ No newline at end of file
diff --git a/rt/share/html/Search/Calendar.html b/rt/share/html/Search/Calendar.html
new file mode 100644 (file)
index 0000000..e711b86
--- /dev/null
@@ -0,0 +1,185 @@
+<%args>
+$Month => (localtime)[4]
+$Year => (localtime)[5] + 1900
+$Query => undef
+$Format => undef
+$Order => undef
+$OrderBy => undef
+$RowsPerPage => undef
+$NewQuery => 0
+</%args>
+
+<& /Elements/Header, Title => $title &>
+<& /Ticket/Elements/Tabs, 
+    current_tab => "Search/Calendar.html?$QueryString",
+    Title => $title &>
+<&| /Widgets/TitleBox,
+     title => loc('Calendar for ') . $rtdate->GetMonth($Month) . " $Year" ,
+     title_class=> 'inverse',
+     color => "#993333" &>
+
+<table width="100%">
+<tr>
+<td align="left">
+% my ($PMonth, $PYear) = ($Month - 1, $Year);
+% if ($PMonth < 0) {
+%    $PYear--;
+%    $PMonth = 11;
+% }
+<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">«<%$rtdate->GetMonth($PMonth)%></a>
+</td>
+<td align="center">
+<a href="<%$RT::WebPath%>/Prefs/Calendar.html">Calendar Preferences and Help</a>
+</td>
+<td align="right">
+% my ($NMonth, $NYear) = ($Month + 1, $Year);
+% if ($NMonth > 11) {
+%    $NYear++;
+%    $NMonth = 0;
+% }
+<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%>»</a>
+</td>
+</tr>
+</table>
+
+<table class="rtxcalendar">
+<thead>
+<tr>
+<th></th>
+% for (1 .. 6, 0) {
+<th width="14%"><%$rtdate->GetWeekday($_)%></th>
+% }
+</tr>
+</thead>
+<tbody>
+<tr>
+% while ($date <= $end) {
+%   if ( $date->day_of_week == 1) {
+<th><% $date->week_number %></th>
+%   }
+<td class="<% $date->month != ($Month + 1) ? 'oddline' : '' %>"
+    style="width:14%;<%  DateTime->compare($today, $date) == 0 ? 'background:#f6f7f8;' : '' %>"
+>
+<p class="date"><%$date->day%></p>
+% for my $t ( @{ $Tickets{$date->strftime("%F")} } ) {
+<& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
+% }
+</td>
+% $date = $set->next($date);
+% if ( $date->day_of_week == 1) {
+</tr><tr>
+% }
+% }
+</tr>
+</tbody>
+</table>
+
+<table width="100%">
+<tr>
+<td align="left">
+<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">«<%$rtdate->GetMonth($PMonth)%></a>
+</td>
+<td align="right">
+<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%>»</a>
+</td>
+</tr>
+</table>
+
+
+<table width="100%">
+<tr>
+<td valign="top" align="center" width="80%">
+<form action="<%$RT::WebPath%>/Search/Calendar.html?<%$QueryString%>" method="post">
+
+<select name="Month">
+% for (0..11) {
+<option value="<%$_%>" <% $_ == $Month ? 'selected' : ''%> ><%$rtdate->GetMonth($_)%></option>
+% }
+</select>
+% my $year = (localtime)[5] + 1900;
+<select name="Year">
+% for ( ($year-5) .. ($year+5)) {
+<option value="<%$_%>" <% $_ == $Year ? 'selected' : ''%>><%$_%></option>
+% }
+</select>
+
+<& /Elements/Submit&>
+</form>
+</td>
+<td valign="top" width="50%" align="right">
+<img src="<%$RT::WebImagesURL%>/created.png" /> : <&|/l&>Created</&><br />
+<img src="<%$RT::WebImagesURL%>/due.png" /> : <&|/l&>Due</&><br />
+<img src="<%$RT::WebImagesURL%>/resolved.png" /> : <&|/l&>Resolved</&><br />
+<img src="<%$RT::WebImagesURL%>/updated.png" /> : <&|/l&>Last Updated</&><br />
+<img src="<%$RT::WebImagesURL%>/created_due.png" /> : <&|/l&>Created</&>, <&|/l&>Due</&><br />
+<img src="<%$RT::WebImagesURL%>/reminder.png" /> : <&|/l&>Reminders</&><br />
+<img src="<%$RT::WebImagesURL%>/starts.png" /> : <&|/l&>Starts</&><br />
+<img src="<%$RT::WebImagesURL%>/started.png" /> : <&|/l&>Started</&><br />
+<img src="<%$RT::WebImagesURL%>/starts_due.png" /> : <&|/l&>Starts</&>, <&|/l&>Due</&><br />
+
+
+</td>
+</table>
+
+</&>
+
+</html>
+<%INIT>
+use RTx::Calendar;
+
+my $title = loc("Calendar");
+
+my @DateTypes = qw/Created Starts Started Due LastUpdated Resolved/;
+
+my $rtdate = RT::Date->new($session{'CurrentUser'});
+
+my $today = DateTime->today;
+my $date  = RTx::Calendar::FirstMonday($Year, $Month + 1);
+my $end   = RTx::Calendar::LastSunday($Year, $Month + 1);
+
+# use this to loop over days until $end
+my $set = DateTime::Set->from_recurrence(
+    next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
+);
+
+my $QueryString = 
+      $m->comp(
+        '/Elements/QueryString',
+        Query   => $Query,
+        Format  => $Format,
+        Order   => $Order,
+        OrderBy => $OrderBy,
+        Rows    => $RowsPerPage
+      )
+      if ($Query);
+
+$QueryString ||= 'NewQuery=1';
+
+# Default Query and Format
+my $TempFormat = "__Starts__ __Due__";
+my $TempQuery = "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
+ AND ( Owner = '" . $session{CurrentUser}->Id ."' OR Owner = 'Nobody'  )
+ AND ( Type = 'reminder' OR 'Type' = 'ticket' )"; 
+
+if ( my $Search = RTx::Calendar::SearchDefaultCalendar($session{CurrentUser}) ) {
+  $TempFormat = $Search->SubValue('Format');
+  $TempQuery = $Search->SubValue('Query');
+}
+
+# we overide them if needed
+$TempQuery  = $Query  if $Query;
+$TempFormat = $Format if $Format;
+
+# we search all date types in Format string
+my @Dates = grep { $TempFormat =~ m/__${_}(Relative)?__/ } @DateTypes;
+
+# used to display or not a date in Element/CalendarEvent
+my %DateTypes = map { $_ => 1 } @Dates;
+
+$TempQuery .= RTx::Calendar::DatesClauses(\@Dates, $date->strftime("%F"), $end->strftime("%F"));
+
+# print STDERR ("-" x 30), "\n", $TempQuery, "\n";
+
+my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
+
+</%INIT>