[Bps-public-commit] rt-extension-rest2 branch, master, updated. 1.07-51-g7191872

? sunnavy sunnavy at bestpractical.com
Fri May 1 16:17:54 EDT 2020


The branch, master has been updated
       via  719187227d6a9f744862311a03b51100da62780a (commit)
       via  980c06423db3fff49daaafc6fc3a4d5ffaae3d32 (commit)
       via  3b30b149198a95b7fafa79ba8b253e49224b05a5 (commit)
       via  b79d92887a585d17d62c7ee0cd1a4da3a85e5bb8 (commit)
       via  d794e41b8048490d9adbe075d9173961adb64a49 (commit)
       via  3f90dd767829319d946b6aaec8f42159636b558f (commit)
       via  79c7df0bff81611878f2718abc706bdaa9f94c21 (commit)
       via  eb50695daee1cac8b2448a0b31c8419c69f8ae62 (commit)
      from  caeb9bf64562f65720f6f11d69dda1f6aba28b03 (commit)

Summary of changes:
 README                                             |  64 +++++++-
 lib/RT/Extension/REST2.pm                          |  65 +++++++-
 lib/RT/Extension/REST2/Resource.pm                 |  37 ++++-
 lib/RT/Extension/REST2/Resource/Collection.pm      |  46 +++++-
 .../REST2/Resource/Collection/QueryByJSON.pm       |   6 +
 xt/pagination.t                                    | 166 +++++++++++++++++++++
 xt/search-json.t                                   | 134 +++++++++++++++++
 xt/search-simple.t                                 |  63 ++++++++
 xt/ticket-customfields.t                           |  35 +++++
 xt/tickets.t                                       |   1 +
 xt/users.t                                         |   1 +
 11 files changed, 599 insertions(+), 19 deletions(-)
 create mode 100644 xt/pagination.t
 create mode 100644 xt/search-simple.t

- Log -----------------------------------------------------------------
commit eb50695daee1cac8b2448a0b31c8419c69f8ae62
Author: Andrew Ruthven <puck at catalystcloud.nz>
Date:   Tue Oct 29 07:35:21 2019 +0000

    Allow including CustomFields in search results.

diff --git a/README b/README
index a6e7830..25aff6f 100644
--- a/README
+++ b/README
@@ -947,13 +947,14 @@ USAGE
     example (line wrapping inserted for readability):
 
         XX_RT_URL_XX/REST/2.0/tickets
-          ?fields=Owner,Status,Created,Subject,Queue
+          ?fields=Owner,Status,Created,Subject,Queue,CustomFields
           &fields[Queue]=Name,Description
 
     Says that in the result set for tickets, the extra fields for Owner,
-    Status, Created, Subject and Queue 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):
+    Status, Created, Subject, Queue and CustomFields 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 in this
+    example):
 
        "items" : [
           {
@@ -974,7 +975,18 @@ USAGE
                 "Name" : "General",
                 "Description" : "The default queue",
                 "_url" : "XX_RT_URL_XX/REST/2.0/queue/1"
-             }
+             },
+             "CustomFields" : [
+                 {
+                     "id" : "1",
+                     "type" : "customfield",
+                     "_url" : "XX_RT_URL_XX/REST/2.0/customfield/1",
+                     "name" : "My Custom Field",
+                     "values" : [
+                         "CustomField value"
+                     },
+                 }
+             ]
           }
           { … },
           …
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 41378dd..0b31276 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -1024,13 +1024,14 @@ 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
+      ?fields=Owner,Status,Created,Subject,Queue,CustomFields
       &fields[Queue]=Name,Description
 
 Says that in the result set for tickets, the extra fields for Owner, Status,
-Created, Subject and Queue 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):
+Created, Subject, Queue and CustomFields 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 in this
+example):
 
    "items" : [
       {
@@ -1051,7 +1052,18 @@ this (only one ticket is displayed):
             "Name" : "General",
             "Description" : "The default queue",
             "_url" : "XX_RT_URL_XX/REST/2.0/queue/1"
-         }
+         },
+         "CustomFields" : [
+             {
+                 "id" : "1",
+                 "type" : "customfield",
+                 "_url" : "XX_RT_URL_XX/REST/2.0/customfield/1",
+                 "name" : "My Custom Field",
+                 "values" : [
+                     "CustomField value"
+                 },
+             }
+         ]
       }
       { … },
       …
