[Bps-public-commit] rt-extension-rest2 branch, saved-searches, created. 1.12-14-ge4442b5

? sunnavy sunnavy at bestpractical.com
Thu May 27 16:30:47 EDT 2021


The branch, saved-searches has been created
        at  e4442b52ddd28808f631c2e236b6eaee4ab11c6b (commit)

- Log -----------------------------------------------------------------
commit 7f6868c16a443f6cc2d74de9dadc0eff0c5cf936
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 27 05:42:56 2021 +0800

    Support additional fields parameters for Roles and CustomFields in Collection
    
    E.g. fields=Requestor&fields[Requestor]=EmailAddress

diff --git a/lib/RT/Extension/REST2/Resource.pm b/lib/RT/Extension/REST2/Resource.pm
index 594f5c9..c58fe9b 100644
--- a/lib/RT/Extension/REST2/Resource.pm
+++ b/lib/RT/Extension/REST2/Resource.pm
@@ -36,7 +36,7 @@ sub expand_field {
             while (my $cf = $cfs->Next) {
                 if (! defined $values{$cf->Id}) {
                     $values{$cf->Id} = {
-                        %{ expand_uid($cf->UID) },
+                        %{ $self->_expand_object($cf, $field, $param_prefix) },
                         name   => $cf->Name,
                         values => [],
                     };
@@ -71,13 +71,14 @@ sub expand_field {
 
                 my $group = $item->RoleGroup($role);
                 if ( !$group->Id ) {
-                    $data{$role} = expand_uid( RT->Nobody->UserObj->UID ) if $item->_ROLES->{$role}{Single};
+                    $data{$role} = $self->_expand_object( RT->Nobody->UserObj, $field, $param_prefix )
+                        if $item->_ROLES->{$role}{Single};
                     next;
                 }
 
                 my $gms = $group->MembersObj;
                 while ( my $gm = $gms->Next ) {
-                    push @{ $data{$role} }, expand_uid( $gm->MemberObj->Object->UID );
+                    push @{ $data{$role} }, $self->_expand_object( $gm->MemberObj->Object, $field, $param_prefix );
                 }
 
                 # Avoid the extra array ref for single member roles
@@ -91,13 +92,14 @@ sub expand_field {
 
             my $group = $item->RoleGroup($field);
             if ( !$group->Id ) {
-                $result = expand_uid( RT->Nobody->UserObj->UID ) if $item->_ROLES->{$field}{Single};
+                $result = $self->_expand_object( RT->Nobody->UserObj, $field, $param_prefix )
+                    if $item->_ROLES->{$field}{Single};
                 next;
             }
 
             my $gms = $group->MembersObj;
             while ( my $gm = $gms->Next ) {
-                push @$result, expand_uid( $gm->MemberObj->Object->UID );
+                push @$result, $self->_expand_object( $gm->MemberObj->Object, $field, $param_prefix );
             }
 
             # Avoid the extra array ref for single member roles
@@ -112,14 +114,8 @@ sub expand_field {
         } elsif ($item->can($field . 'Obj')) {
             my $method = $field . 'Obj';
             my $obj = $item->$method;
-            if ( $obj->can('UID') and $result = expand_uid( $obj->UID ) ) {
-                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;
-                }
+            if ( $obj->can('UID') ) {
+                $result = $self->_expand_object( $obj, $field, $param_prefix );
             }
         }
 
@@ -129,6 +125,25 @@ sub expand_field {
     return $result // '';
 }
 
+sub _expand_object {
+    my $self         = shift;
+    my $object       = shift;
+    my $field        = shift;
+    my $param_prefix = shift || 'fields';
+
+    return unless $object->can('UID');
+
+    my $result      = expand_uid( $object->UID ) or return;
+    my $param_field = $param_prefix . '[' . $field . ']';
+    my @subfields   = split( /,/, $self->request->param($param_field) || '' );
+
+    for my $subfield (@subfields) {
+        my $subfield_result = $self->expand_field( $object, $subfield, $param_field );
+        $result->{$subfield} = $subfield_result if defined $subfield_result;
+    }
+    return $result;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/lib/RT/Extension/REST2/Resource/Assets.pm b/lib/RT/Extension/REST2/Resource/Assets.pm
index 5144833..ae218d9 100644
--- a/lib/RT/Extension/REST2/Resource/Assets.pm
+++ b/lib/RT/Extension/REST2/Resource/Assets.pm
@@ -28,7 +28,7 @@ sub expand_field {
         if ( my $group = $item->RoleGroup($role) ) {
             my $gms = $group->MembersObj;
             while ( my $gm = $gms->Next ) {
-                push @$members, expand_uid( $gm->MemberObj->Object->UID );
+                push @$members, $self->_expand_object( $gm->MemberObj->Object, $field, $param_prefix );
             }
             $members = shift @$members if $group->SingleMemberRoleGroup;
         }
diff --git a/lib/RT/Extension/REST2/Resource/Tickets.pm b/lib/RT/Extension/REST2/Resource/Tickets.pm
index 8abbc7d..f060d49 100644
--- a/lib/RT/Extension/REST2/Resource/Tickets.pm
+++ b/lib/RT/Extension/REST2/Resource/Tickets.pm
@@ -77,7 +77,7 @@ sub expand_field {
         if ( my $group = $item->RoleGroup($role) ) {
             my $gms = $group->MembersObj;
             while ( my $gm = $gms->Next ) {
-                push @$members, expand_uid( $gm->MemberObj->Object->UID );
+                push @$members, $self->_expand_object( $gm->MemberObj->Object, $field, $param_prefix );
             }
         }
         return $members;

commit 649dc129ac4a14f42aaf311ae0c754c26f8f1464
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue May 25 22:26:53 2021 +0800

    Abstract setup_ordering to simplify limit_collection and also avoid duplication

diff --git a/lib/RT/Extension/REST2/Resource/Collection.pm b/lib/RT/Extension/REST2/Resource/Collection.pm
index ee3245a..c27df40 100644
--- a/lib/RT/Extension/REST2/Resource/Collection.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection.pm
@@ -12,6 +12,7 @@ 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 );
+use Encode;
 
 has 'collection_class' => (
     is  => 'ro',
@@ -45,6 +46,21 @@ sub setup_paging {
     $self->collection->GotoPage($page - 1);
 }
 
+sub setup_ordering {
+    my $self = shift;
+    my @orderby_cols;
+    my @orders = $self->request->param('order');
+    foreach my $orderby ($self->request->param('orderby')) {
+        $orderby = decode_utf8($orderby);
+        my $order = shift @orders || 'ASC';
+        $order = uc(decode_utf8($order));
+        $order = 'ASC' unless $order eq 'DESC';
+        push @orderby_cols, {FIELD => $orderby, ORDER => $order};
+    }
+    $self->collection->OrderByCols(@orderby_cols)
+        if @orderby_cols;
+}
+
 sub limit_collection {
     my $self        = shift;
     my $collection  = $self->collection;
@@ -57,6 +73,7 @@ sub limit_collection {
 sub search {
     my $self = shift;
     $self->setup_paging;
+    $self->setup_ordering;
     return $self->limit_collection;
 }
 
diff --git a/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm b/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
index 9a7756c..b4e606c 100644
--- a/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection/QueryByJSON.pm
@@ -66,17 +66,6 @@ sub limit_collection {
         );
     }
 
-    my @orderby_cols;
-    my @orders = $self->request->param('order');
-    foreach my $orderby ($self->request->param('orderby')) {
-        my $order = shift @orders || 'ASC';
-        $order = uc($order);
-        $order = 'ASC' unless $order eq 'DESC';
-        push @orderby_cols, {FIELD => $orderby, ORDER => $order};
-    }
-    $self->collection->OrderByCols(@orderby_cols)
-        if @orderby_cols;
-
     return 1;
 }
 
diff --git a/lib/RT/Extension/REST2/Resource/Tickets.pm b/lib/RT/Extension/REST2/Resource/Tickets.pm
index f060d49..1398d28 100644
--- a/lib/RT/Extension/REST2/Resource/Tickets.pm
+++ b/lib/RT/Extension/REST2/Resource/Tickets.pm
@@ -46,25 +46,13 @@ sub allowed_methods {
     [ 'GET', 'HEAD', 'POST' ]
 }
 
-sub limit_collection {
+override 'limit_collection' => sub {
     my $self = shift;
     my ($ok, $msg) = $self->collection->FromSQL( $self->query );
     return error_as_json( $self->response, 0, $msg ) unless $ok;
-
-    my @orderby_cols;
-    my @orders = $self->request->param('order');
-    foreach my $orderby ($self->request->param('orderby')) {
-        $orderby = decode_utf8($orderby);
-        my $order = shift @orders || 'ASC';
-        $order = uc(decode_utf8($order));
-        $order = 'ASC' unless $order eq 'DESC';
-        push @orderby_cols, {FIELD => $orderby, ORDER => $order};
-    }
-    $self->collection->OrderByCols(@orderby_cols)
-        if @orderby_cols;
-
+    super();
     return 1;
-}
+};
 
 sub expand_field {
     my $self         = shift;

commit 2c9b41c00269c02552a5dd38b431f2fb7662350e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue May 25 23:05:27 2021 +0800

    Abstract serialize_record to easily subclass for collection
    
    This is initially for searches, which are stored in attributes, but we
    don't want to show them as normal attributes.

diff --git a/lib/RT/Extension/REST2/Resource/Collection.pm b/lib/RT/Extension/REST2/Resource/Collection.pm
index c27df40..7c74596 100644
--- a/lib/RT/Extension/REST2/Resource/Collection.pm
+++ b/lib/RT/Extension/REST2/Resource/Collection.pm
@@ -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 format_datetime );
+use RT::Extension::REST2::Util qw( expand_uid format_datetime );
 use POSIX qw( ceil );
 use Encode;
 
@@ -84,7 +84,7 @@ sub serialize {
     my @fields = defined $self->request->param('fields') ? split(/,/, $self->request->param('fields')) : ();
 
     while (my $item = $collection->Next) {
-        my $result = expand_uid( $item->UID );
+        my $result = $self->serialize_record( $item->UID );
 
         # Allow selection of desired fields
         if ($result) {
@@ -154,6 +154,12 @@ sub serialize {
     return \%results;
 }
 
+sub serialize_record {
+    my $self   = shift;
+    my $record = shift;
+    return expand_uid($record);
+}
+
 # XXX TODO: Bulk update via DELETE/PUT on a collection resource?
 
 sub charsets_provided { [ 'utf-8' ] }

commit 39175cafffaa69d2798ea87cd56be9073b73e3e9
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 27 04:48:20 2021 +0800

    Add /searches/ and /search/ endpoits for saved searches

diff --git a/lib/RT/Extension/REST2/Resource/Search.pm b/lib/RT/Extension/REST2/Resource/Search.pm
new file mode 100644
index 0000000..56545f1
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/Search.pm
@@ -0,0 +1,110 @@
+package RT::Extension::REST2::Resource::Search;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'RT::Extension::REST2::Resource::Record';
+with 'RT::Extension::REST2::Resource::Record::Readable',
+    'RT::Extension::REST2::Resource::Record::Hypermedia' =>
+    { -alias => { _self_link => '_default_self_link', hypermedia_links => '_default_hypermedia_links' } };
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/search/?$},
+        block => sub { { record_class => 'RT::Attribute' } },
+        ),
+        Path::Dispatcher::Rule::Regex->new(
+            regex => qr{^/search/(.+)/?$},
+            block => sub {
+                my ($match, $req) = @_;
+                my $desc = $match->pos(1);
+                my $record = _load_search($req, $desc);
+
+                return { record_class => 'RT::Attribute', record_id => $record ? $record->Id : 0 };
+            },
+        );
+}
+
+sub _self_link {
+    my $self   = shift;
+    my $result = $self->_default_self_link(@_);
+
+    $result->{type} = 'search';
+    $result->{_url} =~ s!/attribute/!/search/!;
+    return $result;
+}
+
+sub hypermedia_links {
+    my $self = shift;
+    my $links = $self->_default_hypermedia_links;
+    my $record = $self->record;
+    if ( my $content = $record->Content ) {
+        if ( ( $content->{SearchType} || 'Ticket' ) eq 'Ticket' ) {
+            my $id = $record->Id;
+            push @$links,
+                {   _url => RT::Extension::REST2->base_uri . "/tickets?search=$id",
+                    type => 'results',
+                    ref  => 'tickets',
+                };
+        }
+    }
+    return $links;
+}
+
+sub base_uri { join '/', RT::Extension::REST2->base_uri, 'search' }
+
+sub resource_exists {
+    my $self   = shift;
+    my $record = $self->record;
+    return $record->Id && $record->Name =~ /^(?:SavedSearch$|Search -)/;
+}
+
+sub forbidden {
+    my $self = shift;
+    return 0 unless $self->resource_exists;
+    my $search = RT::SavedSearch->new( $self->current_user );
+    return $search->LoadById( $self->record->Id ) ? 0 : 1;
+}
+
+sub _load_search {
+    my $req = shift;
+    my $id  = shift;
+
+    if ( $id =~ /\D/ ) {
+
+        my $attrs = RT::Attributes->new( $req->env->{"rt.current_user"} );
+
+        $attrs->Limit( FIELD => 'Name',        VALUE => 'SavedSearch' );
+        $attrs->Limit( FIELD => 'Name',        VALUE => 'Search -', OPERATOR => 'STARTSWITH' );
+        $attrs->Limit( FIELD => 'Description', VALUE => $id );
+
+        my @searches;
+        while ( my $attr = $attrs->Next ) {
+            my $search = RT::SavedSearch->new( $req->env->{"rt.current_user"} );
+            if ( $search->LoadById( $attr->Id ) ) {
+                push @searches, $search;
+            }
+        }
+
+        my $record_id;
+        if (@searches) {
+            if ( @searches > 1 ) {
+                RT->Logger->warning("Found multiple searches with description $id");
+            }
+            return $searches[0];
+        }
+    }
+    else {
+        my $search = RT::SavedSearch->new( $req->env->{"rt.current_user"} );
+        if ( $search->LoadById($id) ) {
+            return $search;
+        }
+    }
+    return;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/RT/Extension/REST2/Resource/Searches.pm b/lib/RT/Extension/REST2/Resource/Searches.pm
new file mode 100644
index 0000000..3e0b7a1
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/Searches.pm
@@ -0,0 +1,93 @@
+package RT::Extension::REST2::Resource::Searches;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'RT::Extension::REST2::Resource::Collection';
+with 'RT::Extension::REST2::Resource::Collection::ProcessPOSTasGET',
+    'RT::Extension::REST2::Resource::Collection::QueryByJSON';
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/searches/?$},
+        block => sub { { collection_class => 'RT::Attributes' } },
+    )
+}
+
+use Encode qw( decode_utf8 );
+use RT::Extension::REST2::Util qw( error_as_json );
+use RT::Search::Simple;
+
+sub allowed_methods {
+    [ 'GET', 'HEAD', 'POST' ]
+}
+
+sub limit_collection {
+    my $self = shift;
+    my @objects = RT::SavedSearch->new($self->current_user)->ObjectsForLoading;
+    if ( $self->current_user->HasRight( Object => $RT::System, Right => 'ShowSavedSearches' ) ) {
+        push @objects, RT::System->new( $self->current_user );
+    }
+
+    my $query       = $self->query;
+    my @fields      = $self->searchable_fields;
+    my %searchable  = map {; $_ => 1 } @fields;
+
+    my @ids;
+    my @attrs;
+    for my $object (@objects) {
+        my $attrs = $object->Attributes;
+        $attrs->Limit( FIELD => 'Name', VALUE => 'SavedSearch' );
+        push @attrs, $attrs;
+    }
+
+    # Default system searches
+    my $attrs = RT::System->new( $self->current_user )->Attributes;
+    $attrs->Limit( FIELD => 'Name', VALUE => 'Search -', OPERATOR => 'STARTSWITH' );
+    push @attrs, $attrs;
+
+    for my $attrs (@attrs) {
+        for my $limit (@$query) {
+            next
+                unless $limit->{field}
+                and $searchable{ $limit->{field} }
+                and defined $limit->{value};
+
+            $attrs->Limit(
+                FIELD => $limit->{field},
+                VALUE => $limit->{value},
+                (   $limit->{operator} ? ( OPERATOR => $limit->{operator} )
+                    : ()
+                ),
+                CASESENSITIVE => ( $limit->{case_sensitive} || 0 ),
+                (   $limit->{entry_aggregator} ? ( ENTRYAGGREGATOR => $limit->{entry_aggregator} )
+                    : ()
+                ),
+            );
+        }
+        push @ids, map { $_->Id } @{ $attrs->ItemsArrayRef };
+    }
+
+    while ( @ids > 1000 ) {
+        my @batch = splice( @ids, 0, 1000 );
+        $self->Limit( FIELD => 'id', VALUE => \@ids, OPERATOR => 'IN' );
+    }
+    $self->collection->Limit( FIELD => 'id', VALUE => \@ids, OPERATOR => 'IN' );
+
+    return 1;
+}
+
+sub serialize_record {
+    my $self   = shift;
+    my $record = shift;
+    my $result = $self->SUPER::serialize_record($record);
+    $result->{type} = 'search';
+    $result->{_url} =~ s!/attribute/!/search/!;
+    return $result;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;

commit 133f561e2a22ab87adac3551ea659a32d65532ed
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 27 05:53:09 2021 +0800

    Support to search tickets from saved searches

diff --git a/lib/RT/Extension/REST2/Resource/Collection/Search.pm b/lib/RT/Extension/REST2/Resource/Collection/Search.pm
new file mode 100644
index 0000000..2949bcd
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/Collection/Search.pm
@@ -0,0 +1,161 @@
+package RT::Extension::REST2::Resource::Collection::Search;
+use strict;
+use warnings;
+
+use Moose::Role;
+use namespace::autoclean;
+
+requires 'collection';
+use Regexp::Common qw/delimited/;
+
+around BUILDARGS => sub {
+    my $orig  = shift;
+    my $class = shift;
+
+    my %args = @_;
+
+    if ( my $id = $args{request}->param('search') ) {
+        my $search = RT::Extension::REST2::Resource::Search::_load_search( $args{request}, $id );
+
+        if ( $search && $search->Id ) {
+            if ( !defined $args{query} && !defined $args{request}->param('query') ) {
+                if ( my $query = $search->GetParameter('Query') ) {
+                    $args{request}->parameters->set( query => $query );
+                }
+            }
+
+            if ( !defined $args{order} && !defined $args{request}->param('order') ) {
+                if ( my $order = $search->GetParameter('Order') ) {
+                    $args{request}->parameters->set( order => split /\|/, $order );
+                }
+            }
+
+            if ( !defined $args{orderby} && !defined $args{request}->param('orderby') ) {
+                if ( my $orderby = $search->GetParameter('OrderBy') ) {
+                    $args{request}->parameters->set( orderby => split /\|/, $orderby );
+                }
+            }
+
+            if ( !defined $args{per_page} && !defined $args{request}->param('per_page') ) {
+                if ( my $per_page = $search->GetParameter('RowsPerPage') ) {
+                    $args{request}->parameters->set( per_page => $per_page );
+                }
+            }
+
+            if ( !defined $args{fields} && !defined $args{request}->param('fields') ) {
+                if ( my $format = $search->GetParameter('Format') ) {
+                    my @attrs;
+
+                    # Main logic is copied from share/html/Elements/CollectionAsTable/ParseFormat
+                    while ( $format =~ /($RE{delimited}{-delim=>qq{\'"}}|[{}\w.]+)/go ) {
+                        my $col    = $1;
+                        my $colref = {};
+
+                        if ( $col =~ /^$RE{quoted}$/o ) {
+                            substr( $col, 0,  1 ) = "";
+                            substr( $col, -1, 1 ) = "";
+                            $col =~ s/\\(.)/$1/g;
+                        }
+
+                        while ( $col =~ s{/(STYLE|CLASS|TITLE|ALIGN|SPAN|ATTRIBUTE):([^/]*)}{}i ) {
+                            $colref->{ lc $1 } = $2;
+                        }
+
+                        unless ( length $col ) {
+                            $colref->{'attribute'} = '' unless defined $colref->{'attribute'};
+                        }
+                        elsif ( $col =~ /^__(NEWLINE|NBSP)__$/ || $col =~ /^(NEWLINE|NBSP)$/ ) {
+                            $colref->{'attribute'} = '';
+                        }
+                        elsif ( $col =~ /__(.*?)__/io ) {
+                            while ( $col =~ s/^(.*?)__(.*?)__//o ) {
+                                $colref->{'last_attribute'} = $2;
+                            }
+                            $colref->{'attribute'} = $colref->{'last_attribute'}
+                                unless defined $colref->{'attribute'};
+                        }
+                        else {
+                            $colref->{'attribute'} = $col
+                                unless defined $colref->{'attribute'};
+                        }
+
+                        if ( $colref->{'attribute'} ) {
+                            push @attrs, $colref->{'attribute'};
+                        }
+                    }
+
+                    my %fields;
+
+                    if (@attrs) {
+                        my $record_class = $args{collection_class}->RecordClass;
+                        while ( my $attr = shift @attrs ) {
+                            if ( $attr =~ /^(Requestors?|AdminCc|Cc|CustomRole\.\{.+?\})(?:\.(.+))?/ ) {
+                                my $role  = $1;
+                                my $field = $2;
+
+                                if ( $role eq 'Requestors' ) {
+                                    $role = 'Requestor';
+                                }
+                                elsif ( $role =~ /^CustomRole\.\{(.+?)\}/ ) {
+                                    my $name        = $1;
+                                    my $custom_role = RT::CustomRole->new( $args{request}->env->{"rt.current_user"} );
+                                    $custom_role->Load($name);
+                                    if ( $custom_role->Id ) {
+                                        $role = $custom_role->GroupType;
+                                    }
+                                    else {
+                                        next;
+                                    }
+                                }
+
+                                $fields{$role} = 1;
+                                if ($field) {
+                                    $field = 'CustomFields' if $field =~ /^CustomField\./;
+                                    $args{request}->parameters->set(
+                                        "fields[$role]" => join ',',
+                                        $field,
+                                        $args{request}->parameters->get("fields[$role]") || ()
+                                    );
+                                }
+                            }
+                            elsif ( $attr =~ /^CustomField\./ ) {
+                                $fields{CustomFields} = 1;
+                            }
+                            elsif ( $attr
+                                =~ /^(?:RefersTo|ReferredToBy|DependsOn|DependedOnBy|MemberOf|Members|Parents|Children)$/
+                                )
+                            {
+                                $fields{_hyperlinks} = 1;
+                            }
+                            elsif ( $record_class->can('_Accessible') && $record_class->_Accessible( $attr => 'read' ) )
+                            {
+                                $fields{$attr} = 1;
+                            }
+                            elsif ( $attr =~ s/Relative$// ) {
+
+                                # Date fields like LastUpdatedRelative
+                                push @attrs, $attr;
+                            }
+                            elsif ( $attr =~ s/Name$// ) {
+
+                                # Fields like OwnerName, QueueName
+                                push @attrs, $attr;
+                                $args{request}->parameters->set(
+                                    "fields[$attr]" => join ',',
+                                    'Name',
+                                    $args{request}->parameters->get("fields[$attr]") || ()
+                                );
+                            }
+                        }
+                    }
+
+                    $args{request}->parameters->set( 'fields' => join ',', sort keys %fields );
+                }
+            }
+        }
+    }
+
+    return $class->$orig( %args );
+};
+
+1;
diff --git a/lib/RT/Extension/REST2/Resource/Tickets.pm b/lib/RT/Extension/REST2/Resource/Tickets.pm
index 1398d28..34f16c6 100644
--- a/lib/RT/Extension/REST2/Resource/Tickets.pm
+++ b/lib/RT/Extension/REST2/Resource/Tickets.pm
@@ -6,7 +6,8 @@ use Moose;
 use namespace::autoclean;
 
 extends 'RT::Extension::REST2::Resource::Collection';
-with 'RT::Extension::REST2::Resource::Collection::ProcessPOSTasGET';
+with 'RT::Extension::REST2::Resource::Collection::ProcessPOSTasGET',
+    'RT::Extension::REST2::Resource::Collection::Search';
 
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(

commit e4442b52ddd28808f631c2e236b6eaee4ab11c6b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri May 28 03:03:26 2021 +0800

    Test ticket saved searches

diff --git a/xt/searches.t b/xt/searches.t
new file mode 100644
index 0000000..8f3fa7f
--- /dev/null
+++ b/xt/searches.t
@@ -0,0 +1,215 @@
+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;
+
+$user->PrincipalObj->GrantRight( Right => 'ModifySelf' );
+
+my $custom_role = RT::CustomRole->new( RT->SystemUser );
+my ( $ret, $msg ) = $custom_role->Create(
+    Name      => 'Manager',
+    MaxValues => 0,
+);
+ok( $ret, "created custom role: $msg" );
+
+( $ret, $msg ) = $custom_role->AddToObject(1);
+ok( $ret, "added custom role to queue: $msg" );
+my $custom_role_type = $custom_role->GroupType;
+
+my $cf = RT::CustomField->new( RT->SystemUser );
+( $ret, $msg ) = $cf->Create( Name => 'Boss', Type => 'Freeform', LookupType => RT::User->CustomFieldLookupType );
+ok( $ret, "created custom field: $msg" );
+ok( $cf->AddToObject( RT::User->new( RT->SystemUser ) ) );
+
+ok( $user->SetCountry('US') );
+ok( $user->AddCustomFieldValue( Field => $cf, Value => 'root' ) );
+
+my $search1 = RT::SavedSearch->new($user);
+( $ret, $msg ) = $search1->Save(
+    Privacy      => 'RT::User-' . $user->Id,
+    Type         => 'Ticket',
+    Name         => 'My tickets',
+    SearchParams => {
+        RowsPerPage => 50,
+        'Format'    => join( ',',
+            RT->Config->Get('DefaultSearchResultFormat'), '__Requestors.Country__',
+            '__CustomRole.{Manager}.CustomField.{Boss}__' ),
+        'Query' => "Owner = '" . $user->Name . "'",
+    },
+);
+
+ok( $ret, "created $msg" );
+
+my $search2 = RT::SavedSearch->new( RT->SystemUser );
+( $ret, $msg ) = $search2->Save(
+    Privacy      => 'RT::System-1',
+    Type         => 'Ticket',
+    Name         => 'Recently created tickets',
+    SearchParams => {
+        'Format' => RT->Config->Get('DefaultSearchResultFormat'),
+        'Query'  => "Created >= 'yesterday'",
+    },
+);
+
+ok( $ret, "created $msg" );
+
+{
+    my $res = $mech->get( "$rest_base_path/searches", 'Authorization' => $auth, );
+    is( $res->code, 200, 'got /searches' );
+
+    my $content = $mech->json_response;
+    is( $content->{count},             4,  '4 searches' );
+    is( $content->{page},              1,  '1 page' );
+    is( $content->{per_page},          20, '20 per_page' );
+    is( $content->{total},             4,  '4 total' );
+    is( scalar @{ $content->{items} }, 4,  'items count' );
+
+    for my $item ( @{ $content->{items} } ) {
+        ok( $item->{id}, 'search id' );
+        is( $item->{type}, 'search', 'search type' );
+        like( $item->{_url}, qr{$rest_base_path/search/$item->{id}}, 'search url' );
+    }
+
+    is( $content->{items}[-1]{id}, $search1->Id, 'search id' );
+}
+
+{
+    my $res = $mech->get( "$rest_base_path/search/" . $search2->Id, 'Authorization' => $auth, );
+    is( $res->code, 404, 'can not see system search without permission' );
+}
+
+$user->PrincipalObj->GrantRight( Right => 'SuperUser' );
+
+{
+    my $res = $mech->get( "$rest_base_path/search/" . $search2->Id, 'Authorization' => $auth, );
+    is( $res->code, 200, 'can see system search with permission' );
+}
+
+{
+    my $res = $mech->get( "$rest_base_path/searches", 'Authorization' => $auth, );
+    is( $res->code, 200, 'got /searches' );
+
+    my $content = $mech->json_response;
+    is( $content->{count},             5,  '5 searches' );
+    is( $content->{page},              1,  '1 page' );
+    is( $content->{per_page},          20, '20 per_page' );
+    is( $content->{total},             5,  '5 total' );
+    is( scalar @{ $content->{items} }, 5,  'items count' );
+
+    for my $item ( @{ $content->{items} } ) {
+        ok( $item->{id}, 'search id' );
+        is( $item->{type}, 'search', 'search type' );
+        like( $item->{_url}, qr{$rest_base_path/search/$item->{id}}, 'search url' );
+    }
+
+    is( $content->{items}[3]{id}, $search1->Id, 'search id' );
+    is( $content->{items}[4]{id}, $search2->Id, 'search id' );
+}
+
+{
+    my $res = $mech->post_json(
+        "$rest_base_path/searches",
+        [ { field => 'Description', value => 'My tickets' } ],
+        'Authorization' => $auth,
+    );
+    is( $res->code, 200, "got $rest_base_path/searches" );
+
+    my $content = $mech->json_response;
+    is( $content->{count},             1,  '1 search' );
+    is( $content->{page},              1,  '1 page' );
+    is( $content->{per_page},          20, '20 per_page' );
+    is( $content->{total},             1,  '1 total' );
+    is( scalar @{ $content->{items} }, 1,  'items count' );
+
+    is( $content->{items}[0]{id}, $search1->Id, 'search id' );
+}
+
+# Single search
+{
+    my $res = $mech->get( "$rest_base_path/search/" . $search1->Id, 'Authorization' => $auth, );
+    is( $res->code, 200, "got $rest_base_path/search/" . $search1->Id );
+
+    my $content = $mech->json_response;
+    is( $content->{id},          $search1->id,  'id' );
+    is( $content->{Name},        'SavedSearch', 'Name' );
+    is( $content->{Description}, 'My tickets',  'Description' );
+
+    my $links = $content->{_hyperlinks};
+    is( scalar @$links, 2, 'links count' );
+
+    is( $links->[0]{ref},  'self',       'self link ref' );
+    is( $links->[0]{id},   $search1->Id, 'self link id' );
+    is( $links->[0]{type}, 'search',     'self link type' );
+    like( $links->[0]{_url}, qr[$rest_base_path/search/$links->[0]{id}$], 'self link url' );
+
+    is( $links->[1]{ref},  'tickets', 'results link ref' );
+    is( $links->[1]{type}, 'results', 'results link type' );
+    like( $links->[1]{_url}, qr[$rest_base_path/tickets\?search=$content->{id}$], 'results link url' );
+
+    $res = $mech->get( "$rest_base_path/search/My tickets", 'Authorization' => $auth, );
+    is( $res->code, 200, "got $rest_base_path/search/" . $search1->Id );
+    is_deeply( $content, $mech->json_response, 'Access via search name' );
+}
+
+{
+    my $ticket1 = RT::Test->create_ticket(
+        Queue             => 1,
+        Subject           => 'test ticket',
+        Owner             => $user->Id,
+        Requestor         => $user->Id,
+        $custom_role_type => $user->Id
+    );
+    my $ticket2 = RT::Test->create_ticket( Queue => 1, Subject => 'test ticket' );
+    my $res     = $mech->get( "$rest_base_path/tickets?search=" . $search1->Id, 'Authorization' => $auth, );
+    is( $res->code, 200, "got $rest_base_path/tickets?search=" . $search1->Id );
+
+    my $content = $mech->json_response;
+    is( $content->{count},             1,  '1 search' );
+    is( $content->{page},              1,  '1 page' );
+    is( $content->{per_page},          50, '50 per_page' );
+    is( $content->{total},             1,  '1 total' );
+    is( scalar @{ $content->{items} }, 1,  'items count' );
+
+    my $item = $content->{items}[0];
+    is( $item->{id}, $ticket1->Id, 'ticket id' );
+    for my $field ( qw/Requestor Owner Status TimeLeft Subject Priority Created LastUpdated Told Queue/,
+        $custom_role_type )
+    {
+        ok( length $item->{$field}, "$field value not empty" );
+    }
+    is( $item->{Subject},               'test ticket', 'Subject value' );
+    is( $item->{Queue}{Name},           'General',     'Queue name' );
+    is( $item->{Requestor}[0]{id},      $user->Name,   'Requestor id' );
+    is( $item->{Requestor}[0]{Country}, 'US',          'Requestor Country' );
+
+    is( $item->{$custom_role_type}[0]{id},                         $user->Name, 'Manager id' );
+    is( $item->{$custom_role_type}[0]{CustomFields}[0]{name},      'Boss',      'Manager Boss' );
+    is( $item->{$custom_role_type}[0]{CustomFields}[0]{values}[0], 'root',      'Manager Boss name' );
+
+    $res = $mech->get( "$rest_base_path/tickets?search=My tickets", 'Authorization' => $auth, );
+    is( $res->code, 200, "got $rest_base_path/tickets?search=My tickets" );
+    is_deeply( $content, $mech->json_response, 'search tickets via search name' );
+
+    $res = $mech->get( "$rest_base_path/tickets?search=My tickets&per_page=10&fields=id", 'Authorization' => $auth, );
+    is( $res->code, 200, "got $rest_base_path/tickets?search=My tickets" );
+    $content = $mech->json_response;
+    is( $content->{count},             1,  '1 search' );
+    is( $content->{page},              1,  '1 page' );
+    is( $content->{per_page},          10, '10 per_page' );
+    is( $content->{total},             1,  '1 total' );
+    is( scalar @{ $content->{items} }, 1,  'items count' );
+
+    $item = $content->{items}[0];
+    is( $item->{id}, $ticket1->Id, 'ticket id' );
+    for my $field ( qw/Requestor Owner Status TimeLeft Subject Priority Created LastUpdated Told Queue/,
+        $custom_role_type )
+    {
+        ok( !exists $item->{$field}, "$field not exists" );
+    }
+}
+
+done_testing;

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


More information about the Bps-public-commit mailing list