From ab9be1d2bfedf205eab7a9d399ef22ee14117f53 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 19 May 2010 02:32:01 +0000 Subject: [PATCH] add RTx::Calendar 0.07 --- rt/etc/RT_Config.pm | 4 +- rt/etc/RT_Config.pm.in | 4 +- rt/lib/RTx/Calendar.pm | 230 +++++++++++++++++++++ .../Callbacks/RTx-Calendar/Elements/Header/Head | 2 + .../RTx-Calendar/Ticket/Elements/Tabs/Default | 19 ++ .../RTx-Calendar/User/Elements/Tabs/Default | 9 + rt/share/html/Elements/CalendarEvent | 129 ++++++++++++ rt/share/html/Elements/MyCalendar | 78 +++++++ rt/share/html/NoAuth/Calendar/dhandler | 159 ++++++++++++++ rt/share/html/NoAuth/css/calendar.css | 40 ++++ rt/share/html/NoAuth/images/created.png | Bin 0 -> 994 bytes rt/share/html/NoAuth/images/created_due.png | Bin 0 -> 997 bytes rt/share/html/NoAuth/images/due.png | Bin 0 -> 936 bytes rt/share/html/NoAuth/images/reminder.png | Bin 0 -> 921 bytes rt/share/html/NoAuth/images/resolved.png | Bin 0 -> 229 bytes rt/share/html/NoAuth/images/started.png | Bin 0 -> 934 bytes rt/share/html/NoAuth/images/starts.png | Bin 0 -> 935 bytes rt/share/html/NoAuth/images/starts_due.png | Bin 0 -> 173 bytes rt/share/html/NoAuth/images/updated.png | Bin 0 -> 191 bytes rt/share/html/Prefs/Calendar.html | 123 +++++++++++ rt/share/html/Prefs/Elements/CalendarFeed | 68 ++++++ rt/share/html/Search/Calendar.html | 185 +++++++++++++++++ 22 files changed, 1046 insertions(+), 4 deletions(-) create mode 100644 rt/lib/RTx/Calendar.pm create mode 100644 rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head create mode 100644 rt/share/html/Callbacks/RTx-Calendar/Ticket/Elements/Tabs/Default create mode 100644 rt/share/html/Callbacks/RTx-Calendar/User/Elements/Tabs/Default create mode 100644 rt/share/html/Elements/CalendarEvent create mode 100644 rt/share/html/Elements/MyCalendar create mode 100644 rt/share/html/NoAuth/Calendar/dhandler create mode 100644 rt/share/html/NoAuth/css/calendar.css create mode 100644 rt/share/html/NoAuth/images/created.png create mode 100644 rt/share/html/NoAuth/images/created_due.png create mode 100644 rt/share/html/NoAuth/images/due.png create mode 100644 rt/share/html/NoAuth/images/reminder.png create mode 100644 rt/share/html/NoAuth/images/resolved.png create mode 100644 rt/share/html/NoAuth/images/started.png create mode 100644 rt/share/html/NoAuth/images/starts.png create mode 100644 rt/share/html/NoAuth/images/starts_due.png create mode 100644 rt/share/html/NoAuth/images/updated.png create mode 100644 rt/share/html/Prefs/Calendar.html create mode 100644 rt/share/html/Prefs/Elements/CalendarFeed create mode 100644 rt/share/html/Search/Calendar.html diff --git a/rt/etc/RT_Config.pm b/rt/etc/RT_Config.pm index b36a60759..a976fb366 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 =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 03ee5d9f7..564682b62 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 =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 000000000..515bd4810 --- /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 Enchuche@barna.beE + +Idea borrowed from redmine's calendar (Thanks Jean-Philippe). + +=head1 COPYRIGHT + +Copyright 2007 by Nicolas Chuche Enchuche@barna.beE + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See L + +=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 000000000..c1f24c2b4 --- /dev/null +++ b/rt/share/html/Callbacks/RTx-Calendar/Elements/Header/Head @@ -0,0 +1,2 @@ + + 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 000000000..cb46fdaf6 --- /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" }; + +<%args> +$tabs + 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 000000000..06413e278 --- /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" }; + +<%args> +$tabs +$current_subtab => undef +$Searches => undef + diff --git a/rt/share/html/Elements/CalendarEvent b/rt/share/html/Elements/CalendarEvent new file mode 100644 index 000000000..3a6b00bb8 --- /dev/null +++ b/rt/share/html/Elements/CalendarEvent @@ -0,0 +1,129 @@ +<%args> +$Date => undef +$Object => undef +$DateTypes => undef + +
+ + +% if ($IsReminder and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today) { + + +% } elsif ($DateTypes->{Resolved} +% and RTx::Calendar::LocalDate($Object->ResolvedObj->Unix) eq $today) { + + +% } elsif ($DateTypes->{Starts} and $DateTypes->{Due} +% and RTx::Calendar::LocalDate($Object->StartsObj->Unix) eq $today and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today ) { + + +% } elsif ($DateTypes->{Due} and $DateTypes->{Created} +% and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today and RTx::Calendar::LocalDate($Object->CreatedObj->Unix) eq $today ) { + + +% } elsif ($DateTypes->{Starts} +% and RTx::Calendar::LocalDate($Object->StartsObj->Unix) eq $today) { + + +% } elsif ($DateTypes->{Due} +% and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today) { + + +% } elsif ($DateTypes->{Created} +% and RTx::Calendar::LocalDate($Object->CreatedObj->Unix) eq $today) { + + +% } elsif ($DateTypes->{Started} +% and RTx::Calendar::LocalDate($Object->StartedObj->Unix) eq $today) { + + +% } elsif ($DateTypes->{LastUpdated} +% and RTx::Calendar::LocalDate($Object->LastUpdatedObj->Unix) eq $today) { + + +% } + + + <% $Object->QueueObj->Name %> #<% $TicketId %> + <% $display_owner ? 'by ' . $Object->OwnerObj->Name : '' %> + <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %>
+ + + <% $Object->QueueObj->Name %> #<% $TicketId %> + + : <% $subject%>
+
+ +%# 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 "Check your CalendarPopupFields config in etc/RT_SiteConfig.pm.