diff --git a/lib/RT/Extension/REST2/Resource.pm b/lib/RT/Extension/REST2/Resource.pm
index d7e611c..e420de3 100644
--- a/lib/RT/Extension/REST2/Resource.pm
+++ b/lib/RT/Extension/REST2/Resource.pm
@@ -5,7 +5,7 @@ use warnings;
 use Moose;
 use MooseX::NonMoose;
 use namespace::autoclean;
-use RT::Extension::REST2::Util qw(expand_uid format_datetime );
+use RT::Extension::REST2::Util qw(expand_uid format_datetime custom_fields_for);
 
 extends 'Web::Machine::Resource';
 
@@ -30,7 +30,40 @@ sub expand_field {
     my $param_prefix = shift || 'fields';
 
     my $result;
-    if ($item->can('_Accessible') && $item->_Accessible($field => 'read')) {
+    if ($field eq 'CustomFields') {
+        if (my $cfs = custom_fields_for($item)) {
+            my %values;
+            while (my $cf = $cfs->Next) {
+                if (! defined $values{$cf->Id}) {
+                    $values{$cf->Id} = {
+                        %{ expand_uid($cf->UID) },
+                        name   => $cf->Name,
+                        values => [],
+                    };
+                }
+
+                my $ocfvs = $cf->ValuesForObject($item);
+                my $type  = $cf->Type;
+                while ( my $ocfv = $ocfvs->Next ) {
+                    my $content = $ocfv->Content;
+                    if ( $type eq 'DateTime' ) {
+                        $content = format_datetime($content);
+                    }
+                    elsif ( $type eq 'Image' or $type eq 'Binary' ) {
+                        $content = {
+                            content_type => $ocfv->ContentType,
+                            filename     => $content,
+                            _url         => RT::Extension::REST2->base_uri . "/download/cf/" . $ocfv->id,
+                        };
+                    }
+                    push @{ $values{ $cf->Id }{values} }, $content;
+                }
+            }
+
+            push @{ $result }, values %values if %values;
+        }
+
+    } elsif ($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) {
diff --git a/xt/ticket-customfields.t b/xt/ticket-customfields.t
index 8454160..66c6915 100644
--- a/xt/ticket-customfields.t
+++ b/xt/ticket-customfields.t
@@ -155,6 +155,24 @@ my $no_ticket_cf_values = bag(
   { name => 'Multi',  id => $multi_cf_id,  type => 'customfield', _url => ignore(), values => [] },
 );
 
+# Rights Test - Searching asking for CustomFields without SeeCustomField
+{
+    my $res = $mech->get("$rest_base_path/tickets?query=id>0&fields=Status,Owner,CustomFields,Subject&fields[Owner]=Name",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is(scalar @{$content->{items}}, 4);
+
+    my $ticket = $content->{items}->[0];
+
+    is($ticket->{Status}, 'new');
+    is($ticket->{Owner}{Name}, 'Nobody');
+    is_deeply($ticket->{CustomFields}, '', 'Ticket custom field not present');
+    is($ticket->{Subject}, 'Ticket creation using REST');
+    is(scalar keys %$ticket, 7);
+}
+
 # Rights Test - With ShowTicket and SeeCustomField
 {
     $user->PrincipalObj->GrantRight( Right => 'SeeCustomField');
@@ -548,6 +566,23 @@ for my $value (
         { name => 'Multi',  id => $multi_cf_id,  type => 'customfield', _url => ignore(), values => $output },
     );
     cmp_deeply($content->{'CustomFields'}, $ticket_cf_value, 'Ticket custom field');
+
+    # Ticket Show - Fields, custom fields
+    {
+        $res = $mech->get("$rest_base_path/tickets?query=id>0&fields=Status,Owner,CustomFields,Subject&fields[Owner]=Name",
+            'Authorization' => $auth,
+        );
+        is($res->code, 200);
+        my $content = $mech->json_response;
+
+        # Just look at the last one.
+        my $ticket = $content->{items}->[-1];
+
+        is($ticket->{Status}, 'new');
+        is($ticket->{id}, $ticket_id);
+        is($ticket->{Subject}, 'Multi-value CF');
+        cmp_deeply($ticket->{'CustomFields'}, $ticket_cf_value, 'Ticket custom field');
+    }
 }
 
 {

commit 79c7df0bff81611878f2718abc706bdaa9f94c21
Author: gibus <gibus at easter-eggs.com>
Date:   Sat Oct 20 23:18:01 2018 +0200

    Allow 'entry_aggregator' property in JSON search

diff --git a/README b/README
index 25aff6f..3c7ea02 100644
--- a/README
+++ b/README
@@ -750,6 +750,36 @@ USAGE
     order query parameters are supported in both JSON and TicketSQL
     searches.
 
+    The same field is specified more than one time to express more than one
+    condition on this field. For example:
+
+        [
+            { "field":    "id",
+              "operator": ">",
+              "value":    $min },
+
+            { "field":     "id",
+              "operator": "<",
+              "value":    $max }
+        ]
+
+    By default, RT will aggregate these conditions with an OR, except for
+    when searching queues, where an AND is applied. If you want to search
+    for multiple conditions on the same field aggregated with an AND (or an
+    OR for queues), you can specify entry_aggregator keys in corresponding
+    hashes:
+
+        [
+            { "field":    "id",
+              "operator": ">",
+              "value":    $min },
+
+            { "field":             "id",
+              "operator":         "<",
+              "value":            $max,
+              "entry_aggregator": "AND" }
+        ]
+
     Results are returned in the format described below.
 
   Example of plural resources (collections)
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 0b31276..5ffc694 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -817,6 +817,36 @@ C<orderby=Created&order=ASC&orderby=id&order=DESC>. C<orderby> and
 C<order> query parameters are supported in both JSON and TicketSQL
 searches.
 
+The same C<field> is specified more than one time to express more than one
+condition on this field. For example:
+
+    [
+        { "field":    "id",
+          "operator": ">",
+          "value":    $min },
+
+        { "field":     "id",
+          "operator": "<",
+          "value":    $max }
+    ]
+
+By default, RT will aggregate these conditions with an C<OR>, except for
+when searching queues, where an C<AND> is applied. If you want to search for
+multiple conditions on the same field aggregated with an C<AND> (or an C<OR>
+for queues), you can specify C<entry_aggregator> keys in corresponding
+hashes:
+
+    [
+        { "field":    "id",
+          "operator": ">",
+          "value":    $min },
+
+        { "field":             "id",
+          "operator":         "<",
+          "value":            $max,
+          "entry_aggregator": "AND" }
+    ]
+
 Results are returned in
 L<the format described below|/"Example of plural resources (collections)">.
 
diff --git a/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm b/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
index a834042..3be0190 100644
--- a/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
@@ -57,6 +57,9 @@ sub limit_collection {
                 ? (OPERATOR => $limit->{operator})
                 : () ),
             CASESENSITIVE => ($limit->{case_sensitive} || 0),
+            ( $limit->{entry_aggregator}
+                ? (ENTRYAGGREGATOR => $limit->{entry_aggregator})
+                : () ),
         );
     }
 

commit 3f90dd767829319d946b6aaec8f42159636b558f
Author: gibus <gibus at easter-eggs.com>
Date:   Sat Oct 20 23:17:42 2018 +0200

    Add tests for 'entry_aggregator' property in JSON searches

diff --git a/xt/search-json.t b/xt/search-json.t
index e1fee44..bdccdc6 100644
--- a/xt/search-json.t
+++ b/xt/search-json.t
@@ -166,5 +166,86 @@ my $bravo_id = $bravo->Id;
     like($third->{_url}, qr{$rest_base_path/queue/$beta_id$});
 }
 
+# Aggregate conditions with OR, Queues defaults to AND
+{
+    my $res = $mech->post_json("$rest_base_path/queues",
+        [
+            { field => 'id', operator => '>', value => 2 },
+            { entry_aggregator => 'OR', field => 'id', operator => '<', value => 4 },
+        ],
+        'Authorization' => $auth,
+    );
+
+    my $content = $mech->json_response;
+    is($content->{count}, 4);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 4);
+    is(scalar @{$content->{items}}, 4);
+    my @ids = sort map {$_->{id}} @{$content->{items}};
+    is_deeply(\@ids, [1, $alpha_id, $beta_id, $bravo_id]);
+}
+
+# Aggregate conditions with AND, Queues defaults to AND
+{
+    my $res = $mech->post_json("$rest_base_path/queues",
+        [
+            { field => 'id', operator => '>', value => 2 },
+            { field => 'id', operator => '<', value => 4 },
+        ],
+        'Authorization' => $auth,
+    );
+
+    my $content = $mech->json_response;
+    is($content->{count}, 1);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 1);
+    is(scalar @{$content->{items}}, 1);
+    is($content->{items}->[0]->{id}, $alpha_id);
+}
+
+my $cf1 = RT::Test->load_or_create_custom_field(Name  => 'cf1', Type  => 'FreeformSingle', Queue => 'General');
+my $cf2 = RT::Test->load_or_create_custom_field(Name  => 'cf2', Type  => 'FreeformSingle', Queue => 'General');
+my $cf3 = RT::Test->load_or_create_custom_field(Name  => 'cf3', Type  => 'FreeformSingle', Queue => 'General');
+# Aggregate conditions with OR, CustomFields defaults to OR
+{
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [
+            { field => 'id', operator => '>', value => 2 },
+            { field => 'id', operator => '<', value => 4 },
+        ],
+        'Authorization' => $auth,
+    );
+
+    my $content = $mech->json_response;
+    is($content->{count}, 4);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 4);
+    is(scalar @{$content->{items}}, 4);
+    my @ids = sort map {$_->{id}} @{$content->{items}};
+    is_deeply(\@ids, [1, 2, 3, 4]);
+}
+
+# Aggregate conditions with AND, CustomFields defaults to OR
+{
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [
+            { field => 'id', operator => '>', value => 2 },
+            { entry_aggregator => 'AND', field => 'id', operator => '<', value => 4 },
+        ],
+        'Authorization' => $auth,
+    );
+
+    my $content = $mech->json_response;
+    is($content->{count}, 1);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 1);
+    is(scalar @{$content->{items}}, 1);
+    is($content->{items}->[0]->{id}, 3);
+}
+
 done_testing;
 

