summaryrefslogtreecommitdiff
path: root/rt
diff options
context:
space:
mode:
authorivan <ivan>2010-05-19 02:32:01 +0000
committerivan <ivan>2010-05-19 02:32:01 +0000
commitab9be1d2bfedf205eab7a9d399ef22ee14117f53 (patch)
treea486b28b107b35859ec0f3f7e371d1616b68b335 /rt
parent363f89201b3ad61da89f2141373864e7e28943eb (diff)
add RTx::Calendar 0.07
Diffstat (limited to 'rt')
-rw-r--r--rt/etc/RT_Config.pm4
-rw-r--r--rt/etc/RT_Config.pm.in4
-rw-r--r--rt/lib/RTx/Calendar.pm230
-rw-r--r--rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head2
-rw-r--r--rt/share/html/Callbacks/RTx-Calendar/Ticket/Elements/Tabs/Default19
-rw-r--r--rt/share/html/Callbacks/RTx-Calendar/User/Elements/Tabs/Default9
-rw-r--r--rt/share/html/Elements/CalendarEvent129
-rw-r--r--rt/share/html/Elements/MyCalendar78
-rw-r--r--rt/share/html/NoAuth/Calendar/dhandler159
-rw-r--r--rt/share/html/NoAuth/css/calendar.css40
-rw-r--r--rt/share/html/NoAuth/images/created.pngbin0 -> 994 bytes
-rw-r--r--rt/share/html/NoAuth/images/created_due.pngbin0 -> 997 bytes
-rw-r--r--rt/share/html/NoAuth/images/due.pngbin0 -> 936 bytes
-rw-r--r--rt/share/html/NoAuth/images/reminder.pngbin0 -> 921 bytes
-rw-r--r--rt/share/html/NoAuth/images/resolved.pngbin0 -> 229 bytes
-rw-r--r--rt/share/html/NoAuth/images/started.pngbin0 -> 934 bytes
-rw-r--r--rt/share/html/NoAuth/images/starts.pngbin0 -> 935 bytes
-rw-r--r--rt/share/html/NoAuth/images/starts_due.pngbin0 -> 173 bytes
-rw-r--r--rt/share/html/NoAuth/images/updated.pngbin0 -> 191 bytes
-rw-r--r--rt/share/html/Prefs/Calendar.html123
-rw-r--r--rt/share/html/Prefs/Elements/CalendarFeed68
-rw-r--r--rt/share/html/Search/Calendar.html185
22 files changed, 1046 insertions, 4 deletions
diff --git a/rt/etc/RT_Config.pm b/rt/etc/RT_Config.pm
index b36a607..a976fb3 100644
--- a/rt/etc/RT_Config.pm
+++ b/rt/etc/RT_Config.pm
@@ -1346,7 +1346,7 @@ customized homepage ("RT at a glance").
=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>
@@ -1778,7 +1778,7 @@ C<Set(@Plugins, (qw(Extension::QuickDelete RT::FM)));>
=cut
-Set(@Plugins, ());
+Set(@Plugins, (qw(RTx::Calendar)));
=back
diff --git a/rt/etc/RT_Config.pm.in b/rt/etc/RT_Config.pm.in
index 03ee5d9..564682b 100644
--- a/rt/etc/RT_Config.pm.in
+++ b/rt/etc/RT_Config.pm.in
@@ -1346,7 +1346,7 @@ customized homepage ("RT at a glance").
=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>
@@ -1778,7 +1778,7 @@ C<Set(@Plugins, (qw(Extension::QuickDelete RT::FM)));>
=cut
-Set(@Plugins, ());
+Set(@Plugins, (qw(RTx::Calendar)));
=back
diff --git a/rt/lib/RTx/Calendar.pm b/rt/lib/RTx/Calendar.pm
new file mode 100644
index 0000000..515bd48
--- /dev/null
+++ b/rt/lib/RTx/Calendar.pm
@@ -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
index 0000000..c1f24c2
--- /dev/null
+++ b/rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head
@@ -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
index 0000000..cb46fda
--- /dev/null
+++ b/rt/share/html/Callbacks/RTx-Calendar/Ticket/Elements/Tabs/Default
@@ -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
index 0000000..06413e2
--- /dev/null
+++ b/rt/share/html/Callbacks/RTx-Calendar/User/Elements/Tabs/Default
@@ -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
index 0000000..3a6b00b
--- /dev/null
+++ b/rt/share/html/Elements/CalendarEvent
@@ -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
index 0000000..a54ab39
--- /dev/null
+++ b/rt/share/html/Elements/MyCalendar
@@ -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
index 0000000..4b4aa63
--- /dev/null
+++ b/rt/share/html/NoAuth/Calendar/dhandler
@@ -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
index 0000000..e313dff
--- /dev/null
+++ b/rt/share/html/NoAuth/css/calendar.css
@@ -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
index 0000000..4d5eeb9
--- /dev/null
+++ b/rt/share/html/NoAuth/images/created.png
Binary files 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
index 0000000..52dfc96
--- /dev/null
+++ b/rt/share/html/NoAuth/images/created_due.png
Binary files differ
diff --git a/rt/share/html/NoAuth/images/due.png b/rt/share/html/NoAuth/images/due.png
new file mode 100644
index 0000000..30a3aff
--- /dev/null
+++ b/rt/share/html/NoAuth/images/due.png
Binary files differ
diff --git a/rt/share/html/NoAuth/images/reminder.png b/rt/share/html/NoAuth/images/reminder.png
new file mode 100644
index 0000000..4370b69
--- /dev/null
+++ b/rt/share/html/NoAuth/images/reminder.png
Binary files differ
diff --git a/rt/share/html/NoAuth/images/resolved.png b/rt/share/html/NoAuth/images/resolved.png
new file mode 100644
index 0000000..09db36d
--- /dev/null
+++ b/rt/share/html/NoAuth/images/resolved.png
Binary files differ
diff --git a/rt/share/html/NoAuth/images/started.png b/rt/share/html/NoAuth/images/started.png
new file mode 100644
index 0000000..e177add
--- /dev/null
+++ b/rt/share/html/NoAuth/images/started.png
Binary files differ
diff --git a/rt/share/html/NoAuth/images/starts.png b/rt/share/html/NoAuth/images/starts.png
new file mode 100644
index 0000000..88064ba
--- /dev/null
+++ b/rt/share/html/NoAuth/images/starts.png
Binary files 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
index 0000000..16a4de4
--- /dev/null
+++ b/rt/share/html/NoAuth/images/starts_due.png
Binary files differ
diff --git a/rt/share/html/NoAuth/images/updated.png b/rt/share/html/NoAuth/images/updated.png
new file mode 100644
index 0000000..680e79a
--- /dev/null
+++ b/rt/share/html/NoAuth/images/updated.png
Binary files differ
diff --git a/rt/share/html/Prefs/Calendar.html b/rt/share/html/Prefs/Calendar.html
new file mode 100644
index 0000000..5fbdd27
--- /dev/null
+++ b/rt/share/html/Prefs/Calendar.html
@@ -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
index 0000000..4689343
--- /dev/null
+++ b/rt/share/html/Prefs/Elements/CalendarFeed
@@ -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
index 0000000..e711b86
--- /dev/null
+++ b/rt/share/html/Search/Calendar.html
@@ -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>