Failed to find \"$attr\" - ". $@}; +% } + <&|/l&><% $label_of{$attr} %>: <% $value %>
+% } + +
+
+
+ +<%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; +} + + diff --git a/rt/share/html/Elements/MyCalendar b/rt/share/html/Elements/MyCalendar new file mode 100644 index 000000000..a54ab39d6 --- /dev/null +++ b/rt/share/html/Elements/MyCalendar @@ -0,0 +1,78 @@ +<&|/Widgets/TitleBox, + title => loc("Calendar"), + title_href => "Search/Calendar.html" &> + + + + +% my $date = $begin->clone; +% while ( $date <= $end ) { + +% $date = $set->next($date); +% } + + + + +% $date = $begin->clone; +% while ($date <= $end) { + +% $date = $set->next($date); +% } + + +
<%$rtdate->GetWeekday($date->day_of_week % 7)%>
+

<%$date->day%>

+% for my $t (@{ $Tickets{$date->strftime("%F")} }) { +<& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &> +% } +
+ + + +<%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); + + diff --git a/rt/share/html/NoAuth/Calendar/dhandler b/rt/share/html/NoAuth/Calendar/dhandler new file mode 100644 index 000000000..4b4aa631e --- /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; +} + + + + diff --git a/rt/share/html/NoAuth/css/calendar.css b/rt/share/html/NoAuth/css/calendar.css new file mode 100644 index 000000000..e313dff99 --- /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 0000000000000000000000000000000000000000..4d5eeb9ea6903e0cbc23134adb69a62623007a1e GIT binary patch literal 994 zcmeAS@N?(olHy`uVBq!ia0vp^AT~P(GmvB}(g_4ox+Sg=CC){ui6xo&c?uz!xv31f ziA8z}<_dZFWqJxm1_ovdhL%=_23AG}3fcyS1_rZ|iY9_gDe?4mWxvMFE-I&*E_2Ni zD8yOd5m^kh;{ymYI;}C80Tg6Ocl32+VA$Bt{U?!?fq|JJz$e6&V=Kr1Q7{?;BQ*s2 zBU@sCd4s7W$S?Rm!_(~sUO=9lr;B4q#jWIo1QtetlMJaw3Wkyr2RYa#xL&-+XY0De?4mWxvMFE-EMKQM_s& zP>8d@BeEE1#|IE*bXsFF11QLn?&#~tz_78O`%fY(0|PTdfKQ0)e+GvCqhK@yMrsHw zZb`ce%o|K4L4Lvi8J=!8@B;D_JzX3_DsCkwB&4LUB)YM&8BAvEJjtLaaeyIKBvkhX Z3j-S)gL;U!lvNA9*GX(gA zxc&zM1_ls03PwX<V@(X5gcy=QV$l)yTh%5$XpbsF-=(NUU22hZ_ z#M9T6{W>d`xSHvUZPFKkLUNuijv*Y^lM@)2+$;_rIMHz=g~d>EAw#H0tS(Rsi!*~v T)7DjjK$Q%hu6{1-oD!MVEP;26XNH=O z_UkNs{FZW>=?fHqLZY57jv*Y^lM@)2+}zkC6i&D>NpLYR88Yyi_&r|+RKVcr>gTe~ HDWM4f1g9EC literal 0 HcmV?d00001 diff --git a/rt/share/html/NoAuth/images/resolved.png b/rt/share/html/NoAuth/images/resolved.png new file mode 100644 index 0000000000000000000000000000000000000000..09db36d5a4230d90674b24974b319d2cc8db8d94 GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)M!3HGx`C7_=6id3JuOkD)#(wTUiL5|AXMsm# zF#`kF2M}g-T4OQ;D9B#o>FdgVos~nn=yIS?iKmNW2*-8ZV~RqKff5ZDi|_Y* zWOut((woHlUeq{DVH4|OjxvYqIS;(1Da;VeIdsCD>s|P{=}O1N)N=}sO2?lGdg8Go zIW9ISaMvdf0ZX-t#D?UTr}(7)yS=zBA8tBvaXYu2MY61Al*h!!4YpxB9(F1&v;U?e VVRq>9i2|Vg44$rjF6*2Ung9y`ONsyh literal 0 HcmV?d00001 diff --git a/rt/share/html/NoAuth/images/started.png b/rt/share/html/NoAuth/images/started.png new file mode 100644 index 0000000000000000000000000000000000000000..e177addea3772d9c395cc98f9574280e3a2f7efa GIT binary patch literal 934 zcmeAS@N?(olHy`uVBq!ia0vp^AT~P(GmvB}(g_4oEa{HEjtmSN`?>!lvNA9*GX(gA zxc>kDpJOWrkQ@c0Aus|%VAjh2Z-IG(u_VYZn8D%MjWi&Kv%n*=7?^=RfH0%e8j~47 zLG}_)Usv|)tX$%<3OjEKFad>RJY5_^IIbrrFfhBZStO*mBpEqr8aRl$hI1cbVAf?2 UE#%le8K{oI)78&qol`;+0Q$Hf(EtDd literal 0 HcmV?d00001 diff --git a/rt/share/html/NoAuth/images/starts.png b/rt/share/html/NoAuth/images/starts.png new file mode 100644 index 0000000000000000000000000000000000000000..88064ba50bcfccd7909fe6ef51a2cf890fe2b555 GIT binary patch literal 935 zcmeAS@N?(olHy`uVBq!ia0vp^AT~P(GmvB}(g_4oEa{HEjtmSN`?>!lvNA9*GX(gA zxc&zM1_ls03PwX<V@(X5gcy=QV$l)yTh%5$XpbsF-=(NUU22hZ_ z#M9T6{W>d`xVqrkb?1N?i9y!W#W95AdU8Sv1Cs%xn&d+tH;c)FTmh~VuR3uuu#_># U+zy$c4phkC>FVdQ&MBb@091Y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..16a4de46f2e5d79c4c8bb7848d966cf95299fb1d GIT binary patch literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^{6Ngk!3HF+w~DI+DVB6cUq=Rpjs4tz5?O(K&H|6f zVg?4T4!3HGPo$&JjQY`6?zK#qG8~eHcB(eheoCO|{ z#S9EwA3&JVX^qJYpdfpRr>`sfbyhBM9`%xxWgCG)ex5FlAsp9rPj2LG3Xotsa5>@6 zwoQwwrWP`^6-YWiXA1eFnDfA6{p7an_m8HWV%E;9_gU;G^zP` jZ29(o{;7xNochb~$3v*kODUckXflJRtDnm{r-UW|c(Fiq literal 0 HcmV?d00001 diff --git a/rt/share/html/Prefs/Calendar.html b/rt/share/html/Prefs/Calendar.html new file mode 100644 index 000000000..5fbdd2717 --- /dev/null +++ b/rt/share/html/Prefs/Calendar.html @@ -0,0 +1,123 @@ +<%args> +$ChangeURL => undef +$ResetURL => undef +$SearchType => 'Ticket' +$HiddenField => undef + + +<& /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' &> + +