commit d794e41b8048490d9adbe075d9173961adb64a49
Author: Andrew Ruthven <puck at catalystcloud.nz>
Date:   Fri Oct 25 06:29:15 2019 +0000

    Improve the user experience of pagination.
    
    Collection results which require pagination now include "pages",
    "next_page" and "prev_page". The later two only if there are next
    or previous pages. For example:
    
    https://rt.example.com/REST/2.0/queues/all?per_page=5\&page=3
    
    {
       "count" : 5,
       "pages" : 8,
       "page" : 3,
       "per_page" : 5,
       "prev_page" : "https://rt.example.com/REST/2.0/queues/all?per_page=5&page=2",
       "next_page" : "https://rt.example.com/REST/2.0/queues/all?per_page=5&page=4",
       "total" : 37,
       "items" : [ ... ]
    }
    
    Don't limit the page param by a guess of the number of pages.
    
    Because we haven't applied the limits to the query yet, the call
    to CountAll returns the total number of results. Therefore, using
    that to limit the number of available of pages is useless.

diff --git a/README b/README
index 3c7ea02..d1cc512 100644
--- a/README
+++ b/README
@@ -41,6 +41,7 @@ USAGE
            "total" : 1,
            "count" : 1,
            "page" : 1,
+           "pages" : 1,
            "per_page" : 20,
            "items" : [
               {
@@ -789,7 +790,9 @@ USAGE
         {
            "count" : 20,
            "page" : 1,
+           "pages" : 191,
            "per_page" : 20,
+           "next_page" : "<collection path>?page=2"
            "total" : 3810,
            "items" : [
               { … },
@@ -965,7 +968,10 @@ USAGE
     All plural resources (such as /tickets) require pagination, controlled
     by the query parameters page and 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.
+    numbers start at 1. The number of pages is returned, and if there is a
+    next or previous page, then the URL for that page is returned in the
+    next_page and prev_page variables respectively. It is up to you to store
+    the required JSON to pass with the following page request.
 
   Fields
     When fetching search results you can include additional fields by adding
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 5ffc694..1303cac 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -69,6 +69,7 @@ see a response, typical of search results, like this:
        "total" : 1,
        "count" : 1,
        "page" : 1,
+       "pages" : 1,
        "per_page" : 20,
        "items" : [
           {
@@ -858,7 +859,9 @@ standard JSON format:
     {
        "count" : 20,
        "page" : 1,
+       "pages" : 191,
        "per_page" : 20,
+       "next_page" : "<collection path>?page=2"
        "total" : 3810,
        "items" : [
           { … },
@@ -1041,7 +1044,10 @@ return the content of the file as an octet string:
 All plural resources (such as C</tickets>) require pagination, controlled by
 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.
+numbers start at 1. The number of pages is returned, and if there is a next
+or previous page, then the URL for that page is returned in the next_page
+and prev_page variables respectively. It is up to you to store the required
+JSON to pass with the following page request.
 
 =head2 Fields
 
diff --git a/lib/RT/Extension/REST2/Resource/Collection.pm b/lib/RT/Extension/REST2/Resource/Collection.pm
index f646885..567b396 100644
--- a/lib/RT/Extension/REST2/Resource/Collection.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection.pm
@@ -11,6 +11,7 @@ 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 format_datetime );
+use POSIX qw( ceil );
 
 has 'collection_class' => (
     is  => 'ro',
@@ -33,12 +34,14 @@ sub _build_collection {
 sub setup_paging {
     my $self = shift;
     my $per_page = $self->request->param('per_page') || 20;
-       $per_page = 20  if $per_page <= 0;
-       $per_page = 100 if $per_page > 100;
+    if    ( $per_page !~ /^\d+$/ ) { $per_page = 20 }
+    elsif ( $per_page == 0 )       { $per_page = 20 }
+    elsif ( $per_page > 100 )      { $per_page = 100 }
     $self->collection->RowsPerPage($per_page);
 
     my $page = $self->request->param('page') || 1;
-       $page = 1 if $page < 0;
+    if    ( $page !~ /^\d+$/ ) { $page = 1 }
+    elsif ( $page == 0 )       { $page = 1 }
     $self->collection->GotoPage($page - 1);
 }
 
@@ -68,13 +71,39 @@ sub serialize {
         }
         push @results, $result;
     }
-    return {
+
+    my %results = (
         count       => scalar(@results)         + 0,
         total       => $collection->CountAll    + 0,
         per_page    => $collection->RowsPerPage + 0,
         page        => ($collection->FirstRow / $collection->RowsPerPage) + 1,
         items       => \@results,
+    );
+
+    my $uri = $self->request->uri;
+    my @query_form = $uri->query_form;
+    # find page and if it is set, delete it and its value.
+    for my $i (0..$#query_form) {
+        if ($query_form[$i] eq 'page') {
+            delete @query_form[$i, $i + 1];
+            last;
+        }
+    }
+
+    $results{pages} = ceil($results{total} / $results{per_page});
+    if ($results{page} < $results{pages}) {
+        my $page = $results{page} + 1;
+        $uri->query_form( @query_form, page => $results{page} + 1 );
+        $results{next_page} = $uri->as_string;
+    };
+    if ($results{page} > 1) {
+        # If we're beyond the last page, set prev_page as the last page
+        # available, otherwise, the previous page.
+        $uri->query_form( @query_form, page => ($results{page} > $results{pages} ? $results{pages} : $results{page} - 1) );
+        $results{prev_page} = $uri->as_string;
     };
+
+    return \%results;
 }
 
 # XXX TODO: Bulk update via DELETE/PUT on a collection resource?
diff --git a/xt/pagination.t b/xt/pagination.t
new file mode 100644
index 0000000..60c077b
--- /dev/null
+++ b/xt/pagination.t
@@ -0,0 +1,166 @@
+use strict;
+use warnings;
+use RT::Extension::REST2::Test tests => undef;
+
+my $mech = RT::Extension::REST2::Test->mech;
+my $auth = RT::Extension::REST2::Test->authorization_header;
+my $rest_base_path = '/REST/2.0';
+my $user = RT::Extension::REST2::Test->user;
+
+my $alpha = RT::Test->load_or_create_queue( Name => 'Alpha' );
+my $bravo = RT::Test->load_or_create_queue( Name => 'Bravo' );
+$user->PrincipalObj->GrantRight( Right => 'SuperUser' );
+
+my $alpha_id = $alpha->Id;
+my $bravo_id = $bravo->Id;
+
+# Default per_page (20), only 1 page.
+{
+    my $res = $mech->post_json("$rest_base_path/queues/all",
+        [],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{count}, 3);
+    is($content->{page}, 1);
+    is($content->{pages}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 3);
+    undef($content->{prev_page});
+    undef($content->{next_page});
+    is(scalar @{$content->{items}}, 3);
+}
+
+# per_page = 3, only 1 page.
+{
+    my $res = $mech->post_json("$rest_base_path/queues/all?per_page=3",
+        [],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{count}, 3);
+    is($content->{page}, 1);
+    is($content->{pages}, 1);
+    is($content->{per_page}, 3);
+    is($content->{total}, 3);
+    undef($content->{prev_page});
+    undef($content->{next_page});
+    is(scalar @{$content->{items}}, 3);
+}
+
+# per_page = 1, 3 pages, page 1.
+{
+    my $url = "$rest_base_path/queues/all?per_page=1";
+    my $res = $mech->post_json($url,
+        [],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    # Ensure our use of $url as a regex works.
+    $url =~ s/\?/\\?/;
+
+    my $content = $mech->json_response;
+    is($content->{count}, 1);
+    is($content->{page}, 1);
+    is($content->{pages}, 3);
+    is($content->{per_page}, 1);
+    is($content->{total}, 3);
+    undef($content->{prev_page});
+    like($content->{next_page}, qr[$url&page=2]);
+    is(scalar @{$content->{items}}, 1);
+}
+
+# per_page = 1, 3 pages, page 2.
+{
+    my $url = "$rest_base_path/queues/all?per_page=1";
+    my $res = $mech->post_json("$url&page=2",
+        [],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    # Ensure our use of $url as a regex works.
+    $url =~ s/\?/\\?/;
+
+    my $content = $mech->json_response;
+    is($content->{count}, 1);
+    is($content->{page}, 2);
+    is($content->{pages}, 3);
+    is($content->{per_page}, 1);
+    is($content->{total}, 3);
+    like($content->{prev_page}, qr[$url&page=1]);
+    like($content->{next_page}, qr[$url&page=3]);
+    is(scalar @{$content->{items}}, 1);
+}
+
+# per_page = 1, 3 pages, page 3.
+{
+    my $url = "$rest_base_path/queues/all?per_page=1";
+    my $res = $mech->post_json("$url&page=3",
+        [],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    # Ensure our use of $url as a regex works.
+    $url =~ s/\?/\\?/;
+
+    my $content = $mech->json_response;
+    is($content->{count}, 1);
+    is($content->{page}, 3);
+    is($content->{pages}, 3);
+    is($content->{per_page}, 1);
+    is($content->{total}, 3);
+    like($content->{prev_page}, qr[$url&page=2]);
+    undef($content->{next_page});
+    is(scalar @{$content->{items}}, 1);
+}
+
+# Test sanity checking for the pagination parameters.
+{
+    my $url = "$rest_base_path/queues/all";
+    for my $param ( 'per_page', 'page' ) {
+	for my $value ( 'abc', '-10', '30' ) {
+	    # No need to test the following combination.
+	    next if $param eq 'per_page' && $value eq '30';
+
+	    my $res = $mech->post_json("$url?$param=$value",
+		[],
+		'Authorization' => $auth,
+	    );
+	    is($res->code, 200);
+
+	    my $content = $mech->json_response;
+	    if ($param eq 'page') {
+		if ($value eq '30') {
+		    is($content->{count}, 0);
+		    is($content->{page}, 30);
+		    is(scalar @{$content->{items}}, 0);
+		    like($content->{prev_page}, qr[$url\?page=1]);
+		} else {
+		    is($content->{count}, 3);
+		    is($content->{page}, 1);
+		    is(scalar @{$content->{items}}, 3);
+		    is($content->{prev_page}, undef);
+		}
+	    }
+	    is($content->{pages}, 1);
+	    if ($param eq 'per_page') {
+		if ($value eq '30') {
+		    is($content->{per_page}, 30);
+		} else {
+		    is($content->{per_page}, 20);
+		}
+	    }
+	    is($content->{total}, 3);
+	    is($content->{next_page}, undef);
+	}
+    }
+}
+
+done_testing;
diff --git a/xt/tickets.t b/xt/tickets.t
index 2261bde..3324f69 100644
--- a/xt/tickets.t
+++ b/xt/tickets.t
@@ -180,6 +180,7 @@ my ($ticket_url, $ticket_id);
     my $content = $mech->json_response;
     is($content->{count}, 1);
     is($content->{page}, 1);
+    is($content->{pages}, 1);
     is($content->{per_page}, 20);
     is($content->{total}, 1);
     is(scalar @{$content->{items}}, 1);
diff --git a/xt/users.t b/xt/users.t
index 2621eb3..92d1583 100644
--- a/xt/users.t
+++ b/xt/users.t
@@ -49,6 +49,7 @@ my ($ok, $msg) = $group1->AddMember($user_bar->id);
                    'count' => 7,
                    'total' => 7,
                    'per_page' => 20,
+                   'pages' => 1,
                    'page' => 1
                });
     is(scalar(@$items), 7);

commit b79d92887a585d17d62c7ee0cd1a4da3a85e5bb8
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Mar 30 10:07:03 2020 +0100

    Allow JSON searches to return disabled objects with 'find_disabled_rows=1'

diff --git a/README b/README
index d1cc512..449d07e 100644
--- a/README
+++ b/README
@@ -729,6 +729,9 @@ USAGE
             ]
         '
 
+    By default, only enabled objects are returned. To include disabled
+    objects you can specify find_disabled_rows=1 as query parameter.
+
     The JSON payload must be an array of hashes with the keys field and
     value and optionally operator.
 
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 1303cac..448d9e4 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -795,6 +795,9 @@ values).  An example:
         ]
     '
 
+By default, only enabled objects are returned. To include disabled objects
+you can specify C<find_disabled_rows=1> as query parameter.
+
 The JSON payload must be an array of hashes with the keys C<field> and C<value>
 and optionally C<operator>.
 
diff --git a/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm b/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
index 3be0190..9a7756c 100644
--- a/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
@@ -45,6 +45,9 @@ sub limit_collection {
     my @fields      = $self->searchable_fields;
     my %searchable  = map {; $_ => 1 } @fields;
 
+    $collection->{'find_disabled_rows'} = 1
+        if $self->request->param('find_disabled_rows');
+
     for my $limit (@$query) {
         next unless $limit->{field}
                 and $searchable{$limit->{field}}

commit 3b30b149198a95b7fafa79ba8b253e49224b05a5
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Mar 30 10:15:36 2020 +0100

    Tests for JSON searches returning disabled objects with 'find_disabled_rows=1'

diff --git a/xt/search-json.t b/xt/search-json.t
index bdccdc6..0a1cea4 100644
--- a/xt/search-json.t
+++ b/xt/search-json.t
@@ -247,5 +247,58 @@ my $cf3 = RT::Test->load_or_create_custom_field(Name  => 'cf3', Type  => 'Freefo
     is($content->{items}->[0]->{id}, 3);
 }
 
+# Find disabled row
+{
+    $alpha->SetDisabled(1);
+
+    my $res = $mech->post_json("$rest_base_path/queues",
+        [{ field => 'id', operator => '>', value => 2 }],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{count}, 2);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 2);
+    is(scalar @{$content->{items}}, 2);
+
+    my ($first, $second) = @{ $content->{items} };
+    is($first->{type}, 'queue');
+    is($first->{id}, $beta_id);
+    like($first->{_url}, qr{$rest_base_path/queue/$beta_id$});
+
+    is($second->{type}, 'queue');
+    is($second->{id}, $bravo_id);
+    like($second->{_url}, qr{$rest_base_path/queue/$bravo_id$});
+
+    my $res_disabled = $mech->post_json("$rest_base_path/queues?find_disabled_rows=1",
+        [{ field => 'id', operator => '>', value => 2 }],
+        'Authorization' => $auth,
+    );
+    is($res_disabled->code, 200);
+
+    my $content_disabled = $mech->json_response;
+    is($content_disabled->{count}, 3);
+    is($content_disabled->{page}, 1);
+    is($content_disabled->{per_page}, 20);
+    is($content_disabled->{total}, 3);
+    is(scalar @{$content_disabled->{items}}, 3);
+
+    my ($first_disabled, $second_disabled, $third_disabled) = @{ $content_disabled->{items} };
+    is($first_disabled->{type}, 'queue');
+    is($first_disabled->{id}, $alpha_id);
+    like($first_disabled->{_url}, qr{$rest_base_path/queue/$alpha_id$});
+
+    is($second_disabled->{type}, 'queue');
+    is($second_disabled->{id}, $beta_id);
+    like($second_disabled->{_url}, qr{$rest_base_path/queue/$beta_id$});
+
+    is($third_disabled->{type}, 'queue');
+    is($third_disabled->{id}, $bravo_id);
+    like($third_disabled->{_url}, qr{$rest_base_path/queue/$bravo_id$});
+}
+
 done_testing;
 

commit 980c06423db3fff49daaafc6fc3a4d5ffaae3d32
Author: Aaron Trevena <ast at bestpractical.com>
Date:   Mon Mar 30 14:34:36 2020 +0100

    Allow non-JSON searches to return disabled objects with 'find_disabled_rows=1'

diff --git a/README b/README
index 449d07e..91b8a84 100644
--- a/README
+++ b/README
@@ -729,9 +729,6 @@ USAGE
             ]
         '
 
-    By default, only enabled objects are returned. To include disabled
-    objects you can specify find_disabled_rows=1 as query parameter.
-
     The JSON payload must be an array of hashes with the keys field and
     value and optionally operator.
 
@@ -976,6 +973,10 @@ USAGE
     next_page and prev_page variables respectively. It is up to you to store
     the required JSON to pass with the following page request.
 
+  Disabled items
+    By default, only enabled objects are returned. To include disabled
+    objects you can specify find_disabled_rows=1 as a query parameter.
+
   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
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 448d9e4..27a7af7 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -795,9 +795,6 @@ values).  An example:
         ]
     '
 
-By default, only enabled objects are returned. To include disabled objects
-you can specify C<find_disabled_rows=1> as query parameter.
-
 The JSON payload must be an array of hashes with the keys C<field> and C<value>
 and optionally C<operator>.
 
@@ -1052,6 +1049,11 @@ or previous page, then the URL for that page is returned in the next_page
 and prev_page variables respectively. It is up to you to store the required
 JSON to pass with the following page request.
 
+=head2 Disabled items
+
+By default, only enabled objects are returned. To include disabled objects
+you can specify C<find_disabled_rows=1> as a query parameter.
+
 =head2 Fields
 
 When fetching search results you can include additional fields by adding
diff --git a/lib/RT/Extension/REST2/Resource/Collection.pm b/lib/RT/Extension/REST2/Resource/Collection.pm
index 567b396..848bc1b 100644
--- a/lib/RT/Extension/REST2/Resource/Collection.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection.pm
@@ -45,7 +45,14 @@ sub setup_paging {
     $self->collection->GotoPage($page - 1);
 }
 
-sub limit_collection { 1 }
+sub limit_collection {
+    my $self        = shift;
+    my $collection  = $self->collection;
+    $collection->{'find_disabled_rows'} = 1
+        if $self->request->param('find_disabled_rows');
+
+    return 1;
+}
 
 sub search {
     my $self = shift;
diff --git a/xt/search-simple.t b/xt/search-simple.t
new file mode 100644
index 0000000..12b1fd2
--- /dev/null
+++ b/xt/search-simple.t
@@ -0,0 +1,63 @@
+use strict;
+use warnings;
+use RT::Extension::REST2::Test tests => undef;
+
+my $mech = RT::Extension::REST2::Test->mech;
+my $auth = RT::Extension::REST2::Test->authorization_header;
+my $rest_base_path = '/REST/2.0';
+my $user = RT::Extension::REST2::Test->user;
+
+my $alpha = RT::Test->load_or_create_queue( Name => 'Alpha', Description => 'Queue for test' );
+my $beta  = RT::Test->load_or_create_queue( Name => 'Beta', Description => 'Queue for test' );
+my $bravo = RT::Test->load_or_create_queue( Name => 'Bravo', Description => 'Queue to test sorted search' );
+$user->PrincipalObj->GrantRight( Right => 'SuperUser' );
+
+my $alpha_id = $alpha->Id;
+my $beta_id  = $beta->Id;
+my $bravo_id = $bravo->Id;
+
+# without disabled
+{
+    my $res = $mech->get("$rest_base_path/queues/all",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{count}, 4);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 4);
+    is(scalar @{$content->{items}}, 4);
+}
+
+# Find disabled
+{
+    $alpha->SetDisabled(1);
+    my $res = $mech->get("$rest_base_path/queues/all",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{count}, 3);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 3);
+    is(scalar @{$content->{items}}, 3);
+
+    $res = $mech->get("$rest_base_path/queues/all?find_disabled_rows=1",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    $content = $mech->json_response;
+    is($content->{count}, 5);
+    is($content->{page}, 1);
+    is($content->{per_page}, 20);
+    is($content->{total}, 5);
+    is(scalar @{$content->{items}}, 5);
+
+}
+
+done_testing;

commit 719187227d6a9f744862311a03b51100da62780a
Merge: caeb9bf 980c064
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat May 2 04:14:51 2020 +0800

    Merge branch 'search-enhancements'


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


More information about the Bps-public-commit mailing list