[Bps-public-commit] rt-extension-rest2 branch, master, updated. 1.07-24-g332f0ba

? sunnavy sunnavy at bestpractical.com
Thu Apr 30 18:19:33 EDT 2020


The branch, master has been updated
       via  332f0ba05c5e540574f36a58318d37fb81a7e17e (commit)
       via  8885adf549f74a5784ecfe9dca2acff2e0951937 (commit)
       via  2abbfb703802e6523d7d653adfa7e98337adecc2 (commit)
       via  2e216a5b4f9cf2ec7beb85e8fa9af0058251ec2c (commit)
       via  df34edf6bbfd8a2c4d89042f90f87f459648d7c3 (commit)
       via  402c856edac2b170ce3ae4ffd36000241696ec13 (commit)
       via  379dc2d39bae1fe4f4f084f0bc18244a0cfa9317 (commit)
       via  9d161b0fac084641d7e7c229c5d1da234950fbf8 (commit)
       via  6ef771c2a3cf07cb77d62bd9941586ebadde01c6 (commit)
      from  009714390440a228ba6c684c4d1c5e48fb8dd37e (commit)

Summary of changes:
 README                                             |  43 ++-
 lib/RT/Extension/REST2.pm                          |  44 ++-
 lib/RT/Extension/REST2/Resource/CustomField.pm     |  50 ++-
 .../Extension/REST2/Resource/CustomFieldValue.pm   | 104 ++++++
 .../Extension/REST2/Resource/CustomFieldValues.pm  |  66 ++++
 lib/RT/Extension/REST2/Resource/CustomFields.pm    |  27 ++
 lib/RT/Extension/REST2/Util.pm                     |  13 +
 xt/customfields.t                                  | 371 +++++++++++++++++++++
 xt/customfieldvalues.t                             | 231 +++++++++++++
 xt/local-custom-fields.t                           |  85 +++++
 10 files changed, 1030 insertions(+), 4 deletions(-)
 create mode 100644 lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
 create mode 100644 lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
 create mode 100644 xt/customfields.t
 create mode 100644 xt/customfieldvalues.t
 create mode 100644 xt/local-custom-fields.t

- Log -----------------------------------------------------------------
commit 6ef771c2a3cf07cb77d62bd9941586ebadde01c6
Author: gibus <gibus at easter-eggs.com>
Date:   Thu May 24 16:21:11 2018 +0200

    Add available values for Select RT::CustomField
    
    Also filter values of Select CF with category parameter

diff --git a/README b/README
index 40138e3..ac2f1d8 100644
--- a/README
+++ b/README
@@ -483,7 +483,10 @@ USAGE
             search for custom fields using L</JSON searches> syntax
 
         GET /customfield/:id
-            retrieve a custom field
+            retrieve a custom field, with values if type is Select
+
+        GET /customfield/:id?category=<category name>
+            retrieve a custom field, with values filtered by category if type is Select
 
    Custom Roles
         GET /customroles?query=<JSON>
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index c4b61c6..827f469 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -529,7 +529,10 @@ Below are some examples using the endpoints above.
         search for custom fields using L</JSON searches> syntax
 
     GET /customfield/:id
-        retrieve a custom field
+        retrieve a custom field, with values if type is Select
+
+    GET /customfield/:id?category=<category name>
+        retrieve a custom field, with values filtered by category if type is Select
 
 =head3 Custom Roles
 
diff --git a/lib/RT/Extension/REST2/Resource/CustomField.pm b/lib/RT/Extension/REST2/Resource/CustomField.pm
index 904424b..ddc9aa7 100644
--- a/lib/RT/Extension/REST2/Resource/CustomField.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomField.pm
@@ -7,6 +7,7 @@ use namespace::autoclean;
 
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable',
+        => { -alias => { serialize => '_default_serialize' } },
      'RT::Extension::REST2::Resource::Record::Hypermedia';
 
 sub dispatch_rules {
@@ -20,6 +21,21 @@ sub dispatch_rules {
     )
 }
 
+sub serialize {
+    my $self = shift;
+    my $data = $self->_default_serialize(@_);
+
+    if ($data->{Values}) {
+        if ($self->record->BasedOn && defined $self->request->param('category')) {
+            my $category = $self->request->param('category') || '';
+            @{$data->{Values}} = grep {$_->{category} eq $category} @{$data->{Values}};
+        }
+        @{$data->{Values}} = map {$_->{name}} @{$data->{Values}};
+    }
+
+    return $data;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/lib/RT/Extension/REST2/Util.pm b/lib/RT/Extension/REST2/Util.pm
index e814c1c..9ae1680 100644
--- a/lib/RT/Extension/REST2/Util.pm
+++ b/lib/RT/Extension/REST2/Util.pm
@@ -91,6 +91,19 @@ sub serialize_record {
         }
     }
 