displaying reminders :

+

If you want to have reminders in a search you need to go in the <%loc("Edit Query")%> tab +of the <%loc("query builder")%> and add something like that : + +

+   AND ( Type = 'ticket' OR Type = 'reminder' )
+
+

+ +

displaying other kind of dates :

+

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 <%loc("Query +Builder")%>. The following one will display the two latter and +LastUpdated dates : + +

+  '<small>__Due__</small>',
+  '<small>__Starts__</small>',
+  '<small>__LastUpdated__</small>'
+
+

+ +

changing the default query :

+

You can change the default Query of Calendar.html and MyCalendar +portlet by saving a query with the name calendar in the +<%loc("Query +Builder")%>.

+ + + +<& /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 the Query Builder + + + +% } +% } 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'); +} + + + diff --git a/rt/share/html/Prefs/Elements/CalendarFeed b/rt/share/html/Prefs/Elements/CalendarFeed new file mode 100644 index 000000000..46893435e --- /dev/null +++ b/rt/share/html/Prefs/Elements/CalendarFeed @@ -0,0 +1,68 @@ +<%args> +$Search => undef +$Object => undef +$HiddenField => undef + + +<&| /Widgets/TitleBox, title => $title &> + +% if ($FeedText) { +

<%$FeedText%>

+% } else { +This feed will show tickets with due date find with query:
+"<%$Search->SubValue('Query')%>". +% } + +% if ($ICalURL) { +

Your can paste this url in your calendar : <%$link%>

+ + + + + +
+
+ + +
+
+
+ + +
+
+% } else { + +

+ + +
+% } + + + +<%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; + + \ 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 000000000..e711b861c --- /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 + + +<& /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" &> + + + + + + + +
+% my ($PMonth, $PYear) = ($Month - 1, $Year); +% if ($PMonth < 0) { +% $PYear--; +% $PMonth = 11; +% } +«<%$rtdate->GetMonth($PMonth)%> + +Calendar Preferences and Help + +% my ($NMonth, $NYear) = ($Month + 1, $Year); +% if ($NMonth > 11) { +% $NYear++; +% $NMonth = 0; +% } +<%$rtdate->GetMonth($NMonth)%>» +
+ + + + + +% for (1 .. 6, 0) { + +% } + + + + +% while ($date <= $end) { +% if ( $date->day_of_week == 1) { + +% } + +% $date = $set->next($date); +% if ( $date->day_of_week == 1) { + +% } +% } + + +
<%$rtdate->GetWeekday($_)%>
<% $date->week_number %> +

<%$date->day%>

+% for my $t ( @{ $Tickets{$date->strftime("%F")} } ) { +<& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &> +% } +
+ + + + + + +
+«<%$rtdate->GetMonth($PMonth)%> + +<%$rtdate->GetMonth($NMonth)%>» +
+ + + + + + +
+
+ + +% my $year = (localtime)[5] + 1900; + + +<& /Elements/Submit&> +
+
+ : <&|/l&>Created
+ : <&|/l&>Due
+ : <&|/l&>Resolved
+ : <&|/l&>Last Updated
+ : <&|/l&>Created, <&|/l&>Due
+ : <&|/l&>Reminders
+ : <&|/l&>Starts
+ : <&|/l&>Started
+ : <&|/l&>Starts, <&|/l&>Due
+ + +
+ + + + +<%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")); + + -- 2.11.0