Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search request fields with cfs #10

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,58 @@ USAGE
items, but it may be increased up to 100 (or decreased if desired). Page
numbers start at 1.

Fields
When fetching search results you can include additional fields by adding
a query parameter fields which is a comma seperated list of fields to
include. You must use the camel case version of the name as included in
the results for the actual item.

CustomFields can be specified in the fields parameter either with
CustomField-*cf_name* or CF.{*cf_name*} syntaxes.

You can use additional fields parameters to expand child blocks, for
example (line wrapping inserted for readability):

XX_RT_URL_XX/REST/2.0/tickets
?fields=Owner,Status,Created,Subject,Queue,CF.{My CF}
&fields[Queue]=Name,Description

Says that in the result set for tickets, the extra fields for Owner,
Status, Created, Subject, Queue, and CustomField "My CF" should be
included. But in addition, for the Queue block, also include Name and
Description. The results would be similar to this (only one ticket is
displayed):

"items" : [
{
"Subject" : "Sample Ticket",
"id" : "2",
"type" : "ticket",
"Owner" : {
"id" : "root",
"_url" : "XX_RT_URL_XX/REST/2.0/user/root",
"type" : "user"
},
"_url" : "XX_RT_URL_XX/REST/2.0/ticket/2",
"Status" : "resolved",
"Created" : "2018-06-29:10:25Z",
"CF.{My CF} : "My CF Value",
"Queue" : {
"id" : "1",
"type" : "queue",
"Name" : "General",
"Description" : "The default queue",
"_url" : "XX_RT_URL_XX/REST/2.0/queue/1"
}
}
{ … },
],

If the user performing the query doesn't have rights to view the record
(or sub record), then the empty string or an empty hash will be
returned.

Authentication Methods
Authentication should always be done over HTTPS/SSL for security. You
should only serve up the /REST/2.0/ endpoint over SSL.
Expand Down Expand Up @@ -556,8 +608,9 @@ BUGS
<http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Extension-REST2>.

LICENSE AND COPYRIGHT
This software is Copyright (c) 2015-2017 by Best Practical Solutions,
LLC.
This software is Copyright (c) 2015-2018 by Best Practical Solutions,
LLC. Portions are Copyright (c) 2018 by Catalyst Cloud Ltd. Portions are
Copyright (c) 2018 by Easter-eggs SARL.

This is free software, licensed under:

Expand Down
55 changes: 54 additions & 1 deletion lib/RT/Extension/REST2.pm
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,57 @@ the query parameters C<page> and C<per_page>. The default page size is 20
items, but it may be increased up to 100 (or decreased if desired). Page
numbers start at 1.

=head2 Fields

When fetching search results you can include additional fields by adding
a query parameter C<fields> which is a comma seperated list of fields
to include. You must use the camel case version of the name as included
in the results for the actual item.

CustomFields can be specified in the C<fields> parameter either with
CustomField-I<cf_name> or CF.{I<cf_name>} syntaxes.

You can use additional fields parameters to expand child blocks, for
example (line wrapping inserted for readability):

XX_RT_URL_XX/REST/2.0/tickets
?fields=Owner,Status,Created,Subject,Queue,CF.{My CF}
&fields[Queue]=Name,Description

Says that in the result set for tickets, the extra fields for Owner, Status,
Created, Subject, Queue, and CustomField "My CF" should be included. But in addition, for the Queue
block, also include Name and Description. The results would be similar to
this (only one ticket is displayed):

"items" : [
{
"Subject" : "Sample Ticket",
"id" : "2",
"type" : "ticket",
"Owner" : {
"id" : "root",
"_url" : "XX_RT_URL_XX/REST/2.0/user/root",
"type" : "user"
},
"_url" : "XX_RT_URL_XX/REST/2.0/ticket/2",
"Status" : "resolved",
"Created" : "2018-06-29:10:25Z",
"CF.{My CF} : "My CF Value",
"Queue" : {
"id" : "1",
"type" : "queue",
"Name" : "General",
"Description" : "The default queue",
"_url" : "XX_RT_URL_XX/REST/2.0/queue/1"
}
}
{ … },
],

If the user performing the query doesn't have rights to view the record
(or sub record), then the empty string or an empty hash will be returned.