+    # Add available values for Select RT::CustomField
+    if (ref($record) eq 'RT::CustomField' && $record->Type eq 'Select') {
+        my $values = $record->Values;
+        while (my $val = $values->Next) {
+            my $category = $record->BasedOn ? $val->Category : '';
+            if (exists $data{Values}) {
+                push @{$data{Values}}, {name => $val->Name, category => $category};
+            } else {
+                $data{Values} = [{name => $val->Name, category => $category}];
+            }
+        }
+    }
+
     # Replace UIDs with object placeholders
     for my $uid (grep ref eq 'SCALAR', values %data) {
         $uid = expand_uid($uid);

commit 9d161b0fac084641d7e7c229c5d1da234950fbf8
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Sep 25 22:49:45 2018 +0200

    Add tests for CustomField

diff --git a/xt/customfields.t b/xt/customfields.t
new file mode 100644
index 0000000..13d75fa
--- /dev/null
+++ b/xt/customfields.t
@@ -0,0 +1,312 @@
+use strict;
+use warnings;
+use lib 't/lib';
+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 $freeform_cf = RT::CustomField->new(RT->SystemUser);
+$freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
+my $freeform_cf_id = $freeform_cf->id;
+
+my $select_cf = RT::CustomField->new(RT->SystemUser);
+$select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1, Queue => 'General');
+$select_cf->AddValue(Name => 'First Value', SortOder => 0);
+$select_cf->AddValue(Name => 'Second Value', SortOrder => 1);
+$select_cf->AddValue(Name => 'Third Value', SortOrder => 2);
+my $select_cf_id = $select_cf->id;
+my $select_cf_values = $select_cf->Values->ItemsArrayRef;
+
+my $basedon_cf = RT::CustomField->new(RT->SystemUser);
+$basedon_cf->Create(Name => 'SubSelect CF', Type => 'Select', MaxValues => 1, Queue => 'General', BasedOn => $select_cf->id);
+$basedon_cf->AddValue(Name => 'With First Value', Category => $select_cf_values->[0]->Name, SortOder => 0);
+$basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
+my $basedon_cf_id = $basedon_cf->id;
+my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
+
+# Right test - search all tickets customfields without SeeCustomField
+{
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 0);
+    is_deeply($content->{items}, []);
+}
+
+# search all tickets customfields
+{
+    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
+
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 3);
+    my $items = $content->{items};
+    is(scalar(@$items), 3);
+
+    is($items->[0]->{type}, 'customfield');
+    is($items->[0]->{id}, $freeform_cf->id);
+    like($items->[0]->{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
+
+    is($items->[1]->{type}, 'customfield');
+    is($items->[1]->{id}, $select_cf->id);
+    like($items->[1]->{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
+
+    is($items->[2]->{type}, 'customfield');
+    is($items->[2]->{id}, $basedon_cf->id);
+    like($items->[2]->{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+}
+
+# Freeform CustomField display
+{
+    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $freeform_cf_id);
+    is($content->{Name}, $freeform_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Freeform');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $freeform_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
+}
+
+# Select CustomField display
+{
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $select_cf_id);
+    is($content->{Name}, $select_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $select_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['First Value', 'Second Value', 'Third Value']);
+}
+
+# BasedOn CustomField display
+{
+    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $basedon_cf_id);
+    is($content->{Name}, $basedon_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $basedon_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['With First Value', 'With No Value']);
+}
+
+# BasedOn CustomField display with category filter
+{
+    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id?category=First%20Value",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $basedon_cf_id);
+    is($content->{Name}, $basedon_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $basedon_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['With First Value']);
+}
+
+# BasedOn CustomField display with null category filter
+{
+    my $res = $mech->get("$rest_base_path/customfield/$basedon_cf_id?category=",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $basedon_cf_id);
+    is($content->{Name}, $basedon_cf->Name);
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Select');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $basedon_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
+
+    my $values = $content->{Values};
+    is_deeply($values, ['With No Value']);
+}
+
+# Display customfield
+{
+    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
+
+    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+    is($content->{id}, $freeform_cf_id);
+    is($content->{Name}, 'Freeform CF');
+    is($content->{Description}, '');
+    is($content->{LookupType}, 'RT::Queue-RT::Ticket');
+    is($content->{Type}, 'Freeform');
+    is($content->{MaxValues}, 1);
+    is($content->{Disabled}, 0);
+
+    my @fields = qw(SortOrder Pattern Created Creator LastUpdated LastUpdatedBy);
+    push @fields, qw(UniqueValues EntryHint) if RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+    ok(exists $content->{$_}, "got $_") for @fields;
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 1);
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $freeform_cf_id);
+    is($links->[0]{type}, 'customfield');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$freeform_cf_id$});
+}
+
+# Right test - update customfield without AdminCustomField
+{
+    $user->PrincipalObj->RevokeRight( Right => 'AdminCustomField' );
+
+    my $payload = {
+        Description  => 'This is a CF for testing REST CRUD on CFs',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$freeform_cf_id",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+    is($res->message, 'Forbidden');
+}
+
+# Update customfield
+{
+    $user->PrincipalObj->GrantRight( Right => 'AdminCustomField' );
+
+    my $payload = {
+        Description  => 'This is a CF for testing REST CRUD on CFs',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$freeform_cf_id",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->id, $freeform_cf_id);
+    is($freeform_cf->Description, 'This is a CF for testing REST CRUD on CFs');
+}
+
+# Right test - delete customfield without AdminCustomField
+{
+    $user->PrincipalObj->RevokeRight( Right => 'AdminCustomField' );
+
+    my $res = $mech->delete("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+    is($res->message, 'Forbidden');
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->Disabled, 0);
+}
+
+# Delete customfield
+{
+    $user->PrincipalObj->GrantRight( Right => 'AdminCustomField' );
+
+    my $res = $mech->delete("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 204);
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->Disabled, 1);
+}
+
+done_testing;

commit 379dc2d39bae1fe4f4f084f0bc18244a0cfa9317
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Oct 8 13:28:33 2018 +0200

    Add all CRUD operations for CustomField

diff --git a/README b/README
index ac2f1d8..12ba48b 100644
--- a/README
+++ b/README
@@ -482,12 +482,21 @@ USAGE
         POST /customfields
             search for custom fields using L</JSON searches> syntax
 
+        POST /customfield
+            create a customfield; provide JSON content
+
         GET /customfield/:id
             retrieve a custom field, with values if type is Select
 
         GET /customfield/:id?category=<category name>
             retrieve a custom field, with values filtered by category if type is Select
 
+        PUT /customfield/:id
+            update a custom field's metadata; provide JSON content
+
+        DELETE /customfield/:id
+            disable customfield
+
    Custom Roles
         GET /customroles?query=<JSON>
         POST /customroles
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 827f469..8723db8 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -528,12 +528,21 @@ Below are some examples using the endpoints above.
     POST /customfields
         search for custom fields using L</JSON searches> syntax
 
+    POST /customfield
+        create a customfield; provide JSON content
+
     GET /customfield/:id
         retrieve a custom field, with values if type is Select
 
     GET /customfield/:id?category=<category name>
         retrieve a custom field, with values filtered by category if type is Select
 
+    PUT /customfield/:id
+        update a custom field's metadata; provide JSON content
+
+    DELETE /customfield/:id
+        disable customfield
+
 =head3 Custom Roles
 
     GET /customroles?query=<JSON>
diff --git a/lib/RT/Extension/REST2/Resource/CustomField.pm b/lib/RT/Extension/REST2/Resource/CustomField.pm
index ddc9aa7..bd927c6 100644
--- a/lib/RT/Extension/REST2/Resource/CustomField.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomField.pm
@@ -8,7 +8,9 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable',
         => { -alias => { serialize => '_default_serialize' } },
-     'RT::Extension::REST2::Resource::Record::Hypermedia';
+     'RT::Extension::REST2::Resource::Record::Hypermedia',
+     'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
+     'RT::Extension::REST2::Resource::Record::Writable';
 
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(
@@ -36,6 +38,21 @@ sub serialize {
     return $data;
 }
 
+sub forbidden {
+    my $self = shift;
+    my $method = $self->request->method;
+    if ($self->record->id) {
+        if ($method eq 'GET') {
+            return !$self->record->CurrentUserHasRight('SeeCustomField');
+        } else {
+            return !($self->record->CurrentUserHasRight('SeeCustomField') && $self->record->CurrentUserHasRight('AdminCustomField'));
+        }
+    } else {
+        return !$self->current_user->HasRight(Right => "AdminCustomField", Object => RT->System);
+    }
+    return 0;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;

commit 402c856edac2b170ce3ae4ffd36000241696ec13
Author: gibus <gibus at easter-eggs.com>
Date:   Mon Oct 8 13:29:11 2018 +0200

    Add tests for all CRUD operations for CustomField

diff --git a/xt/customfields.t b/xt/customfields.t
index 13d75fa..69ab19a 100644
--- a/xt/customfields.t
+++ b/xt/customfields.t
@@ -8,10 +8,6 @@ my $auth = RT::Extension::REST2::Test->authorization_header;
 my $rest_base_path = '/REST/2.0';
 my $user = RT::Extension::REST2::Test->user;
 
-my $freeform_cf = RT::CustomField->new(RT->SystemUser);
-$freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
-my $freeform_cf_id = $freeform_cf->id;
-
 my $select_cf = RT::CustomField->new(RT->SystemUser);
 $select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1, Queue => 'General');
 $select_cf->AddValue(Name => 'First Value', SortOder => 0);
@@ -27,8 +23,58 @@ $basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
 my $basedon_cf_id = $basedon_cf->id;
 my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
 
+my $freeform_cf;
+my $freeform_cf_id;
+
+# Right test - create customfield without SeeCustomField nor AdminCustomField
+{
+    my $payload = {
+        Name      => 'Freeform CF',
+        Type      => 'Freeform',
+        MaxValues => 1,
+    };
+    my $res = $mech->post_json("$rest_base_path/customfield",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+    is($res->message, 'Forbidden');
+
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    my ($ok, $msg) = $freeform_cf->Load('Freeform CF');
+    is($freeform_cf->id, undef);
+    ok(!$ok);
+    is($msg, 'Not found');
+}
+
+# Customfield create
+{
+    $user->PrincipalObj->GrantRight( Right => 'SeeCustomField' );
+    $user->PrincipalObj->GrantRight( Right => 'AdminCustomField' );
+    my $payload = {
+        Name       => 'Freeform CF',
+        Type       => 'Freeform',
+        LookupType => 'RT::Queue-RT::Ticket',
+        MaxValues  => 1,
+    };
+    my $res = $mech->post_json("$rest_base_path/customfield",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Load('Freeform CF');
+    $freeform_cf_id = $freeform_cf->id;
+    is($freeform_cf->id, 4);
+    is($freeform_cf->Description, '');
+}
+
+
 # Right test - search all tickets customfields without SeeCustomField
 {
+    $user->PrincipalObj->RevokeRight( Right => 'SeeCustomField' );
+
     my $res = $mech->post_json("$rest_base_path/customfields",
         [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
         'Authorization' => $auth,

commit df34edf6bbfd8a2c4d89042f90f87f459648d7c3
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 13:55:53 2018 +0200

    Add customfieldvalue(s) endpoints

diff --git a/README b/README
index 12ba48b..108469a 100644
--- a/README
+++ b/README
@@ -497,6 +497,23 @@ USAGE
         DELETE /customfield/:id
             disable customfield
 
+   Custom Field Values
+        GET /customfield/:id/values?query=<JSON>
+        POST /customfield/:id/values
+            search for values of a custom field  using L</JSON searches> syntax
+
+        POST /customfield/:id/value
+            add a value to a custom field; provide JSON content
+
+        GET /customfield/:id/value/:id
+            retrieve a value of a custom field
+
+        PUT /customfield/:id/value/:id
+            update a value of a custom field; provide JSON content
+
+        DELETE /customfield/:id/value/:id
+            remove a value from a custom field
+
    Custom Roles
         GET /customroles?query=<JSON>
         POST /customroles
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 8723db8..7b17587 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -543,6 +543,24 @@ Below are some examples using the endpoints above.
     DELETE /customfield/:id
         disable customfield
 
+=head3 Custom Field Values
+
+    GET /customfield/:id/values?query=<JSON>
+    POST /customfield/:id/values
+        search for values of a custom field  using L</JSON searches> syntax
+
+    POST /customfield/:id/value
+        add a value to a custom field; provide JSON content
+
+    GET /customfield/:id/value/:id
+        retrieve a value of a custom field
+
+    PUT /customfield/:id/value/:id
+        update a value of a custom field; provide JSON content
+
+    DELETE /customfield/:id/value/:id
+        remove a value from a custom field
+
 =head3 Custom Roles
 
     GET /customroles?query=<JSON>
diff --git a/lib/RT/Extension/REST2/Resource/CustomField.pm b/lib/RT/Extension/REST2/Resource/CustomField.pm
index bd927c6..399c5b5 100644
--- a/lib/RT/Extension/REST2/Resource/CustomField.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomField.pm
@@ -8,7 +8,8 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Record';
 with 'RT::Extension::REST2::Resource::Record::Readable',
         => { -alias => { serialize => '_default_serialize' } },
-     'RT::Extension::REST2::Resource::Record::Hypermedia',
+     'RT::Extension::REST2::Resource::Record::Hypermedia'
+        => { -alias => { hypermedia_links => '_default_hypermedia_links' } },
      'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
      'RT::Extension::REST2::Resource::Record::Writable';
 
@@ -53,8 +54,20 @@ sub forbidden {
     return 0;
 }
 
+sub hypermedia_links {
+    my $self = shift;
+    my $links = $self->_default_hypermedia_links(@_);
+
+    if ($self->record->IsSelectionType) {
+        push @$links, {
+            ref  => 'customfieldvalues',
+            _url => RT::Extension::REST2->base_uri . "/customfield/" . $self->record->id . "/values",
+        };
+    }
+    return $links;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;
 
-
diff --git a/lib/RT/Extension/REST2/Resource/CustomFieldValue.pm b/lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
new file mode 100644
index 0000000..6901f04
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/CustomFieldValue.pm
@@ -0,0 +1,104 @@
+package RT::Extension::REST2::Resource::CustomFieldValue;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+use RT::Extension::REST2::Util qw(expand_uid);
+
+extends 'RT::Extension::REST2::Resource::Record';
+with 'RT::Extension::REST2::Resource::Record::Readable',
+     'RT::Extension::REST2::Resource::Record::Hypermedia',
+     'RT::Extension::REST2::Resource::Record::Deletable',
+     'RT::Extension::REST2::Resource::Record::Writable';
+
+has 'customfield' => (
+    is  => 'ro',
+    isa => 'RT::CustomField',
+);
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/customfield/(\d+)/value/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $cf_id = $match->pos(1);
+            my $cf = RT::CustomField->new($req->env->{"rt.current_user"});
+            $cf->Load($cf_id);
+            return { record_class => 'RT::CustomFieldValue', customfield => $cf }
+        },
+    ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/customfield/(\d+)/value/(\d+)/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $cf_id = $match->pos(1);
+            my $cf = RT::CustomField->new($req->env->{"rt.current_user"});
+            $cf->Load($cf_id);
+            return { record_class => 'RT::CustomFieldValue', record_id => shift->pos(2), customfield => $cf }
+        },
+    )
+}
+
+sub forbidden {
+    my $self = shift;
+    my $method = $self->request->method;
+    if ($method eq 'GET') {
+        return !$self->customfield->CurrentUserHasRight('SeeCustomField');
+    } else {
+        return !($self->customfield->CurrentUserHasRight('AdminCustomField') ||$self->customfield->CurrentUserHasRight('AdminCustomFieldValues'));
+    }
+}
+
+sub create_record {
+    my $self = shift;
+    my $data = shift;
+
+    my ($ok, $msg) = $self->customfield->AddValue(%$data);
+    $self->record->Load($ok) if $ok;
+    return ($ok, $msg);
+}
+
+sub delete_resource {
+    my $self = shift;
+
+    my ($ok, $msg) = $self->customfield->DeleteValue($self->record->id);
+    return $ok;
+}
+
+sub hypermedia_links {
+    my $self = shift;
+    my $record = $self->record;
+    my $cf = $self->customfield;
+
+    my $class = blessed($record);
+    $class =~ s/^RT:://;
+    $class = lc $class;
+    my $id = $record->id;
+
+    my $cf_class = blessed($cf);
+    $cf_class =~ s/^RT:://;
+    $cf_class = lc $cf_class;
+    my $cf_id = $cf->id;
+
+    my $cf_entry = expand_uid($cf->UID);
+
+    my $links = [
+        {
+            ref  => 'self',
+            type => $class,
+            id   => $id,
+            _url => RT::Extension::REST2->base_uri . "/$cf_class/$cf_id/$class/$id",
+        },
+        {
+            %$cf_entry,
+            ref  => 'customfield',
+        },
+    ];
+
+    return $links;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/RT/Extension/REST2/Resource/CustomFieldValues.pm b/lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
new file mode 100644
index 0000000..7fd94c8
--- /dev/null
+++ b/lib/RT/Extension/REST2/Resource/CustomFieldValues.pm
@@ -0,0 +1,66 @@
+package RT::Extension::REST2::Resource::CustomFieldValues;
+use strict;
+use warnings;
+
+use Moose;
+use namespace::autoclean;
+
+extends 'RT::Extension::REST2::Resource::Collection';
+with 'RT::Extension::REST2::Resource::Collection::QueryByJSON';
+
+has 'customfield' => (
+    is  => 'ro',
+    isa => 'RT::CustomField',
+);
+
+sub dispatch_rules {
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/customfield/(\d+)/values/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $cf_id = $match->pos(1);
+            my $cf = RT::CustomField->new($req->env->{"rt.current_user"});
+            $cf->Load($cf_id);
+            my $values = $cf->Values;
+            return { customfield => $cf, collection => $values }
+        },
+    )
+}
+
+sub forbidden {
+    my $self = shift;
+    my $method = $self->request->method;
+    if ($method eq 'GET') {
+        return !$self->customfield->CurrentUserHasRight('SeeCustomField');
+    } else {
+        return !($self->customfield->CurrentUserHasRight('AdminCustomField') ||$self->customfield->CurrentUserHasRight('AdminCustomFieldValues'));
+    }
+}
+
+sub serialize {
+    my $self = shift;
+    my $collection = $self->collection;
+    my $cf = $self->customfield;
+    my @results;
+
+    while (my $item = $collection->Next) {
+        my $result = {
+            type => 'customfieldvalue',
+            id   => $item->id,
+            name   => $item->Name,
+            _url => RT::Extension::REST2->base_uri . "/customfield/" . $cf->id . '/value/' . $item->id,
+        };
+        push @results, $result;
+    }
+    return {
+        count       => scalar(@results)         + 0,
+        total       => $collection->CountAll    + 0,
+        per_page    => $collection->RowsPerPage + 0,
+        page        => ($collection->FirstRow / $collection->RowsPerPage) + 1,
+        items       => \@results,
+    };
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;

commit 2e216a5b4f9cf2ec7beb85e8fa9af0058251ec2c
Author: gibus <gibus at easter-eggs.com>
Date:   Tue Oct 9 13:56:57 2018 +0200

    Add tests for customfieldvalue(s) REST2 API endpoints

diff --git a/xt/customfields.t b/xt/customfields.t
index 69ab19a..238606f 100644
--- a/xt/customfields.t
+++ b/xt/customfields.t
@@ -163,12 +163,15 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $select_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
 
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$select_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['First Value', 'Second Value', 'Third Value']);
 }
@@ -193,12 +196,15 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $basedon_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
 
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['With First Value', 'With No Value']);
 }
@@ -223,12 +229,15 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $basedon_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
 
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['With First Value']);
 }
