X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FTickets_Overlay.pm;h=5a7e020566adfa7f0a3b94e272f155e448e9cd65;hb=09aa4ca78df448f73e440a25a024e44eaf00c827;hp=b8f9756e8d5dc657020327de5753d99dab8b4a22;hpb=b4b0c7e72d7eaee2fbfc7022022c9698323203dd;p=freeside.git diff --git a/rt/lib/RT/Tickets_Overlay.pm b/rt/lib/RT/Tickets_Overlay.pm index b8f9756e8..5a7e02056 100644 --- a/rt/lib/RT/Tickets_Overlay.pm +++ b/rt/lib/RT/Tickets_Overlay.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# +# # (Except where explicitly superseded by other copyright notices) -# -# +# +# # LICENSE: -# +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# +# +# # CONTRIBUTION SUBMISSION POLICY: -# +# # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) -# +# # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that @@ -43,7 +43,7 @@ # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. -# +# # END BPS TAGGED BLOCK }}} # Major Changes: @@ -143,6 +143,11 @@ our %FIELD_METADATA = ( CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair + HasAttribute => [ 'HASATTRIBUTE', 1 ], + HasNoAttribute => [ 'HASATTRIBUTE', 0 ], + Agentnum => [ 'FREESIDEFIELD', ], + Classnum => [ 'FREESIDEFIELD', ], + Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ], ); # Mapping of Field Type to Function @@ -158,6 +163,8 @@ our %dispatch = ( WATCHERFIELD => \&_WatcherLimit, MEMBERSHIPFIELD => \&_WatcherMembershipLimit, CUSTOMFIELD => \&_CustomFieldLimit, + HASATTRIBUTE => \&_HasAttributeLimit, + FREESIDEFIELD => \&_FreesideFieldLimit, ); our %can_bundle = ();# WATCHERFIELD => "yes", ); @@ -195,6 +202,11 @@ my %DefaultEA = ( 'NOT LIKE' => 'AND' }, + HASATTRIBUTE => { + '=' => 'AND', + '!=' => 'AND', + }, + CUSTOMFIELD => 'OR', ); @@ -511,6 +523,14 @@ sub _DateLimit { die "Incorrect Meta Data for $field" unless ( defined $meta->[1] ); + $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest ); +} + +# Factor this out for use by custom fields + +sub _DateFieldLimit { + my ( $sb, $field, $op, $value, @rest ) = @_; + my $date = RT::Date->new( $sb->CurrentUser ); $date->Set( Format => 'unknown', Value => $value ); @@ -519,23 +539,44 @@ sub _DateLimit { # if we're specifying =, that means we want everything on a # particular single day. in the database, we need to check for > # and < the edges of that day. - - $date->SetToMidnight( Timezone => 'server' ); - my $daystart = $date->ISO; - $date->AddDay; - my $dayend = $date->ISO; + # + # Except if the value is 'this month' or 'last month', check + # > and < the edges of the month. + + my ($daystart, $dayend); + if ( lc($value) eq 'this month' ) { + $date->SetToNow; + $date->SetToStart('month', Timezone => 'server'); + $daystart = $date->ISO; + $date->AddMonth; + $dayend = $date->ISO; + } + elsif ( lc($value) eq 'last month' ) { + $date->SetToNow; + $date->SetToStart('month', Timezone => 'server'); + $dayend = $date->ISO; + $date->AddDays(-1); + $date->SetToStart('month', Timezone => 'server'); + $daystart = $date->ISO; + } + else { + $date->SetToMidnight( Timezone => 'server' ); + $daystart = $date->ISO; + $date->AddDay; + $dayend = $date->ISO; + } $sb->_OpenParen; $sb->_SQLLimit( - FIELD => $meta->[1], + FIELD => $field, OPERATOR => ">=", VALUE => $daystart, @rest, ); $sb->_SQLLimit( - FIELD => $meta->[1], + FIELD => $field, OPERATOR => "<", VALUE => $dayend, @rest, @@ -547,7 +588,7 @@ sub _DateLimit { } else { $sb->_SQLLimit( - FIELD => $meta->[1], + FIELD => $field, OPERATOR => $op, VALUE => $date->ISO, @rest, @@ -715,7 +756,7 @@ sub _TransLimit { # them all into the same subclause when you have (A op B op C) - the # way they get parsed in the tree they're in different subclauses. - my ( $self, $field, $op, $value, @rest ) = @_; + my ( $self, $field, $op, $value, %rest ) = @_; unless ( $self->{_sql_transalias} ) { $self->{_sql_transalias} = $self->Join( @@ -741,41 +782,36 @@ sub _TransLimit { ); } - $self->_OpenParen; - #Search for the right field if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) { - $self->_SQLLimit( - ALIAS => $self->{_sql_trattachalias}, - FIELD => 'Filename', - OPERATOR => 'IS', - VALUE => 'NULL', - SUBCLAUSE => 'contentquery', - ENTRYAGGREGATOR => 'AND', - ); - $self->_SQLLimit( + $self->_OpenParen; + $self->_SQLLimit( + %rest, ALIAS => $self->{_sql_trattachalias}, FIELD => $field, OPERATOR => $op, VALUE => $value, CASESENSITIVE => 0, - @rest, + ); + $self->_SQLLimit( ENTRYAGGREGATOR => 'AND', - SUBCLAUSE => 'contentquery', + ALIAS => $self->{_sql_trattachalias}, + FIELD => 'Filename', + OPERATOR => 'IS', + VALUE => 'NULL', ); + $self->_CloseParen; } else { $self->_SQLLimit( + %rest, ALIAS => $self->{_sql_trattachalias}, FIELD => $field, OPERATOR => $op, VALUE => $value, CASESENSITIVE => 0, - ENTRYAGGREGATOR => 'AND', - @rest ); } - $self->_CloseParen; } @@ -1362,7 +1398,8 @@ sub _CustomFieldLimit { # we explicitly don't include the "IS NULL" case, since we would # otherwise end up with a redundant clause. - my ($negative_op, $null_op, $inv_op, $range_op) = $self->ClassifySQLOperation( $op ); + my ($negative_op, $null_op, $inv_op, $range_op) + = $self->ClassifySQLOperation( $op ); my $fix_op = sub { my $op = shift; @@ -1419,6 +1456,50 @@ sub _CustomFieldLimit { %rest ); } + elsif ( $cf->Type eq 'Date' ) { + $self->_DateFieldLimit( + 'Content', + $op, + $value, + ALIAS => $TicketCFs, + %rest + ); + } + elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) { + unless ( length( Encode::encode_utf8($value) ) > 255 ) { + $self->_SQLLimit( + ALIAS => $TicketCFs, + FIELD => 'Content', + OPERATOR => $op, + VALUE => $value, + %rest + ); + } else { + $self->_OpenParen; + $self->_SQLLimit( + ALIAS => $TicketCFs, + FIELD => 'Content', + OPERATOR => '=', + VALUE => '', + ENTRYAGGREGATOR => 'OR' + ); + $self->_SQLLimit( + ALIAS => $TicketCFs, + FIELD => 'Content', + OPERATOR => 'IS', + VALUE => 'NULL', + ENTRYAGGREGATOR => 'OR' + ); + $self->_CloseParen; + $self->_SQLLimit( + ALIAS => $TicketCFs, + FIELD => 'LargeContent', + OPERATOR => $fix_op->($op), + VALUE => $value, + ENTRYAGGREGATOR => 'AND', + ); + } + } else { $self->_SQLLimit( ALIAS => $TicketCFs, @@ -1528,6 +1609,39 @@ sub _CustomFieldLimit { } } +sub _HasAttributeLimit { + my ( $self, $field, $op, $value, %rest ) = @_; + + my $alias = $self->Join( + TYPE => 'LEFT', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'Attributes', + FIELD2 => 'ObjectId', + ); + $self->SUPER::Limit( + LEFTJOIN => $alias, + FIELD => 'ObjectType', + VALUE => 'RT::Ticket', + ENTRYAGGREGATOR => 'AND' + ); + $self->SUPER::Limit( + LEFTJOIN => $alias, + FIELD => 'Name', + OPERATOR => $op, + VALUE => $value, + ENTRYAGGREGATOR => 'AND' + ); + $self->_SQLLimit( + %rest, + ALIAS => $alias, + FIELD => 'id', + OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS', + VALUE => 'NULL', + QUOTEVALUE => 0, + ); +} + # End Helper Functions # End of SQL Stuff ------------------------------------------------- @@ -1660,7 +1774,38 @@ sub OrderByCols { } push @res, { %$row, FIELD => "Priority", ORDER => $order } ; - } + + } elsif ( $field eq 'Customer' ) { #Freeside + if ( $subkey eq 'Number' ) { + my ($linkalias, $custnum_sql) = $self->JoinToCustLinks; + push @res, { %$row, + ALIAS => '', + FIELD => $custnum_sql, + }; + } + else { + my $custalias = $self->JoinToCustomer; + my $field; + if ( $subkey eq 'Name' ) { + $field = "COALESCE( $custalias.company, + $custalias.last || ', ' || $custalias.first + )"; + } + elsif ( $subkey eq 'Class' ) { + $field = "$custalias.classnum"; + } + elsif ( $subkey eq 'Agent' ) { + $field = "$custalias.agentnum"; + } + else { + # no other cases exist yet, but for obviousness: + $field = $subkey; + } + push @res, { %$row, ALIAS => '', FIELD => $field }; + } + + } #Freeside + else { push @res, $row; } @@ -1668,6 +1813,100 @@ sub OrderByCols { return $self->SUPER::OrderByCols(@res); } +#Freeside + +sub JoinToCustLinks { + # Set up join to links (id = localbase), + # limit link type to 'MemberOf', + # and target value to any Freeside custnum URI. + # Return the linkalias for further join/limit action, + # and an sql expression to retrieve the custnum. + my $self = shift; + my $linkalias = $self->Join( + TYPE => 'LEFT', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'Links', + FIELD2 => 'LocalBase', + ); + + $self->SUPER::Limit( + LEFTJOIN => $linkalias, + FIELD => 'Type', + OPERATOR => '=', + VALUE => 'MemberOf', + ); + $self->SUPER::Limit( + LEFTJOIN => $linkalias, + FIELD => 'Target', + OPERATOR => 'STARTSWITH', + VALUE => 'freeside://freeside/cust_main/', + ); + my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS "; + if ( RT->Config->Get('DatabaseType') eq 'mysql' ) { + $custnum_sql .= 'SIGNED INTEGER)'; + } + else { + $custnum_sql .= 'INTEGER)'; + } + return ($linkalias, $custnum_sql); +} + +sub JoinToCustomer { + my $self = shift; + my ($linkalias, $custnum_sql) = $self->JoinToCustLinks; + + my $custalias = $self->Join( + TYPE => 'LEFT', + EXPRESSION => $custnum_sql, + TABLE2 => 'cust_main', + FIELD2 => 'custnum', + ); + return $custalias; +} + +sub _FreesideFieldLimit { + my ( $self, $field, $op, $value, %rest ) = @_; + my $alias = $self->JoinToCustomer; + my $is_negative = 0; + if ( $op eq '!=' || $op =~ /\bNOT\b/i ) { + # if the op is negative, do the join as though + # the op were positive, then accept only records + # where the right-side join key is null. + $is_negative = 1; + $op = '=' if $op eq '!='; + $op =~ s/\bNOT\b//; + } + my $meta = $FIELD_METADATA{$field}; + if ( $meta->[1] ) { + $alias = $self->Join( + TYPE => 'LEFT', + ALIAS1 => $alias, + FIELD1 => 'custnum', + TABLE2 => $meta->[1], + FIELD2 => 'custnum', + ); + } + + $self->SUPER::Limit( + LEFTJOIN => $alias, + FIELD => lc($field), + OPERATOR => $op, + VALUE => $value, + ENTRYAGGREGATOR => 'AND', + ); + $self->_SQLLimit( + %rest, + ALIAS => $alias, + FIELD => lc($field), + OPERATOR => $is_negative ? 'IS' : 'IS NOT', + VALUE => 'NULL', + QUOTEVALUE => 0, + ); +} + +#Freeside + # }}} # {{{ Limit the result set based on content @@ -2705,18 +2944,40 @@ Returns a reference to the set of all items found in this search sub ItemsArrayRef { my $self = shift; - unless ( $self->{'items_array'} ) { + return $self->{'items_array'} if $self->{'items_array'}; - my $placeholder = $self->_ItemsCounter; - $self->GotoFirstItem(); - while ( my $item = $self->Next ) { - push( @{ $self->{'items_array'} }, $item ); - } - $self->GotoItem($placeholder); - $self->{'items_array'} - = $self->ItemsOrderBy( $self->{'items_array'} ); + my $placeholder = $self->_ItemsCounter; + $self->GotoFirstItem(); + while ( my $item = $self->Next ) { + push( @{ $self->{'items_array'} }, $item ); + } + $self->GotoItem($placeholder); + $self->{'items_array'} + = $self->ItemsOrderBy( $self->{'items_array'} ); + + return $self->{'items_array'}; +} + +sub ItemsArrayRefWindow { + my $self = shift; + my $window = shift; + + my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1); + + $self->RowsPerPage( $window ); + $self->FirstRow(1); + $self->GotoFirstItem; + + my @res; + while ( my $item = $self->Next ) { + push @res, $item; } - return ( $self->{'items_array'} ); + + $self->RowsPerPage( $old[1] ); + $self->FirstRow( $old[2] ); + $self->GotoItem( $old[0] ); + + return \@res; } # }}} @@ -2913,6 +3174,17 @@ sub CurrentUserCanSee { } } + unless ( @direct_queues || keys %roles ) { + $self->SUPER::Limit( + SUBCLAUSE => 'ACL', + ALIAS => 'main', + FIELD => 'id', + VALUE => 0, + ENTRYAGGREGATOR => 'AND', + ); + return $self->{'_sql_current_user_can_see_applied'} = 1; + } + { my $join_roles = keys %roles; $join_roles = 0 if $join_roles == 1 && $roles{'Owner'}; @@ -2933,16 +3205,18 @@ sub CurrentUserCanSee { return unless @queues; if ( @queues == 1 ) { - $self->_SQLLimit( + $self->SUPER::Limit( + SUBCLAUSE => 'ACL', ALIAS => 'main', FIELD => 'Queue', VALUE => $_[0], ENTRYAGGREGATOR => $ea, ); } else { - $self->_OpenParen; + $self->SUPER::_OpenParen('ACL'); foreach my $q ( @queues ) { - $self->_SQLLimit( + $self->SUPER::Limit( + SUBCLAUSE => 'ACL', ALIAS => 'main', FIELD => 'Queue', VALUE => $q, @@ -2950,25 +3224,27 @@ sub CurrentUserCanSee { ); $ea = 'OR'; } - $self->_CloseParen; + $self->SUPER::_CloseParen('ACL'); } return 1; }; - $self->_OpenParen; + $self->SUPER::_OpenParen('ACL'); my $ea = 'AND'; $ea = 'OR' if $limit_queues->( $ea, @direct_queues ); while ( my ($role, $queues) = each %roles ) { - $self->_OpenParen; + $self->SUPER::_OpenParen('ACL'); if ( $role eq 'Owner' ) { - $self->_SQLLimit( + $self->SUPER::Limit( + SUBCLAUSE => 'ACL', FIELD => 'Owner', VALUE => $id, ENTRYAGGREGATOR => $ea, ); } else { - $self->_SQLLimit( + $self->SUPER::Limit( + SUBCLAUSE => 'ACL', ALIAS => $cgm_alias, FIELD => 'MemberId', OPERATOR => 'IS NOT', @@ -2976,7 +3252,8 @@ sub CurrentUserCanSee { QUOTEVALUE => 0, ENTRYAGGREGATOR => $ea, ); - $self->_SQLLimit( + $self->SUPER::Limit( + SUBCLAUSE => 'ACL', ALIAS => $role_group_alias, FIELD => 'Type', VALUE => $role, @@ -2985,9 +3262,9 @@ sub CurrentUserCanSee { } $limit_queues->( 'AND', @$queues ) if ref $queues; $ea = 'OR' if $ea eq 'AND'; - $self->_CloseParen; + $self->SUPER::_CloseParen('ACL'); } - $self->_CloseParen; + $self->SUPER::_CloseParen('ACL'); } return $self->{'_sql_current_user_can_see_applied'} = 1; } @@ -3230,47 +3507,61 @@ sub _ProcessRestrictions { =head2 _BuildItemMap - # Build up a map of first/last/next/prev items, so that we can display search nav quickly +Build up a L of first/last/next/prev items, so that we can +display search nav quickly. =cut sub _BuildItemMap { my $self = shift; - my $items = $self->ItemsArrayRef; - my $prev = 0; + my $window = RT->Config->Get('TicketsItemMapSize'); - delete $self->{'item_map'}; - if ( $items->[0] ) { - $self->{'item_map'}->{'first'} = $items->[0]->EffectiveId; - while ( my $item = shift @$items ) { - my $id = $item->EffectiveId; - $self->{'item_map'}->{$id}->{'defined'} = 1; - $self->{'item_map'}->{$id}->{prev} = $prev; - $self->{'item_map'}->{$id}->{next} = $items->[0]->EffectiveId - if ( $items->[0] ); - $prev = $id; - } - $self->{'item_map'}->{'last'} = $prev; + $self->{'item_map'} = {}; + + my $items = $self->ItemsArrayRefWindow( $window ); + return unless $items && @$items; + + my $prev = 0; + $self->{'item_map'}{'first'} = $items->[0]->EffectiveId; + for ( my $i = 0; $i < @$items; $i++ ) { + my $item = $items->[$i]; + my $id = $item->EffectiveId; + $self->{'item_map'}{$id}{'defined'} = 1; + $self->{'item_map'}{$id}{'prev'} = $prev; + $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId + if $items->[$i+1]; + $prev = $id; } + $self->{'item_map'}{'last'} = $prev + if !$window || @$items < $window; } =head2 ItemMap -Returns an a map of all items found by this search. The map is of the form +Returns an a map of all items found by this search. The map is a hash +of the form: -$ItemMap->{'first'} = first ticketid found -$ItemMap->{'last'} = last ticketid found -$ItemMap->{$id}->{prev} = the ticket id found before $id -$ItemMap->{$id}->{next} = the ticket id found after $id + { + first => , + last => , + + => { + prev => , + next => , + }, + => { + prev => ..., + next => ..., + }, + } =cut sub ItemMap { my $self = shift; - $self->_BuildItemMap() - unless ( $self->{'items_array'} and $self->{'item_map'} ); - return ( $self->{'item_map'} ); + $self->_BuildItemMap unless $self->{'item_map'}; + return $self->{'item_map'}; } @@ -3280,13 +3571,16 @@ sub ItemMap { =head2 PrepForSerialization -You don't want to serialize a big tickets object, as the {items} hash will be instantly invalid _and_ eat lots of space +You don't want to serialize a big tickets object, as +the {items} hash will be instantly invalid _and_ eat +lots of space =cut sub PrepForSerialization { my $self = shift; delete $self->{'items'}; + delete $self->{'items_array'}; $self->RedoSearch(); }