[Rt-commit] rt branch, 4.4-trunk, updated. rt-4.4.4-500-g6a0c9a23f7

Jim Brandt jbrandt at bestpractical.com
Fri May 28 16:04:47 EDT 2021


The branch, 4.4-trunk has been updated
       via  6a0c9a23f7216104249cb32492cfe8d0d4a087de (commit)
       via  e6f26b46100b539827f2c5e43ad0b427de413750 (commit)
       via  a8956daba22b7e09b8823e0479928fcdd0bfe79c (commit)
       via  b419da95308f1c09e0739b7800431b96a160d4ec (commit)
       via  771ee67205471a8d68717684e5e87a16913271c8 (commit)
       via  75d348fa6b4fe9eb0545334acdaebc079dfa71a6 (commit)
       via  e21fdf5346e44547c8100262846a81f0bbaa2608 (commit)
       via  2da669e38137c87646afd938293ea2a9e42e6ed6 (commit)
       via  0adc04189f71102052b19653f057722fead0ace2 (commit)
       via  ed6f82c59d37fe970bfb42b5005ad55d9fa331f2 (commit)
       via  7a74a0e07b81001c7710cdc2ad93264f3646d763 (commit)
       via  f790e5d499723552cb42568bed56b0c0fc8239de (commit)
       via  670d88dc7ebb8a0461c4055ebc9e20d62d04307c (commit)
       via  146ed9f4d57d28b19effb95d2c361f08c2a97382 (commit)
       via  fc72b6521bba8287546ce3fa287a6e3e5fb4745e (commit)
       via  dc1a5204c687d3d00d1f76598862311a4b49a103 (commit)
       via  74280dc783422058a3b2c11c1b464d89f9deed2e (commit)
       via  1cad1ce87520dffaab889966870ee9b6a9da523c (commit)
       via  10c640ef34bf49171934cceab4edee91072c99af (commit)
       via  528277f60977be25aa66d44a3d91bdd8619999e1 (commit)
      from  1768ea860cc0dd2e83b5bc2f92fc658870663550 (commit)

Summary of changes:
 docs/query_builder.pod                    |  49 +++++++++++
 lib/RT/Interface/Web/QueryBuilder/Tree.pm |  15 ++--
 lib/RT/SQL.pm                             |  33 +++++++-
 lib/RT/SearchBuilder.pm                   |  44 +++++++++-
 lib/RT/SearchBuilder/Role/Roles.pm        |   4 +-
 lib/RT/Tickets.pm                         | 117 +++++++++++++++++++++++---
 share/html/Search/Build.html              |   1 +
 t/api/tickets.t                           | 133 +++++++++++++++++++++++++++++-
 8 files changed, 371 insertions(+), 25 deletions(-)

- Log -----------------------------------------------------------------
commit 6a0c9a23f7216104249cb32492cfe8d0d4a087de
Merge: 1768ea860c e6f26b4610
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri May 28 15:55:46 2021 -0400

    Merge branch '4.4/columns-as-values-in-ticket-search' into 4.4-trunk

diff --cc docs/query_builder.pod
index eb13e8c915,ae0ffd82be..90f9f65822
--- a/docs/query_builder.pod
+++ b/docs/query_builder.pod
@@@ -239,10 -158,51 +239,59 @@@ you can search for 
  
  =cut
  
+ =head1 Advanced
+ 
+ In addition to the graphical query builder, RT also has an Advanced page
+ where you can write and modify queries and formats directly. For example,
+ you can type the following query directly in the Query box:
+ 
+     Status = 'resolved' AND Resolved > '2019-04-01' AND Queue = 'RT'
+ 
+ and then click "Apply" to let RT validate it. RT will display any syntax errors
+ if you make any mistakes so you can fix them.
+ 
+ =head2 Valid Search Values
+ 
+ In the above example, search values like C<'resolved'>, C<'2019-04-01'>,
+ and C<'RT'> are all literal search terms: a status, a date, and a string
+ representing the name of a queue. These are all static values that will then
+ return matching tickets. However, sometimes you want to compare 2 columns
+ in RT tables without knowing exact values. This is also supported, for
+ example:
+ 
+ =over
+ 
+ =item Search tickets where LastUpdated is after (later than) Resolved
+ 
+     LastUpdated > Resolved
+ 
+ This finds tickets that have been commented on or otherwise updated after
+ they were resolved. Note that C<Resolved> is not quoted, indicating that it's
+ the relative date value from a ticket instead of the literal string "Resolved".
+ 
+ =item Search tickets where the Requestor is also the Owner
+ 
+     Requestor.id = Owner
+ 
+ =item Search tickets where custom fields Foo and Bar have the same value
+ 
+     CF.Foo = CF.Bar
+ 
+ This is equal to:
+ 
+     CF.{Foo} = CF.{Bar}
+     CF.Foo = CF.Bar.Content
+ 
+ To compare LargeContent instead:
+ 
+     CF.IP = CF.IPRange.LargeContent
+ 
+ =back
++
 +=head1 Learn More
 +
 +To use the query builder to build and save reports, see
 +L<Dashboard and Reports|docs/dashboards_reporting.pod>. For definitions of
 +RT metadata, see L<Ticket Metadata|docs/ticket_metadata.pod>.
 +
 +=cut