=head2 Authentication Methods

Authentication should B<always> be done over HTTPS/SSL for
Expand Down Expand Up @@ -676,7 +727,9 @@ L<rt.cpan.org|http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Extension-REST

=head1 LICENSE AND COPYRIGHT

This software is Copyright (c) 2015-2017 by Best Practical Solutions, LLC.
This software is Copyright (c) 2015-2018 by Best Practical Solutions, LLC.
Portions are Copyright (c) 2018 by Catalyst Cloud Ltd.
Portions are Copyright (c) 2018 by Easter-eggs SARL.

This is free software, licensed under:

Expand Down
85 changes: 82 additions & 3 deletions lib/RT/Extension/REST2/Resource/Collection.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extends 'RT::Extension::REST2::Resource';
use Scalar::Util qw( blessed );
use Web::Machine::FSM::States qw( is_status_code );
use Module::Runtime qw( require_module );
use RT::Extension::REST2::Util qw( serialize_record expand_uid );
use RT::Extension::REST2::Util qw( serialize_record expand_uid format_datetime );

has 'collection_class' => (
is => 'ro',
Expand Down Expand Up @@ -54,10 +54,19 @@ sub serialize {
my $self = shift;
my $collection = $self->collection;
my @results;
my @fields = defined $self->request->param('fields') ? split(/,/, $self->request->param('fields')) : ();

while (my $item = $collection->Next) {
# TODO: Allow selection of desired fields
push @results, expand_uid( $item->UID );
my $result = expand_uid( $item->UID );

# Allow selection of desired fields
if ($result) {
for my $field (@fields) {
my $field_result = $self->expand_field($item, $field);
$result->{$field} = $field_result if defined $field_result;
}
}
push @results, $result;
}
return {
count => scalar(@results) + 0,
Expand All @@ -68,6 +77,76 @@ sub serialize {
};
}

# Used in Serialize to allow additional fields to be selected ala JSON API on:
# http://jsonapi.org/examples/
sub expand_field {
my $self = shift;
my $item = shift;
my $field = shift;
my $param_prefix = shift || 'fields';

my ($result, $obj);
if ($item->can('_Accessible') && $item->_Accessible($field => 'read')) {
# RT::Record derived object, so we can check access permissions.

if ($item->_Accessible($field => 'type') =~ /(datetime|timestamp)/i) {
$result = format_datetime($item->$field);
} elsif ($item->can($field . 'Obj')) {
my $method = $field . 'Obj';
$obj = $item->$method;
if ($obj->can('UID')) {
$result = expand_uid( $obj->UID );
} else {
$result = {};
}
} else {
$result = $item->$field;
}
}

# Process CF
if ($field =~ /^C(?:ustom)?F(?:ield)?-(.+)|CF\.\{([^\}]+)\}$/) {
my $cf_name = $1 // $2;
my $cf = RT::CustomField->new($self->current_user);
my ($cf_id, $msg) = $cf->Load($cf_name);
unless ($cf_id) {
$RT::Logger->warn("Cannot find CustomField $cf_name: $msg")
} else {
my $vals = $item->CustomFieldValues($cf->id);
if ( $cf->SingleValue ) {
my $v = $vals->Next;
$result = $v->Content if $v;
} else {
my @results;
while (my $v = $vals->Next()) {
my $content = $v->Content;
if ( $v->Content =~ /,/ ) {
$content =~ s/([\\'])/\\$1/g;
push @results, q{'} . $content . q{'};
} else {
push @results, $content;
}
}
$result = \@results;
}
}
}

$result //= '';

if (defined $obj && defined $result) {
my $param_field = $param_prefix . '[' . $field . ']';
my @subfields = split(/,/, $self->request->param($param_field) || '');

for my $subfield (@subfields) {
my $subfield_result = $self->expand_field($obj, $subfield, $param_field);
$result->{$subfield} = $subfield_result if defined $subfield_result;
}
}

return $result;
}

# XXX TODO: Bulk update via DELETE/PUT on a collection resource?

sub charsets_provided { [ 'utf-8' ] }
Expand Down
1 change: 1 addition & 0 deletions lib/RT/Extension/REST2/Util.pm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use Sub::Exporter -setup => {
escape_uri
query_string
custom_fields_for
format_datetime
]]
};

Expand Down
114 changes: 113 additions & 1 deletion t/queues.t
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,117 @@ my ($features_url, $features_id);
like($queue->{_url}, qr{$rest_base_path/queue/$features_id$});
}

