diff options
22 files changed, 1046 insertions, 4 deletions
| 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<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 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<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 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 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 000000000..c1f24c2b4 --- /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 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" }; +</%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 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" }; +</%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 000000000..3a6b00bb8 --- /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 000000000..a54ab39d6 --- /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 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; +} + + + +</%init> 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.pngBinary files differ new file mode 100644 index 000000000..4d5eeb9ea --- /dev/null +++ b/rt/share/html/NoAuth/images/created.png diff --git a/rt/share/html/NoAuth/images/created_due.png b/rt/share/html/NoAuth/images/created_due.pngBinary files differ new file mode 100644 index 000000000..52dfc96f0 --- /dev/null +++ b/rt/share/html/NoAuth/images/created_due.png diff --git a/rt/share/html/NoAuth/images/due.png b/rt/share/html/NoAuth/images/due.pngBinary files differ new file mode 100644 index 000000000..30a3aff8a --- /dev/null +++ b/rt/share/html/NoAuth/images/due.png diff --git a/rt/share/html/NoAuth/images/reminder.png b/rt/share/html/NoAuth/images/reminder.pngBinary files differ new file mode 100644 index 000000000..4370b6902 --- /dev/null +++ b/rt/share/html/NoAuth/images/reminder.png diff --git a/rt/share/html/NoAuth/images/resolved.png b/rt/share/html/NoAuth/images/resolved.pngBinary files differ new file mode 100644 index 000000000..09db36d5a --- /dev/null +++ b/rt/share/html/NoAuth/images/resolved.png diff --git a/rt/share/html/NoAuth/images/started.png b/rt/share/html/NoAuth/images/started.pngBinary files differ new file mode 100644 index 000000000..e177addea --- /dev/null +++ b/rt/share/html/NoAuth/images/started.png diff --git a/rt/share/html/NoAuth/images/starts.png b/rt/share/html/NoAuth/images/starts.pngBinary files differ new file mode 100644 index 000000000..88064ba50 --- /dev/null +++ b/rt/share/html/NoAuth/images/starts.png diff --git a/rt/share/html/NoAuth/images/starts_due.png b/rt/share/html/NoAuth/images/starts_due.pngBinary files differ new file mode 100644 index 000000000..16a4de46f --- /dev/null +++ b/rt/share/html/NoAuth/images/starts_due.png diff --git a/rt/share/html/NoAuth/images/updated.png b/rt/share/html/NoAuth/images/updated.pngBinary files differ new file mode 100644 index 000000000..680e79a54 --- /dev/null +++ b/rt/share/html/NoAuth/images/updated.png 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 +</%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> +  '<small>__Due__</small>', +  '<small>__Starts__</small>', +  '<small>__LastUpdated__</small>' +</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 000000000..46893435e --- /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 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 +</%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> | 