diff --cc lib/RT/SQL.pm
index 96c9b45ac2,9599375a06..33e0d4e233
--- a/lib/RT/SQL.pm
+++ b/lib/RT/SQL.pm
@@@ -64,9 -64,17 +64,17 @@@ my @tokens = qw[VALUE AGGREGATOR OPERAT
  use Regexp::Common qw /delimited/;
  my $re_aggreg      = qr[(?i:AND|OR)];
  my $re_delim       = qr[$RE{delimited}{-delim=>qq{\'\"}}];
- my $re_value       = qr[[+-]?\d+|(?i:NULL)|$re_delim];
- my $re_keyword     = qr[[{}\w\.]+|$re_delim];
+ 
+ # We need to support bare(not quoted) strings like CF.{Beta Date} to use the
+ # content of related custom field as the value to compare, e.g.
+ #
+ #       Due < CF.{Beta Date}
+ #
+ # Support it in keyword part is mainly for consistency.
+ 
+ my $re_value       = qr[(?i:CF)\.\{.+?\}(?:\.(?i:Content|LargeContent))?|[\w\.]+|[+-]?\d+|(?i:NULL)|$re_delim];
+ my $re_keyword     = qr[(?i:CF)\.\{.+?\}(?:\.(?i:Content|LargeContent))?|[{}\w\.]+|$re_delim];
 -my $re_op          = qr[=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)|(?i:NOT STARTSWITH)|(?i:STARTSWITH)|(?i:NOT ENDSWITH)|(?i:ENDSWITH)]; # long to short
 +my $re_op          = qr[(?i:SHALLOW )?(?:=|!=|>=|<=|>|<|(?i:IS NOT)|(?i:IS)|(?i:NOT LIKE)|(?i:LIKE)|(?i:NOT STARTSWITH)|(?i:STARTSWITH)|(?i:NOT ENDSWITH)|(?i:ENDSWITH))]; # long to short
  my $re_open_paren  = qr[\(];
  my $re_close_paren = qr[\)];
  
diff --cc t/api/tickets.t
index facaefa7d1,6ca845d222..99648ee68b
--- a/t/api/tickets.t
+++ b/t/api/tickets.t
@@@ -161,83 -161,132 +161,212 @@@ ok( $unlimittickets->Count > 0, "UnLimi
      );
  }
  
 +diag "Ticket role group members";
 +{
 +    my $ticket = RT::Test->create_ticket( Queue => 'General', Subject => 'test ticket role group' );
 +    my $admincc = $ticket->RoleGroup('AdminCc');
 +
 +    my $delegates = RT::Test->load_or_create_group('delegates');
 +    my $core      = RT::Test->load_or_create_group('core team');
 +    my $alice     = RT::Test->load_or_create_user( Name => 'alice' );
 +    my $bob       = RT::Test->load_or_create_user( Name => 'bob' );
 +    ok( $delegates->AddMember( $core->PrincipalId ), 'Add core team to delegates' );
 +    ok( $delegates->AddMember( $bob->PrincipalId ),  'Add bob to delegates' );
 +    ok( $core->AddMember( $alice->PrincipalId ),     'Add alice to core team' );
 +
 +    for my $name ( 'alice', 'bob' ) {
 +        my $tickets = RT::Tickets->new( RT->SystemUser );
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name = '$name'");
 +        ok( !$tickets->Count, 'No tickets found' );
 +
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name != '$name'");
 +        is( $tickets->Count,     1,           'Found 1 ticket' );
 +        is( $tickets->First->id, $ticket->id, 'Found the ticket' );
 +
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name LIKE '$name'");
 +        ok( !$tickets->Count, 'No tickets found' );
 +
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name NOT LIKE '$name'");
 +        is( $tickets->Count,     1,           'Found 1 ticket' );
 +        is( $tickets->First->id, $ticket->id, 'Found the ticket' );
 +    }
 +
 +    ok( $admincc->AddMember( $delegates->PrincipalId ), 'Add delegates to AdminCc' );
 +
 +    for my $name ( 'alice', 'bob' ) {
 +        my $tickets = RT::Tickets->new( RT->SystemUser );
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name = '$name'");
 +        is( $tickets->Count,     1,           'Found 1 ticket' );
 +        is( $tickets->First->id, $ticket->id, 'Found the ticket' );
 +
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name != '$name'");
 +        ok( !$tickets->Count, 'No tickets found' );
 +
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name LIKE '$name'");
 +        is( $tickets->Count,     1,           'Found 1 ticket' );
 +        is( $tickets->First->id, $ticket->id, 'Found the ticket' );
 +
 +        $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name NOT LIKE '$name'");
 +        ok( !$tickets->Count, 'No tickets found' );
 +    }
 +
 +    my $abc = RT::Test->load_or_create_user( Name => 'abc' ); # so there are multiple users to search
 +    my $abc_ticket = RT::Test->create_ticket( Queue => 'General', Subject => 'test ticket role group' );
 +    ok( $abc_ticket->RoleGroup('AdminCc')->AddMember( $abc->PrincipalId ), 'Add abc to AdminCc' );
 +
 +    my $tickets = RT::Tickets->new( RT->SystemUser );
 +    $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name LIKE 'a'");
 +    is( $tickets->Count,     2,           'Found 2 ticket' );
 +
 +    $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCc.Name NOT LIKE 'a'");
 +    TODO: {
 +        local $TODO = <<EOF;
 +Searching NOT LIKE with multiple users is not the opposite of "LIKE", e.g.
 +
 +    "alice", "bob" are AdminCcs of ticket 1, abc is AdminCc of ticket 2:
 +    "AdminCc.Name LIKE 'a'" returns tickets 1 and 2.
 +    "AdminCc.Name NOT LIKE 'a'" returns ticket 1 because it has AdminCc "bob" which doesn't match "a".
 +
 +EOF
 +        ok( !$tickets->Count, 'No tickets found' );
 +    }
 +    if ( $tickets->Count ) {
 +        is( $tickets->Count,     1,           'Found 1 ticket' );
 +        is( $tickets->First->id, $ticket->id, 'Found the ticket' );
 +    }
 +
 +    $tickets->FromSQL("Subject = 'test ticket role group' AND AdminCcGroup = 'delegates'");
 +    is( $tickets->Count,     1,           'Found 1 ticket' );
 +    is( $tickets->First->id, $ticket->id, 'Found the ticket' );
 +}
 +
++diag "Columns as values in searches";
+ {
+     my $ticket = RT::Ticket->new( RT->SystemUser );
+     ok $ticket->Load(1), "Loaded test ticket 1";
+     my $date = RT::Date->new(RT->SystemUser);
+     $date->SetToNow();
+     $date->AddDays(1);
+ 
+     ok $ticket->SetDue( $date->ISO ), "Set Due to tomorrow";
+     my $tickets = RT::Tickets->new( RT->SystemUser );
+     my ( $ret, $msg ) = $tickets->FromSQL("LastUpdated < Due");
+ 
+     ok( $ret, 'Ran query with Due as searched value' );
+     my $count = $tickets->Count();
+     ok $count == 1, "Found one ticket";
+ 
+     my $cf_foo = RT::Test->load_or_create_custom_field( Name => 'foo', Type => 'FreeformSingle', Queue => 0 );
+     my $cf_bar = RT::Test->load_or_create_custom_field( Name => 'bar', Type => 'FreeformSingle', Queue => 0 );
+     ok( $ticket->AddCustomFieldValue( Field => $cf_foo, Value => 'this rocks!' ) );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.foo = CF.bar');
+     ok( $ret, 'Ran query with CF.foo = CF.bar' );
+     $count = $tickets->Count();
+     is( $count, 0, 'Found 0 tickets' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_bar, Value => 'this does not rock' ) );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.foo = CF.bar');
+     ok( $ret, 'Ran query with CF.foo = CF.bar' );
+     $count = $tickets->Count();
+     is( $count, 0, 'Found 0 tickets' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_bar, Value => 'this rocks!' ) );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.foo = CF.bar');
+     ok( $ret, 'Ran query with CF.foo = CF.bar' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.foo = "CF.{bar}"');
+     ok( $ret, 'Ran query with CF.foo = "CF.bar"' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.foo = Owner');
+     ok( $ret, 'Ran query with CF.foo = Owner' );
+     $count = $tickets->Count();
+     is( $count, 0, 'Found 0 tickets' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_foo, Value => RT->Nobody->id ) );
+     ( $ret, $msg ) = $tickets->FromSQL('CF.foo = Owner');
+     ok( $ret, 'Ran query with CF.foo = Owner' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     my $cf_beta = RT::Test->load_or_create_custom_field( Name => 'Beta Date', Type => 'DateTime', Queue => 0 );
+     ( $ret, $msg ) = $tickets->FromSQL('Due = CF.{Beta Date}');
+     ok( $ret, 'Ran query with Due = CF.{Beta Date}' );
+     $count = $tickets->Count();
+     is( $count, 0, 'Found 0 tickets' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_foo, Value => '1900' ) );
+     for my $operator ( '=', 'LIKE' ) {
+         ( $ret, $msg ) = $tickets->FromSQL("CF.foo $operator 1900");
+         ok( $ret, "Ran query with CF.foo $operator 1900" );
+         $count = $tickets->Count();
+         is( $count, 1, 'Found 1 ticket' );
+     }
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_beta, Value => $date->ISO( Timezone => 'user' ) ) );
+     ( $ret, $msg ) = $tickets->FromSQL('Due = CF.{Beta Date}');
+     ok( $ret, 'Ran query with Due = CF.{Beta Date}' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_beta, Value => $date->ISO( Timezone => 'user' ) ) );
+     ( $ret, $msg ) = $tickets->FromSQL('Due = CF.{Beta Date}.Content');
+     ok( $ret, 'Ran query with Due = CF.{Beta Date}.Content' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_beta, Value => $date->ISO( Timezone => 'user' ) ) );
+     ( $ret, $msg ) = $tickets->FromSQL('CF.{Beta Date} = Due');
+     ok( $ret, 'Ran query with CF.{Beta Date} = Due' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     my $cf_ip1  = RT::Test->load_or_create_custom_field( Name => 'IPRange 1', Type => 'IPAddressRangeSingle', Queue => 0 );
+     my $cf_ip2  = RT::Test->load_or_create_custom_field( Name => 'IPRange 2', Type => 'IPAddressRangeSingle', Queue => 0 );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1} = CF.{IPRange 2}');
+     ok( $ret, 'Ran query with CF.{IPRange 1} = CF.{IPRange 2}' );
+     $count = $tickets->Count();
+     is( $count, 0, 'Found 0 tickets' );
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_ip1, Value => '192.168.1.1-192.168.1.5' ));
+     ok( $ticket->AddCustomFieldValue( Field => $cf_ip2, Value => '192.168.1.1-192.168.1.6' ));
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1}.Content = CF.{IPRange 2}.Content');
+     ok( $ret, 'Ran query with CF.{IPRange 1}.Content = CF.{IPRange 2}.Content' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1}.Content = "CF.{IPRange 2}.Content"');
+     ok( $ret, 'Ran query with CF.{IPRange 1}.Content = "CF.{IPRange 2}.Content"' );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ 
+     ( $ret, $msg ) = $tickets->FromSQL('CF.{IPRange 1} = CF.{IPRange 2}');
+     ok( $ret, 'Ran query with CF.{IPRange 1} = CF.{IPRange 2}' );
+     $count = $tickets->Count();
+     TODO: {
+         local $TODO
 -            = "It'll be great if we can automatially compare both Content and LargeContent for queries like CF.{IPRange 1} = CF.{IPRange 2}";
++            = "It'll be great if we can automatically compare both Content and LargeContent for queries like CF.{IPRange 1} = CF.{IPRange 2}";
+         is( $count, 0, 'Found 0 tickets' );
+     }
+ 
+     ok( $ticket->AddCustomFieldValue( Field => $cf_ip2, Value => '192.168.1.1-192.168.1.5' ) );
+     ( $ret, $msg )
+         = $tickets->FromSQL(
+         'CF.{IPRange 1}.Content = CF.{IPRange 2}.Content AND CF.{IPRange 1}.LargeContent = CF.{IPRange 2}.LargeContent'
+         );
+     ok( $ret,
+         'Ran query with CF.{IPRange 1}.Content = CF.{IPRange 2}.Content AND CF.{IPRange 1}.LargeContent = CF.{IPRange 2}.LargeContent'
+       );
+     $count = $tickets->Count();
+     is( $count, 1, 'Found 1 ticket' );
+ }
+ 
  done_testing;

-----------------------------------------------------------------------


More information about the rt-commit mailing list