done_testing;
# id > 0 (finds new Features queue but not disabled Bugs queue), include Name field
{
my $res = $mech->post_json("$rest_base_path/queues?fields=Name",
[{ field => 'id', operator => '>', value => 0 }],
'Authorization' => $auth,
);
is($res->code, 200);

my $content = $mech->json_response;
is(scalar @{$content->{items}}, 1);

my $queue = $content->{items}->[0];
is($queue->{Name}, 'Features');
is(scalar keys %$queue, 4);
}


# all queues, basic fields
{
my $res = $mech->post_json("$rest_base_path/queues/all",
[],
'Authorization' => $auth,
);
is($res->code, 200);

my $content = $mech->json_response;
is(scalar @{$content->{items}}, 1);

my $queue = $content->{items}->[0];
is(scalar keys %$queue, 3);
}

# all queues, basic fields plus Name
{
my $res = $mech->post_json("$rest_base_path/queues/all?fields=Name",
[],
'Authorization' => $auth,
);
is($res->code, 200);

my $content = $mech->json_response;
is(scalar @{$content->{items}}, 1);

my $queue = $content->{items}->[0];
is(scalar keys %$queue, 4);
is($queue->{Name}, 'Features');
}

# all queues, basic fields plus Name, Lifecycle. Lifecycle should be empty
# string as we don't allow returning it.
{
my $res = $mech->post_json("$rest_base_path/queues/all?fields=Name,Lifecycle",
[],
'Authorization' => $auth,
);
is($res->code, 200);

my $content = $mech->json_response;
is(scalar @{$content->{items}}, 1);

my $queue = $content->{items}->[0];
is(scalar keys %$queue, 5);
is($queue->{Name}, 'Features');
is_deeply($queue->{Lifecycle}, {}, 'Lifecycle is empty');
}

# all queues, basic fields plus Name and CustomFields
{
my $features_queue = RT::Queue->new( RT->SystemUser );
my ($ok, $msg) = $features_queue->Load( $features_id );
ok($ok, $msg);

my $single_cf = RT::CustomField->new( RT->SystemUser );
($ok, $msg) = $single_cf->Create( Name => 'Single', LookupType => 'RT::Queue', Type => 'FreeformSingle' );
ok($ok, $msg);
my $single_cf_id = $single_cf->Id;

($ok, $msg) = $single_cf->AddToObject( $features_queue );
ok($ok, $msg);

($ok, $msg) = $features_queue->AddCustomFieldValue( Field => $single_cf_id , Value => "I'm a single CF" );
ok($ok, $msg);

my $multi_cf = RT::CustomField->new( RT->SystemUser );
($ok, $msg) = $multi_cf->Create( Name => 'Multi CF', LookupType => 'RT::Queue', Type => 'FreeformMultiple' );
ok($ok, $msg);
my $multi_cf_id = $multi_cf->Id;

($ok, $msg) = $multi_cf->AddToObject( $features_queue );
ok($ok, $msg);

($ok, $msg) = $features_queue->AddCustomFieldValue( Field => $multi_cf_id , Value => "First Value" );
ok($ok, $msg);

($ok, $msg) = $features_queue->AddCustomFieldValue( Field => $multi_cf_id , Value => "Second Value" );
ok($ok, $msg);

my $res = $mech->post_json("$rest_base_path/queues/all?fields=Name,CustomField-Single,CF.{Multi CF}",
[],
'Authorization' => $auth,
);
is($res->code, 200);

my $content = $mech->json_response;
is(scalar @{$content->{items}}, 1);

my $queue = $content->{items}->[0];
is(scalar keys %$queue, 6);
is($queue->{Name}, 'Features');
is($queue->{'CustomField-Single'}, "I'm a single CF");
is_deeply($queue->{'CF.{Multi CF}'}, [ 'First Value', 'Second Value' ]);
}

done_testing;
Loading