From dd82a27357402466390044d001824657f6617626 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 25 May 2016 16:29:05 -0700 Subject: [PATCH] indicator on the top bar for new activity on tickets, #41670 --- httemplate/elements/header-full.html | 3 + httemplate/elements/notify-tickets.html | 37 +++++++ rt/lib/RT/Search/UnrepliedTickets.pm | 62 ++++++++++++ rt/share/html/Search/UnrepliedTickets.html | 156 +++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 httemplate/elements/notify-tickets.html create mode 100644 rt/lib/RT/Search/UnrepliedTickets.pm create mode 100755 rt/share/html/Search/UnrepliedTickets.html diff --git a/httemplate/elements/header-full.html b/httemplate/elements/header-full.html index 699f82c53..db38eafba 100644 --- a/httemplate/elements/header-full.html +++ b/httemplate/elements/header-full.html @@ -67,6 +67,9 @@ Example: <% $company_name || 'ExampleCo' %> + + <& notify-tickets.html &> + Logged in as <% $FS::CurrentUser::CurrentUser->username |h %>  logout
Preferences % if ( $conf->config("ticket_system") % && FS::TicketSystem->access_right(\%session, 'ModifySelf') ) { diff --git a/httemplate/elements/notify-tickets.html b/httemplate/elements/notify-tickets.html new file mode 100644 index 000000000..f7db52e7b --- /dev/null +++ b/httemplate/elements/notify-tickets.html @@ -0,0 +1,37 @@ +% if ($enabled) { + +
+% if ( $UnrepliedTickets->Count > 0 ) { + +
+ <% emt('New activity on [quant,_1,ticket]', $UnrepliedTickets->Count) %> +
+% } else { + <% emt('No new activity on tickets') %> +% } +
+% } +<%init> +use Class::Load 'load_class'; + +my $enabled = $FS::TicketSystem::system eq 'RT_Internal'; +my $UnrepliedTickets; +if ($enabled) { + my $class = 'RT::Search::UnrepliedTickets'; + load_class($class); + my $session = FS::TicketSystem->session; + my $CurrentUser = $session->{CurrentUser}; + $UnrepliedTickets = RT::Tickets->new($CurrentUser); + my $search = $class->new(TicketsObj => $UnrepliedTickets); +warn Dumper $search; + $search->Prepare; +} + diff --git a/rt/lib/RT/Search/UnrepliedTickets.pm b/rt/lib/RT/Search/UnrepliedTickets.pm new file mode 100644 index 000000000..a99690156 --- /dev/null +++ b/rt/lib/RT/Search/UnrepliedTickets.pm @@ -0,0 +1,62 @@ +=head1 NAME + + RT::Search::UnrepliedTickets + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +Find all unresolved tickets owned by the current user where the last correspondence +from a requestor (or ticket creation) is more recent than the last +correspondence from a non-requestor (if there is any). + +=head1 METHODS + +=cut + +package RT::Search::UnrepliedTickets; + +use strict; +use warnings; +use base qw(RT::Search); + + +sub Describe { + my $self = shift; + return ($self->loc("Tickets awaiting a reply")); +} + +sub Prepare { + my $self = shift; + + my $TicketsObj = $self->TicketsObj; + $TicketsObj->Limit( + FIELD => 'Owner', + VALUE => $TicketsObj->CurrentUser->id + ); + $TicketsObj->Limit( + FIELD => 'Status', + OPERATOR => '!=', + VALUE => 'resolved' + ); + my $txn_alias = $TicketsObj->JoinTransactions; + $TicketsObj->Limit( + ALIAS => $txn_alias, + FIELD => 'Created', + OPERATOR => '>', + VALUE => 'COALESCE(main.Told,\'1970-01-01\')', + QUOTEVALUE => 0, + ); + $TicketsObj->Limit( + ALIAS => $txn_alias, + FIELD => 'Type', + OPERATOR => 'IN', + VALUE => [ 'Correspond', 'Create' ], + ); + + return(1); +} + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/share/html/Search/UnrepliedTickets.html b/rt/share/html/Search/UnrepliedTickets.html new file mode 100755 index 000000000..37f94e0b2 --- /dev/null +++ b/rt/share/html/Search/UnrepliedTickets.html @@ -0,0 +1,156 @@ +%# false laziness with Results.html; basically this is the same thing but with +%# a hardcoded RT::Tickets object instead of a Query param + +<& /Elements/Header, Title => $title, + Refresh => $refresh, + LinkRel => \%link_rel &> + +% $m->callback( ARGSRef => \%ARGS, Format => \$Format, CallbackName => 'BeforeResults' ); + +<& /Elements/CollectionList, + Class => 'RT::Tickets', + Collection => $session{tickets}, + TotalFound => $ticketcount, + AllowSorting => 1, + OrderBy => $OrderBy, + Order => $Order, + Rows => $Rows, + Page => $Page, + Format => $Format, + BaseURL => $BaseURL, + SavedSearchId => $ARGS{'SavedSearchId'}, + SavedChartSearchId => $ARGS{'SavedChartSearchId'}, + PassArguments => [qw(Format Rows Page Order OrderBy SavedSearchId SavedChartSearchId)], +&> +% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' ); + +% my %hiddens = (Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page, SavedChartSearchId => $SavedChartSearchId ); +
+
+% foreach my $key (keys(%hiddens)) { + +% } +<& /Elements/Refresh, Name => 'TicketsRefreshInterval', Default => $session{'tickets_refresh_interval'}||RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'}) &> + +
+
+<%INIT> +$m->callback( ARGSRef => \%ARGS, CallbackName => 'Initial' ); + +# Read from user preferences +my $prefs = $session{'CurrentUser'}->UserObj->Preferences("SearchDisplay") || {}; + +# These variables are what define a search_hash; this is also +# where we give sane defaults. +$Format ||= $prefs->{'Format'} || RT->Config->Get('DefaultSearchResultFormat'); +$Order ||= $prefs->{'Order'} || RT->Config->Get('DefaultSearchResultOrder'); +$OrderBy ||= $prefs->{'OrderBy'} || RT->Config->Get('DefaultSearchResultOrderBy'); + +# In this case the search UI isn't available, so trust the defaults. + +# Some forms pass in "RowsPerPage" rather than "Rows" +# We call it RowsPerPage everywhere else. + +if ( defined $prefs->{'RowsPerPage'} ) { + $Rows = $prefs->{'RowsPerPage'}; +} else { + $Rows = 50; +} +$Page = 1 unless $Page && $Page > 0; + +use RT::Search::UnrepliedTickets; + +$session{'i'}++; +$session{'tickets'} = RT::Tickets->new($session{'CurrentUser'}) ; +my $search = RT::Search::UnrepliedTickets->new( TicketsObj => $session{'tickets'} ); +$search->Prepare; + +if ($OrderBy =~ /\|/) { + # Multiple Sorts + my @OrderBy = split /\|/,$OrderBy; + my @Order = split /\|/,$Order; + $session{'tickets'}->OrderByCols( + map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } } ( 0 + .. $#OrderBy ) );; +} else { + $session{'tickets'}->OrderBy(FIELD => $OrderBy, ORDER => $Order); +} +$session{'tickets'}->RowsPerPage( $Rows ) if $Rows; +$session{'tickets'}->GotoPage( $Page - 1 ); + +# use this to set a CSRF token applying to the search, so that the user can come +# back to this page without triggering a referrer check +$session{'CurrentSearchHash'} = { + Format => $Format, + Page => $Page, + Order => $Order, + OrderBy => $OrderBy, + RowsPerPage => $Rows +}; + + +my $ticketcount = $session{tickets}->CountAll(); +my $title = loc('New activity on [quant,_1,ticket,tickets]', $ticketcount); + +# pass this through on pagination links +my $QueryString = "?".$m->comp('/Elements/QueryString', + Format => $Format, + Rows => $Rows, + OrderBy => $OrderBy, + Order => $Order, + Page => $Page); + +if ($ARGS{'TicketsRefreshInterval'}) { + $session{'tickets_refresh_interval'} = $ARGS{'TicketsRefreshInterval'}; +} + +my $refresh = $session{'tickets_refresh_interval'} + || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} ); + +# Check $m->request_args, not $DECODED_ARGS, to avoid creating a new CSRF token on each refresh +if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) { + my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentSearchHash'} ); + $m->notes->{RefreshURL} = RT->Config->Get('WebURL') + . "Search/UnrepliedTickets.html?CSRF_Token=" + . $token; +} + +my %link_rel; +my $genpage = sub { + return $m->comp( + '/Elements/QueryString', + Format => $Format, + Rows => $Rows, + OrderBy => $OrderBy, + Order => $Order, + Page => shift(@_), + ); +}; + +if ( RT->Config->Get('SearchResultsAutoRedirect') && $ticketcount == 1 && + $session{tickets}->First ) { +# $ticketcount is not always precise unless $UseSQLForACLChecks is set to true, +# check $session{tickets}->First here is to make sure the ticket is there. + RT::Interface::Web::Redirect( RT->Config->Get('WebURL') + ."Ticket/Display.html?id=". $session{tickets}->First->id ); +} + +my $BaseURL = RT->Config->Get('WebPath')."/Search/UnrepliedTickets.html?"; +$link_rel{first} = $BaseURL . $genpage->(1) if $Page > 1; +$link_rel{prev} = $BaseURL . $genpage->($Page - 1) if $Page > 1; +$link_rel{next} = $BaseURL . $genpage->($Page + 1) if ($Page * $Rows) < $ticketcount; +$link_rel{last} = $BaseURL . $genpage->(POSIX::ceil($ticketcount/$Rows)) if $Rows and ($Page * $Rows) < $ticketcount; + +<%CLEANUP> +$session{'tickets'}->PrepForSerialization(); + +<%ARGS> +$HideResults => 0 +$Rows => undef +$Page => 1 +$OrderBy => undef +$Order => undef +$SavedSearchId => undef +$SavedChartSearchId => undef +$Format => undef + -- 2.11.0