@@ -253,12 +262,16 @@ my $freeform_cf_id;
     ok(exists $content->{$_}, "got $_") for @fields;
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $basedon_cf_id);
     is($links->[0]{type}, 'customfield');
     like($links->[0]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id$});
 
+
+    is($links->[1]{ref}, 'customfieldvalues');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/values$});
+
     my $values = $content->{Values};
     is_deeply($values, ['With No Value']);
 }
diff --git a/xt/customfieldvalues.t b/xt/customfieldvalues.t
new file mode 100644
index 0000000..9f9d5e1
--- /dev/null
+++ b/xt/customfieldvalues.t
@@ -0,0 +1,231 @@
+use strict;
+use warnings;
+use lib 't/lib';
+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 $select_cf = RT::CustomField->new(RT->SystemUser);
+$select_cf->Create(Name => 'Select CF', Type => 'Select', MaxValues => 1);
+$select_cf->AddValue(Name => 'First Value', SortOrder => 0);
+$select_cf->AddValue(Name => 'Second Value', SortOrder => 1);
+$select_cf->AddValue(Name => 'Third Value', SortOrder => 2);
+my $select_cf_id = $select_cf->id;
+my $select_cf_values = $select_cf->Values->ItemsArrayRef;
+
+my $basedon_cf = RT::CustomField->new(RT->SystemUser);
+$basedon_cf->Create(Name => 'SubSelect CF', Type => 'Select', MaxValues => 1, BasedOn => $select_cf->id);
+$basedon_cf->AddValue(Name => 'With First Value', Category => $select_cf_values->[0]->Name, SortOder => 0);
+$basedon_cf->AddValue(Name => 'With No Value', SortOder => 0);
+my $basedon_cf_id = $basedon_cf->id;
+my $basedon_cf_values = $basedon_cf->Values->ItemsArrayRef;
+
+# Right test - retrieve all values without SeeCustomField
+{
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id/values",
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+}
+
+$user->PrincipalObj->GrantRight(Right => 'SeeCustomField');
+
+# Retrieve customfield's hypermedia link for customfieldvalues
+{
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    my $links = $content->{_hyperlinks};
+    my @cfvs_links = grep { $_->{ref} eq 'customfieldvalues' } @$links;
+    is(scalar(@cfvs_links), 1);
+    like($cfvs_links[0]->{_url}, qr{$rest_base_path/customfield/$select_cf_id/values$});
+}
+
+# No customfieldvalues hypermedia link for non-select customfield
+{
+    my $freeform_cf = RT::CustomField->new(RT->SystemUser);
+    $freeform_cf->Create(Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
+    my $freeform_cf_id = $freeform_cf->id;
+
+    my $res = $mech->get("$rest_base_path/customfield/$freeform_cf_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    my $links = $content->{_hyperlinks};
+    my @cfvs_links = grep { $_->{ref} eq 'customfieldvalues' } @$links;
+    is(scalar(@cfvs_links), 0);
+}
+
+# Retrieve all values
+{
+
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id/values",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 3);
+    my $items = $content->{items};
+    is(scalar(@$items), 3);
+
+    for (my $i=0; $i < scalar @$items; $i++) {
+        my $cf_value_id = $select_cf_values->[$i]->id;
+        is($items->[$i]->{type}, 'customfieldvalue');
+        is($items->[$i]->{id}, $cf_value_id);
+        is($items->[$i]->{name}, $select_cf_values->[$i]->Name);
+        like($items->[$i]->{_url}, qr{$rest_base_path/customfield/$select_cf_id/value/$cf_value_id$});
+    }
+}
+
+# Right test - udpate a value without AdminCustomFieldValues nor AdminCustomField
+{
+    my $payload = {
+        Name => 'Third and Last Value',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 403);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is($select_cf_values->[-1]->Name, 'Third Value');
+}
+
+# Right test - udpate a value without AdminCustomFieldValues but with AdminCustomField
+{
+    $user->PrincipalObj->GrantRight(Right => 'AdminCustomField');
+
+    my $payload = {
+        Name => 'Third and Last Value',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is($select_cf_values->[-1]->Name, 'Third and Last Value');
+}
+
+# Right test - udpate a value without AdminCustomField but with AdminCustomFieldValues
+{
+    $user->PrincipalObj->RevokeRight(Right => 'AdminCustomField');
+    $user->PrincipalObj->GrantRight(Right => 'AdminCustomFieldValues', Object => $select_cf);
+    $user->PrincipalObj->GrantRight(Right => 'AdminCustomFieldValues', Object => $basedon_cf);
+
+    my $payload = {
+        Name => 'Third and Last but NOT Least Value',
+    };
+    my $res = $mech->put_json("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is($select_cf_values->[-1]->Name, 'Third and Last but NOT Least Value');
+}
+
+# Add a value
+{
+    my $payload = {
+        Name      => 'Fourth Value',
+        SortOrder => 3,
+    };
+    my $res = $mech->post_json("$rest_base_path/customfield/$select_cf_id/value",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is(scalar(@$select_cf_values), 4);
+    is($select_cf_values->[-1]->Name, 'Fourth Value');
+}
+
+# Retrieve a value
+{
+    my $cfv = $select_cf_values->[-2];
+    my $cfv_id = $cfv->id;
+    my $res = $mech->get("$rest_base_path/customfield/$select_cf_id/value/$cfv_id",
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    foreach my $field (qw/id Name Description SortOrder Category/) {
+        is($content->{$field}, $cfv->$field);
+    }
+
+    ok(exists $content->{$_}, "got $_") for qw/Created Creator LastUpdated LastUpdatedBy/;
+
+    is($content->{CustomField}->{id}, $select_cf_id);
+
+    my $links = $content->{_hyperlinks};
+    is(scalar @$links, 2);
+
+    is($links->[0]{ref}, 'self');
+    is($links->[0]{id}, $cfv_id);
+    is($links->[0]{type}, 'customfieldvalue');
+    like($links->[0]{_url}, qr{$rest_base_path/customfield/$select_cf_id/customfieldvalue/$cfv_id$});
+
+    is($links->[1]{ref}, 'customfield');
+    is($links->[1]{id}, $select_cf_id);
+    is($links->[1]{type}, 'customfield');
+    like($links->[1]{_url}, qr{$rest_base_path/customfield/$select_cf_id$});
+}
+
+# Retrieve all values filtered by category
+{
+    my $payload = [
+        {
+            field => 'Category',
+            value => $select_cf_values->[0]->Name,
+        }
+    ];
+    my $res = $mech->post_json("$rest_base_path/customfield/$basedon_cf_id/values",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 1);
+    is($content->{count}, 1);
+    my $items = $content->{items};
+    is(scalar(@$items), 1);
+
+    for (my $i=0; $i < scalar @$items; $i++) {
+        my $cf_value_id = $basedon_cf_values->[$i]->id;
+        is($items->[$i]->{type}, 'customfieldvalue');
+        is($items->[$i]->{id}, $cf_value_id);
+        is($items->[$i]->{name}, $basedon_cf_values->[$i]->Name);
+        like($items->[$i]->{_url}, qr{$rest_base_path/customfield/$basedon_cf_id/value/$cf_value_id$});
+    }
+}
+
+# Delete a value
+{
+    my $res = $mech->delete("$rest_base_path/customfield/$select_cf_id/value/" . $select_cf_values->[-1]->id,
+        'Authorization' => $auth,
+    );
+    is($res->code, 204);
+
+    $select_cf_values = $select_cf->Values->ItemsArrayRef;
+    is(scalar(@$select_cf_values), 3);
+    is($select_cf_values->[-1]->Name, 'Third and Last but NOT Least Value');
+}
+
+done_testing;

commit 2abbfb703802e6523d7d653adfa7e98337adecc2
Author: gibus <gibus at easter-eggs.com>
Date:   Sun Oct 21 10:28:08 2018 +0200

    Add searching for customfields attached to a catalog/class/queue

diff --git a/README b/README
index 108469a..01dc68a 100644
--- a/README
+++ b/README
@@ -485,6 +485,18 @@ USAGE
         POST /customfield
             create a customfield; provide JSON content
 
+        GET /catalog/:id/customfields?query=<JSON>
+        POST /catalog/:id/customfields
+            search for custom fields attached to a catalog using L</JSON searches> syntax
+
+        GET /class/:id/customfields?query=<JSON>
+        POST /class/:id/customfields
+            search for custom fields attached to a class using L</JSON searches> syntax
+
+        GET /queue/:id/customfields?query=<JSON>
+        POST /queue/:id/customfields
+            search for custom fields attached to a queue using L</JSON searches> syntax
+
         GET /customfield/:id
             retrieve a custom field, with values if type is Select
 
diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index 7b17587..4cc1514 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -531,6 +531,18 @@ Below are some examples using the endpoints above.
     POST /customfield
         create a customfield; provide JSON content
 
+    GET /catalog/:id/customfields?query=<JSON>
+    POST /catalog/:id/customfields
+        search for custom fields attached to a catalog using L</JSON searches> syntax
+
+    GET /class/:id/customfields?query=<JSON>
+    POST /class/:id/customfields
+        search for custom fields attached to a class using L</JSON searches> syntax
+
+    GET /queue/:id/customfields?query=<JSON>
+    POST /queue/:id/customfields
+        search for custom fields attached to a queue using L</JSON searches> syntax
+
     GET /customfield/:id
         retrieve a custom field, with values if type is Select
 
diff --git a/lib/RT/Extension/REST2/Resource/CustomFields.pm b/lib/RT/Extension/REST2/Resource/CustomFields.pm
index e227dca..a041010 100644
--- a/lib/RT/Extension/REST2/Resource/CustomFields.pm
+++ b/lib/RT/Extension/REST2/Resource/CustomFields.pm
@@ -8,13 +8,40 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Collection';
 with 'RT::Extension::REST2::Resource::Collection::QueryByJSON';
 
+has 'object_applied_to' => (
+    is  => 'ro',
+    required => 0,
+);
+
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(
         regex => qr{^/customfields/?$},
         block => sub { { collection_class => 'RT::CustomFields' } },
     ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/(catalog|class|queue)/(\d+)/customfields/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $object_type = 'RT::'. ucfirst($match->pos(1));
+            my $object_id = $match->pos(2);
+            my $object_applied_to = $object_type->new($req->env->{"rt.current_user"});
+            $object_applied_to->Load($object_id);
+            return {object_applied_to => $object_applied_to, collection_class => 'RT::CustomFields'};
+        },
+    ),
 }
 
+after 'limit_collection' => sub {
+    my $self = shift;
+    my $collection = $self->collection;
+    my $object = $self->object_applied_to;
+    if ($object && $object->id) {
+        $collection->Limit(ENTRYAGGREGATOR => "AND", FIELD => 'LookupType', OPERATOR => 'STARTSWITH', VALUE => ref($object));
+        $collection->LimitToGlobalOrObjectId($object->id);
+    }
+    return 1;
+};
+
 __PACKAGE__->meta->make_immutable;
 
 1;

commit 8885adf549f74a5784ecfe9dca2acff2e0951937
Author: gibus <gibus at easter-eggs.com>
Date:   Sun Oct 21 10:28:08 2018 +0200

    Add searching for queue-level custom fields

diff --git a/xt/local-custom-fields.t b/xt/local-custom-fields.t
new file mode 100644
index 0000000..de67a63
--- /dev/null
+++ b/xt/local-custom-fields.t
@@ -0,0 +1,85 @@
+use strict;
+use warnings;
+use lib 't/lib';
+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 => 'SeeCustomField' );
+
+my $queue = RT::Queue->new(RT->SystemUser);
+$queue->Load('General');
+my $queue_id = $queue->id;
+
+my $attached_single_cf = RT::CustomField->new(RT->SystemUser);
+$attached_single_cf->Create(LookupType => 'RT::Queue-RT::Ticket', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1, Queue => 'General');
+my $attached_single_cf_id = $attached_single_cf->id;
+
+my $attached_multiple_cf = RT::CustomField->new(RT->SystemUser);
+$attached_multiple_cf->Create(LookupType => 'RT::Queue-RT::Ticket', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 0, Queue => 'General');
+my $attached_multiple_cf_id = $attached_multiple_cf->id;
+
+my $detached_cf = RT::CustomField->new(RT->SystemUser);
+$detached_cf->Create(LookupType => 'RT::Queue-RT::Ticket', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1);
+my $detached_cf_id = $detached_cf->id;
+
+my $queue_cf = RT::CustomField->new(RT->SystemUser);
+$queue_cf->Create(LookupType => 'RT::Queue', Name => 'Freeform CF', Type => 'Freeform', MaxValues => 1);
+$queue_cf->AddToObject($queue);
+my $queue_cf_id = $queue_cf->id;
+
+# All tickets customfields
+{
+    my $res = $mech->post_json("$rest_base_path/customfields",
+        [{field => 'LookupType', value => 'RT::Queue-RT::Ticket'}],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 3);
+    is($content->{count}, 3);
+    is(scalar @{$content->{items}}, 3);
+    my @ids = sort map {$_->{id}} @{$content->{items}};
+    is_deeply(\@ids, [$attached_single_cf_id, $attached_multiple_cf_id, $detached_cf_id]);
+}
+
+# All tickets single customfields attached to queue 'General'
+{
+    my $res = $mech->post_json("$rest_base_path/queue/$queue_id/customfields",
+        [
+            {field => 'LookupType', value => 'RT::Queue-RT::Ticket'},
+            {field => 'MaxValues', value => 1},
+        ],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 1);
+    is($content->{count}, 1);
+    is(scalar @{$content->{items}}, 1);
+    is($content->{items}->[0]->{id}, $attached_single_cf_id);
+}
+
+# All single customfields attached to queue 'General'
+{
+    my $res = $mech->post_json("$rest_base_path/queue/$queue_id/customfields",
+        [
+            {field => 'MaxValues', value => 1},
+        ],
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+    is($content->{total}, 2);
+    is($content->{count}, 2);
+    is(scalar @{$content->{items}}, 2);
+    my @ids = sort map {$_->{id}} @{$content->{items}};
+    is_deeply(\@ids, [$attached_single_cf_id, $queue_cf_id]);
+}
+
+done_testing;

commit 332f0ba05c5e540574f36a58318d37fb81a7e17e
Merge: 0097143 8885adf
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri May 1 06:18:35 2020 +0800

    Merge branch 'admin-custom-fields'


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


More information about the Bps-public-commit mailing list