[Rt-commit] rt branch, 4.2-on-4.0/search-txn-and-queue-cfs, created. rt-4.0.13-105-gef5f696
Thomas Sibley
trs at bestpractical.com
Fri Jun 28 18:01:24 EDT 2013
The branch, 4.2-on-4.0/search-txn-and-queue-cfs has been created
at ef5f696df2db172b578a8daa4c995081fa4bf01f (commit)
- Log -----------------------------------------------------------------
commit 71ff489be674b4976463f403b821415fafa23e29
Author: Thomas Sibley <trs at bestpractical.com>
Date: Wed Jun 26 16:53:24 2013 -0700
Add TxnCF.{Name} and TransactionCF.{Name} syntax to TicketSQL
Reuses all the logic of ticket CF searching.
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 3b614e6..1e0a5af 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -145,6 +145,8 @@ our %FIELD_METADATA = (
CustomFieldValue => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
CustomField => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
CF => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+ TxnCF => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
+ TransactionCF => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
Updated => [ 'TRANSDATE', ], #loc_left_pair
RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
@@ -1384,6 +1386,7 @@ Factor out the Join of custom fields so we can use it for sorting too
our %JOIN_ALIAS_FOR_LOOKUP_TYPE = (
RT::Ticket->CustomFieldLookupType => sub { "main" },
+ RT::Transaction->CustomFieldLookupType => sub { $_[0]->JoinTransactions },
);
sub _CustomFieldJoin {
commit 2f91c33c73af509d46ff588c15f078db29f2c490
Author: Thomas Sibley <trs at bestpractical.com>
Date: Tue Jun 25 17:05:36 2013 -0700
Basic tests for searching transaction CFs
diff --git a/t/customfields/transaction_searching.t b/t/customfields/transaction_searching.t
new file mode 100644
index 0000000..b31d4f5
--- /dev/null
+++ b/t/customfields/transaction_searching.t
@@ -0,0 +1,108 @@
+use strict;
+use warnings;
+
+use RT::Test tests => 'no_declare';
+
+my $initialdata = RT::Test::get_relocatable_file("transaction-cfs" => "..", "data", "initialdata");
+my ($rv, $msg) = RT->DatabaseHandle->InsertData( $initialdata, undef, disconnect_after => 0 );
+ok($rv, "Inserted test data from $initialdata")
+ or diag "Error: $msg";
+
+my %ticket = (
+ Spam => { },
+ Coffee => { Billable => "No", },
+ Phone => { Billable => "Yes", Who => ["Telecom", "Information Technology"], When => "2013-06-25", Location => "Geology" },
+ Stacks => { Billable => "Yes", Who => "Library", When => "2013-06-01" },
+ Benches => { Billable => "Yes", Location => "Outdoors" },
+);
+
+create_tickets();
+
+# Sanity check
+results_are("CF.Location IS NOT NULL", [qw( Phone Benches )]);
+results_are("CF.Location IS NULL", [qw( Spam Coffee Stacks )]);
+
+results_are("TxnCF.Billable IS NULL", [qw( Spam )]);
+results_are("TxnCF.Billable IS NOT NULL", [qw( Coffee Phone Stacks Benches )]);
+results_are("TxnCF.Billable = 'No'", [qw( Coffee )]);
+results_are("TxnCF.Billable = 'Yes'", [qw( Phone Stacks Benches )]);
+results_are("TxnCF.Billable = 'Yes' AND CF.Location IS NOT NULL", [qw( Phone Benches )]);
+results_are("TxnCF.Billable = 'Yes' AND CF.Location = 'Outdoors'", [qw( Benches )]);
+results_are("TxnCF.Billable = 'Yes' AND CF.Location LIKE 'o'", [qw( Phone Benches )]);
+
+results_are("TxnCF.Who = 'Telecom' OR TxnCF.Who = 'Library'", [qw( Phone Stacks )]);
+results_are("TxnCF.Who != 'Library'", [qw( Spam Coffee Phone Benches )]);
+
+results_are("TxnCF.When > '2013-06-24'", [qw( Phone )]);
+results_are("TxnCF.When < '2013-06-24'", [qw( Stacks )]);
+results_are("TxnCF.When >= '2013-06-01' and TxnCF.When <= '2013-06-30'", [qw( Phone Stacks )]);
+
+results_are("TxnCF.Who LIKE 'e'", [qw( Phone )]);
+results_are("TxnCF.Who NOT LIKE 'e'", [qw( Spam Coffee Stacks Benches )]);
+results_are("TxnCF.Who NOT LIKE 'e' and TxnCF.Who IS NOT NULL", [qw( Stacks )]);
+
+# XXX TODO:
+# Queue-specific txn CFs
+# Multiple transaction CFs by name
+
+done_testing;
+
+sub results_are {
+ my $query = shift;
+ my $expected = shift;
+ my %expected = map { $_ => 1 } @$expected;
+ my @unexpected;
+
+ my $tickets = RT::Tickets->new(RT->SystemUser);
+ my ($ok, $msg) = $tickets->FromSQL($query);
+ ok($ok, "Searched: $query")
+ or return diag $msg;
+ for my $t (@{$tickets->ItemsArrayRef}) {
+ if (delete $expected{$t->Subject}) {
+ ok(1, "Found expected ticket ".$t->Subject);
+ } else {
+ push @unexpected, $t->Subject;
+ }
+ }
+ ok(0, "Didn't find expected ticket $_")
+ for grep $expected{$_}, @$expected;
+ ok(0, "Found unexpected tickets: ".join ", ", @unexpected)
+ if @unexpected;
+}
+
+sub create_tickets {
+ for my $subject (sort keys %ticket) {
+ my %cfs = %{$ticket{$subject}};
+ my $location = delete $cfs{Location};
+
+ my $ticket = RT::Ticket->new( RT->SystemUser );
+ my ($ok, $msg) = $ticket->Create(
+ Queue => "General",
+ Subject => $subject,
+ );
+ ok($ticket->id, "Created ticket: $msg") or next;
+
+ if ($location) {
+ ($ok, $msg) = $ticket->AddCustomFieldValue( Field => "Location", Value => $location );
+ ok($ok, "Added Location: $msg") or next;
+ }
+
+ my ($txnid, $txnmsg, $txn) = $ticket->Correspond( Content => "test transaction" );
+ unless ($txnid) {
+ RT->Logger->error("Unable to correspond on ticket $ok: $txnmsg");
+ next;
+ }
+ for my $name (sort keys %cfs) {
+ my $values = ref $cfs{$name} ? $cfs{$name} : [$cfs{$name}];
+ for my $v (@$values) {
+ ($ok, $msg) = $txn->_AddCustomFieldValue(
+ Field => $name,
+ Value => $v,
+ RecordTransaction => 0
+ );
+ RT->Logger->error("Unable to add value '$v' to CF '$name': $msg")
+ unless $ok;
+ }
+ }
+ }
+}
diff --git a/t/data/initialdata/transaction-cfs b/t/data/initialdata/transaction-cfs
new file mode 100644
index 0000000..e43d986
--- /dev/null
+++ b/t/data/initialdata/transaction-cfs
@@ -0,0 +1,36 @@
+use strict;
+use warnings;
+
+our @CustomFields = (
+ map +{
+ LookupType => RT::Transaction->CustomFieldLookupType,
+ MaxValues => 1,
+ Type => "Freeform",
+ %$_
+ },
+ { Name => "Billable",
+ Type => "Select",
+ Values => [
+ { Name => "Yes", SortOrder => 1 },
+ { Name => "No", SortOrder => 2 },
+ ],
+ },
+ { Name => "Who",
+ Type => "SelectMultiple",
+ Values => [
+ map +{ Name => $_ },
+ "Facilities",
+ "Information Technology",
+ "Library",
+ "Telecom",
+ ],
+ },
+ { Name => "When",
+ Type => "Date",
+ },
+
+ # Some ticket CFs to test mixed searches
+ { Name => "Location",
+ LookupType => RT::Ticket->CustomFieldLookupType,
+ },
+);
commit a3165e5d699753d2119f736983eeb8ba23390004
Author: Thomas Sibley <trs at bestpractical.com>
Date: Wed Jun 26 16:40:25 2013 -0700
Report correct line numbers for failed tests
diff --git a/t/customfields/transaction_searching.t b/t/customfields/transaction_searching.t
index b31d4f5..97a5206 100644
--- a/t/customfields/transaction_searching.t
+++ b/t/customfields/transaction_searching.t
@@ -48,6 +48,8 @@ results_are("TxnCF.Who NOT LIKE 'e' and TxnCF.Who IS NOT NULL", [qw( Stacks )]);
done_testing;
sub results_are {
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
my $query = shift;
my $expected = shift;
my %expected = map { $_ => 1 } @$expected;
commit e22472e2f486f5457676530373394fc420b6f029
Author: Thomas Sibley <trs at bestpractical.com>
Date: Wed Jun 26 16:39:32 2013 -0700
Meaningful negative searching is hard, comment some tests out as TODO for now
Negative searching is especially hard when there are multiple records
joining back to a ticket.
diff --git a/t/customfields/transaction_searching.t b/t/customfields/transaction_searching.t
index 97a5206..88b6b28 100644
--- a/t/customfields/transaction_searching.t
+++ b/t/customfields/transaction_searching.t
@@ -22,7 +22,9 @@ create_tickets();
results_are("CF.Location IS NOT NULL", [qw( Phone Benches )]);
results_are("CF.Location IS NULL", [qw( Spam Coffee Stacks )]);
-results_are("TxnCF.Billable IS NULL", [qw( Spam )]);
+# TODO: Ideal behaviour of TxnCF IS NULL not yet determined
+#results_are("TxnCF.Billable IS NULL", [qw( Spam )]);
+
results_are("TxnCF.Billable IS NOT NULL", [qw( Coffee Phone Stacks Benches )]);
results_are("TxnCF.Billable = 'No'", [qw( Coffee )]);
results_are("TxnCF.Billable = 'Yes'", [qw( Phone Stacks Benches )]);
@@ -31,14 +33,19 @@ results_are("TxnCF.Billable = 'Yes' AND CF.Location = 'Outdoors'", [qw( Benches
results_are("TxnCF.Billable = 'Yes' AND CF.Location LIKE 'o'", [qw( Phone Benches )]);
results_are("TxnCF.Who = 'Telecom' OR TxnCF.Who = 'Library'", [qw( Phone Stacks )]);
-results_are("TxnCF.Who != 'Library'", [qw( Spam Coffee Phone Benches )]);
+
+# TODO: Negative searching finds tickets with at least one txn doesn't have the value
+#results_are("TxnCF.Who != 'Library'", [qw( Spam Coffee Phone Benches )]);
results_are("TxnCF.When > '2013-06-24'", [qw( Phone )]);
results_are("TxnCF.When < '2013-06-24'", [qw( Stacks )]);
results_are("TxnCF.When >= '2013-06-01' and TxnCF.When <= '2013-06-30'", [qw( Phone Stacks )]);
results_are("TxnCF.Who LIKE 'e'", [qw( Phone )]);
-results_are("TxnCF.Who NOT LIKE 'e'", [qw( Spam Coffee Stacks Benches )]);
+
+# TODO: Negative searching finds tickets with at least one txn doesn't have the value
+#results_are("TxnCF.Who NOT LIKE 'e'", [qw( Spam Coffee Stacks Benches )]);
+
results_are("TxnCF.Who NOT LIKE 'e' and TxnCF.Who IS NOT NULL", [qw( Stacks )]);
# XXX TODO:
commit 886dce7f9a238227de7ea8f33614ffaaca322f87
Author: Thomas Sibley <trs at bestpractical.com>
Date: Thu Jun 27 17:14:54 2013 -0700
Tests for multiple transaction CFs named the same, applied to different queues
diff --git a/t/customfields/transaction_searching.t b/t/customfields/transaction_searching.t
index 88b6b28..0958b5e 100644
--- a/t/customfields/transaction_searching.t
+++ b/t/customfields/transaction_searching.t
@@ -8,7 +8,7 @@ my ($rv, $msg) = RT->DatabaseHandle->InsertData( $initialdata, undef, disconnect
ok($rv, "Inserted test data from $initialdata")
or diag "Error: $msg";
-my %ticket = (
+create_tickets(
Spam => { },
Coffee => { Billable => "No", },
Phone => { Billable => "Yes", Who => ["Telecom", "Information Technology"], When => "2013-06-25", Location => "Geology" },
@@ -16,8 +16,6 @@ my %ticket = (
Benches => { Billable => "Yes", Location => "Outdoors" },
);
-create_tickets();
-
# Sanity check
results_are("CF.Location IS NOT NULL", [qw( Phone Benches )]);
results_are("CF.Location IS NULL", [qw( Spam Coffee Stacks )]);
@@ -48,9 +46,26 @@ results_are("TxnCF.Who LIKE 'e'", [qw( Phone )]);
results_are("TxnCF.Who NOT LIKE 'e' and TxnCF.Who IS NOT NULL", [qw( Stacks )]);
-# XXX TODO:
+
+# Multiple CFs with same name applied to different queues
+clear_tickets();
+create_tickets(
+ BlueNone => { Queue => "Blues" },
+ PurpleNone => { Queue => "Purples" },
+
+ Blue => { Queue => "Blues", Color => "Blue" },
+ Purple => { Queue => "Purples", Color => "Purple" },
+);
+
# Queue-specific txn CFs
+results_are("TxnCF.Blues.{Color} = 'Blue'", [qw( Blue )]);
+results_are("TxnCF.Blues.{Color} = 'Purple'", []);
+
# Multiple transaction CFs by name
+results_are("TxnCF.{Color} IS NOT NULL", [qw( Blue Purple )]);
+results_are("TxnCF.{Color} = 'Blue'", [qw( Blue )]);
+results_are("TxnCF.{Color} = 'Purple'", [qw( Purple )]);
+results_are("TxnCF.{Color} LIKE 'e'", [qw( Blue Purple )]);
done_testing;
@@ -66,7 +81,7 @@ sub results_are {
my ($ok, $msg) = $tickets->FromSQL($query);
ok($ok, "Searched: $query")
or return diag $msg;
- for my $t (@{$tickets->ItemsArrayRef}) {
+ for my $t (@{$tickets->ItemsArrayRef || []}) {
if (delete $expected{$t->Subject}) {
ok(1, "Found expected ticket ".$t->Subject);
} else {
@@ -80,13 +95,15 @@ sub results_are {
}
sub create_tickets {
+ my %ticket = @_;
for my $subject (sort keys %ticket) {
- my %cfs = %{$ticket{$subject}};
- my $location = delete $cfs{Location};
+ my %data = %{$ticket{$subject}};
+ my $location = delete $data{Location};
+ my $queue = delete $data{Queue} || "General";
my $ticket = RT::Ticket->new( RT->SystemUser );
my ($ok, $msg) = $ticket->Create(
- Queue => "General",
+ Queue => $queue,
Subject => $subject,
);
ok($ticket->id, "Created ticket: $msg") or next;
@@ -101,17 +118,23 @@ sub create_tickets {
RT->Logger->error("Unable to correspond on ticket $ok: $txnmsg");
next;
}
- for my $name (sort keys %cfs) {
- my $values = ref $cfs{$name} ? $cfs{$name} : [$cfs{$name}];
+ for my $name (sort keys %data) {
+ my $values = ref $data{$name} ? $data{$name} : [$data{$name}];
for my $v (@$values) {
($ok, $msg) = $txn->_AddCustomFieldValue(
Field => $name,
Value => $v,
RecordTransaction => 0
);
- RT->Logger->error("Unable to add value '$v' to CF '$name': $msg")
- unless $ok;
+ ok($ok, "Added txn CF $name value '$v'")
+ or diag $msg;
}
}
}
}
+
+sub clear_tickets {
+ my $tickets = RT::Tickets->new( RT->SystemUser );
+ $tickets->FromSQL("id > 0");
+ $_->SetStatus("deleted") for @{$tickets->ItemsArrayRef};
+}
diff --git a/t/data/initialdata/transaction-cfs b/t/data/initialdata/transaction-cfs
index e43d986..25c8274 100644
--- a/t/data/initialdata/transaction-cfs
+++ b/t/data/initialdata/transaction-cfs
@@ -1,6 +1,11 @@
use strict;
use warnings;
+our @Queues = (
+ { Name => "Blues" },
+ { Name => "Purples" },
+);
+
our @CustomFields = (
map +{
LookupType => RT::Transaction->CustomFieldLookupType,
@@ -29,6 +34,17 @@ our @CustomFields = (
Type => "Date",
},
+ # Two CFs named the same, but each applied to only one queue
+ # Note: Queue => ref forces RT::Handle to apply rather than
+ # RT::CustomField->Create; the former respects LookupType, the latter
+ # doesn't.
+ { Name => "Color",
+ Queue => ["Blues"],
+ },
+ { Name => "Color",
+ Queue => ["Purples"],
+ },
+
# Some ticket CFs to test mixed searches
{ Name => "Location",
LookupType => RT::Ticket->CustomFieldLookupType,
commit 6159a5c5b1e9dde90506925b6c773e1ea407cf55
Author: Thomas Sibley <trs at bestpractical.com>
Date: Thu Jun 27 17:41:39 2013 -0700
QueueCF.{Name} in TicketSQL
This makes queue CFs suddenly useful for queries such as:
QueueCF.{Type} = 'support'
Currently it joins from Tickets to Queues in order to join to
OCFs/OCFVs, but a future optimization could remove the Tickets â Queues
join in favor of limiting on Tickets.Queue directly.
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index 1e0a5af..61c30d5 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -147,6 +147,7 @@ our %FIELD_METADATA = (
CF => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
TxnCF => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
TransactionCF => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
+ QueueCF => [ 'CUSTOMFIELD' => 'Queue' ], #loc_left_pair
Updated => [ 'TRANSDATE', ], #loc_left_pair
RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
@@ -1387,6 +1388,14 @@ Factor out the Join of custom fields so we can use it for sorting too
our %JOIN_ALIAS_FOR_LOOKUP_TYPE = (
RT::Ticket->CustomFieldLookupType => sub { "main" },
RT::Transaction->CustomFieldLookupType => sub { $_[0]->JoinTransactions },
+ RT::Queue->CustomFieldLookupType => sub {
+ return $_[0]->{_sql_aliases}{queues} ||= $_[0]->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'Queue',
+ TABLE2 => 'Queues',
+ FIELD2 => 'id',
+ );
+ },
);
sub _CustomFieldJoin {
@@ -1407,6 +1416,7 @@ sub _CustomFieldJoin {
my ($ObjectCFs, $CFs);
if ( $cfid ) {
+ # XXX: Could avoid join and use main.Queue instead?
$ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
TYPE => 'LEFT',
ALIAS1 => $ObjectAlias,
@@ -1466,6 +1476,7 @@ sub _CustomFieldJoin {
$self->SUPER::Limit(
LEFTJOIN => $ObjectCFs,
FIELD => 'ObjectId',
+ # XXX: Could avoid join and use main.Queue instead?
VALUE => "$ObjectAlias.id",
QUOTEVALUE => 0,
ENTRYAGGREGATOR => 'AND',
commit ef5f696df2db172b578a8daa4c995081fa4bf01f
Author: Thomas Sibley <trs at bestpractical.com>
Date: Fri Jun 28 14:34:39 2013 -0700
Transaction and Queue CF pickers in the Query Builder
diff --git a/share/html/NoAuth/css/aileron/ticket-search.css b/share/html/NoAuth/css/aileron/ticket-search.css
index 5b0aa83..039c91c 100644
--- a/share/html/NoAuth/css/aileron/ticket-search.css
+++ b/share/html/NoAuth/css/aileron/ticket-search.css
@@ -49,6 +49,17 @@
position: relative;
}
+#pick-criteria tr.separator td {
+ position: relative;
+}
+#pick-criteria tr.separator td em {
+ position: absolute;
+ right: 0;
+
+ font-weight: normal;
+ font-variant: italic;
+}
+
#pick-criteria select {
width: 8em;
}
diff --git a/share/html/Search/Elements/PickCriteria b/share/html/Search/Elements/PickCriteria
index 485fceb..9641f79 100644
--- a/share/html/Search/Elements/PickCriteria
+++ b/share/html/Search/Elements/PickCriteria
@@ -53,6 +53,8 @@
% $m->callback( %ARGS, CallbackName => "BeforeBasics" );
<& PickBasics, queues => \%queues &>
<& PickTicketCFs, queues => \%queues &>
+<& PickObjectCFs, Class => 'Transaction', queues => \%queues &>
+<& PickObjectCFs, Class => 'Queue', queues => \%queues &>
% $m->callback( %ARGS, CallbackName => "AfterCFs" );
<tr class="separator"><td colspan="3"><hr /></td></tr>
diff --git a/share/html/Search/Elements/PickObjectCFs b/share/html/Search/Elements/PickObjectCFs
new file mode 100644
index 0000000..929893a
--- /dev/null
+++ b/share/html/Search/Elements/PickObjectCFs
@@ -0,0 +1,27 @@
+<%ARGS>
+$Class
+%queues => ()
+</%ARGS>
+<%init>
+my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'} );
+$CustomFields->ApplySortOrder;
+$CustomFields->LimitToLookupType( "RT::$Class"->CustomFieldLookupType );
+$CustomFields->LimitToObjectId(0);
+
+foreach my $name (keys %queues) {
+ my $queue = RT::Queue->new($session{'CurrentUser'});
+ $queue->Load($name);
+ $CustomFields->LimitToObjectId($queue->Id) if $queue->Id;
+}
+
+my $has_cf = $CustomFields->First ? 1 : 0;
+$CustomFields->GotoFirstItem;
+</%init>
+% if ($has_cf) {
+<tr class="separator">
+ <td colspan="3">
+ <hr><em><% loc("[_1] CFs", loc($Class)) %></em>
+ </td>
+</tr>
+% }
+<& PickCFs, %ARGS, TicketSQLField => "${Class}CF", CustomFields => $CustomFields &>
-----------------------------------------------------------------------
More information about the Rt-commit
mailing list