[Bps-public-commit] rt-extension-rest2 branch, update-custom-roles-on-correspond-and-comment, created. 1.09-7-g95f2e43

Dianne Skoll dianne at bestpractical.com
Tue Dec 8 11:42:41 EST 2020


The branch, update-custom-roles-on-correspond-and-comment has been created
        at  95f2e4395dfdf7f860674cad8b88bf0b68e5a63c (commit)

- Log -----------------------------------------------------------------
commit e476923f2829a1099ac40397899d1445f3acf706
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Tue Dec 8 11:40:37 2020 -0500

    Allow custom role updates via the correspond and comment endpoints.

diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index ec7faa3..1538bda 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -269,6 +269,19 @@ below).
 
 The time, in minutes, you've taken to work on your response/comment, optional.
 
+=item C<CustomRoles>
+
+A hash whose keys are custom role names and values are as described below:
+
+For a single-value custom role, the value must be a string representing an
+email address; the custom role is set to that address.
+
+For a multi-value custom role, the value can be a string representing
+an email address, in which case the address is added to the members of
+the custom role.  Alternatively, it can be an array of email addresses,
+in which case the members of the custom role are set to those email
+addresses.
+
 =back
 
 =head3 Add Attachments
diff --git a/lib/RT/Extension/REST2/Resource/Message.pm b/lib/RT/Extension/REST2/Resource/Message.pm
index 9b05b00..da3d4c2 100644
--- a/lib/RT/Extension/REST2/Resource/Message.pm
+++ b/lib/RT/Extension/REST2/Resource/Message.pm
@@ -7,7 +7,7 @@ use namespace::autoclean;
 use MIME::Base64;
 
 extends 'RT::Extension::REST2::Resource';
-use RT::Extension::REST2::Util qw( error_as_json update_custom_fields );
+use RT::Extension::REST2::Util qw( error_as_json update_custom_fields update_custom_roles);
 
 sub dispatch_rules {
     Path::Dispatcher::Rule::Regex->new(
@@ -150,6 +150,7 @@ sub add_message {
 
     push @results, $msg;
     push @results, update_custom_fields($self->record, $args{CustomFields});
+    push @results, update_custom_roles($self->record, $args{CustomRoles});
     push @results, $self->_update_txn_custom_fields( $TransObj, $args{TxnCustomFields} || $args{TransactionCustomFields} );
 
     $self->created_transaction($TransObj);
diff --git a/lib/RT/Extension/REST2/Util.pm b/lib/RT/Extension/REST2/Util.pm
index 6a2ba19..30d58f7 100644
--- a/lib/RT/Extension/REST2/Util.pm
+++ b/lib/RT/Extension/REST2/Util.pm
@@ -21,6 +21,7 @@ use Sub::Exporter -setup => {
         custom_fields_for
         format_datetime
         update_custom_fields
+        update_custom_roles
     ]]
 };
 
@@ -396,5 +397,112 @@ sub update_custom_fields {
     return @results;
 }
 
+# Update custom roles associated with a ticket
+# Returns an array of results; one result for each attempted update
+sub update_custom_roles {
+    my $ticket = shift;
+    my $data = shift;
+
+    my @results;
+
+    return @results unless $data;
+
+    # Make sure we're passed a hash
+    if ( ref($data) ne 'HASH') {
+        push(@results, 'Expecting a hash value for CustomRoles');
+        return @results;
+    }
+
+    foreach my $rolename (keys %$data) {
+        my $val = $data->{$rolename};
+        push @results, update_custom_role($ticket, $rolename, $val);
+    }
+    return @results;
+}
+
+# Update a single custom role on a ticket
+sub update_custom_role {
+    my ($ticket, $rolename, $val) = @_;
+
+    # Get the custom role ID
+    my $cr = RT::CustomRole->new($ticket->CurrentUser);
+    if (!$cr->Load($rolename)) {
+        return "Could not load custom role '$rolename'";
+    }
+
+    # This is the name used internally by RT
+    my $role_type = 'RT::CustomRole-' . $cr->Id;
+
+    if ($cr->SingleValue) {
+        # Single-valued role
+        if (ref($val)) {
+            return "Expecting a single string for single-valued role '$rolename'";
+        }
+        my ($ok, $msg) = $ticket->AddWatcher(Type => $role_type,
+                                             User => $val);
+        if ($ok) {
+            return "Set '$rolename' to $val";
+        } else {
+            return $msg || "Unable to set '$rolename' to $val";
+        }
+    }
+
+    # Must be a multi-value custom role.  If $val is a scalar,
+    # add the watcher.  If $val is an array, replace existing watchers
+    # with $val
+    if (!ref($val)) {
+        my ($ok, $msg) = $ticket->AddWatcher(Type => $role_type,
+                                                 User => $val);
+        if ($ok) {
+            return "Added $val to '$rolename'";
+        } else {
+            return $msg || "Unable to add $val to '$rolename'";
+        }
+    }
+
+    # Multi-value custom role and we should have been given an array
+    if (ref($val) ne 'ARRAY') {
+        return "Expecting scalar or array for '$rolename'";
+    }
+
+    # Get the existing role addresses
+    my $group = $ticket->RoleGroup($role_type);
+    my @existing_addresses = $group->MemberEmailAddresses;
+
+    my $ok = 1;
+    my $msg = '';
+
+    # Add addresses that are not currently members
+    # The loop below has O(N^2) behavior, but I expect that
+    # typically there will not be a lot of members in a custom
+    # role... revisit if this assumption turns out to be false.
+    foreach my $addr (@$val) {
+        next if grep { lc($_) eq lc($addr) } @existing_addresses;
+        my ($local_ok, $local_msg) = $ticket->AddWatcher(Type => $role_type,
+                                                         User => $addr);
+        $ok = 0 unless $local_ok;
+        if ($local_msg) {
+            $msg .= ", " if $msg;
+            $msg .= $local_msg;
+        }
+    }
+
+    # Delete current members that are not part of passed-in value
+    # See comment above about O(N^2) behavior.
+    foreach my $addr (@existing_addresses) {
+        next if grep { lc($_) eq lc($addr) } @$val;
+        my ($local_ok, $local_msg) = $ticket->DeleteWatcher(Type => $role_type,
+                                                            User => $addr);
+        $ok = 0 unless $local_ok;
+        if ($local_msg) {
+            $msg .= ", " if $msg;
+            $msg .= $local_msg;
+        }
+    }
+    if (!$ok) {
+        return $msg || "Unknown error updating '$role_type'";
+    }
+    return "Updated '$role_type'";
+}
 
 1;

commit 95f2e4395dfdf7f860674cad8b88bf0b68e5a63c
Author: Dianne Skoll <dianne at bestpractical.com>
Date:   Tue Dec 8 11:41:02 2020 -0500

    Add unit test for new feature: Allow custom role updates via the correspond and comment endpoints

diff --git a/xt/ticket-correspond-update-customroles.t b/xt/ticket-correspond-update-customroles.t
new file mode 100644
index 0000000..57ef77c
--- /dev/null
+++ b/xt/ticket-correspond-update-customroles.t
@@ -0,0 +1,223 @@
+use strict;
+use warnings;
+use RT::Extension::REST2::Test tests => undef;
+use Test::Deep;
+
+BEGIN {
+    plan skip_all => 'RT 4.4 required'
+        unless RT::Handle::cmp_version($RT::VERSION, '4.4.0') >= 0;
+}
+
+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;
+
+# Set up a couple of custom roles
+my $queue = RT::Test->load_or_create_queue( Name => "General" );
+
+my $single = RT::CustomRole->new(RT->SystemUser);
+my ($ok, $msg) = $single->Create(Name => 'Single Member', MaxValues => 1);
+ok($ok, $msg);
+my $single_id = $single->Id;
+
+($ok, $msg) = $single->AddToObject($queue->id);
+ok($ok, $msg);
+
+my $multi = RT::CustomRole->new(RT->SystemUser);
+($ok, $msg) = $multi->Create(Name => 'Multi Member');
+ok($ok, $msg);
+my $multi_id = $multi->Id;
+
+($ok, $msg) = $multi->AddToObject($queue->id);
+ok($ok, $msg);
+
+#for my $email (qw/multi at example.com test at localhost multi2 at example.com single2 at example.com/) {
+#    my $user = RT::User->new(RT->SystemUser);
+#    my ($ok, $msg) = $user->Create(Name => $email, EmailAddress => $email);
+#    ok($ok, $msg);
+#}
+
+$user->PrincipalObj->GrantRight( Right => $_ )
+    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers SeeGroup SeeQueue/;
+
+# Ticket Creation
+my ($ticket_url, $ticket_id);
+{
+    my $payload = {
+        Subject => 'Ticket creation using REST',
+        Queue   => 'General',
+        Content => 'Testing ticket creation using REST API.',
+    };
+
+    my $res = $mech->post_json("$rest_base_path/ticket",
+        $payload,
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    ok($ticket_url = $res->header('location'));
+    ok(($ticket_id) = $ticket_url =~ qr[/ticket/(\d+)]);
+}
+
+# Ticket Display
+{
+    $user->PrincipalObj->GrantRight( Right => 'ShowTicket' );
+
+    my $res = $mech->get($ticket_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+
+    my $content = $mech->json_response;
+
+    is($content->{id}, $ticket_id);
+    is($content->{Type}, 'ticket');
+    is($content->{Status}, 'new');
+    is($content->{Subject}, 'Ticket creation using REST');
+
+    ok(exists $content->{$_}, "Content exists for $_") for qw(AdminCc TimeEstimated Started Cc
+                                     LastUpdated TimeWorked Resolved
+                                     RT::CustomRole-1 RT::CustomRole-2
+                                     Created Due Priority EffectiveId);
+}
+
+# Ticket Reply
+{
+    $user->PrincipalObj->GrantRight( Right => 'ReplyToTicket' );
+
+    my $res = $mech->get($ticket_url,
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    my $content = $mech->json_response;
+
+    my ($hypermedia) = grep { $_->{ref} eq 'correspond' } @{ $content->{_hyperlinks} };
+    ok($hypermedia, 'got correspond hypermedia');
+    like($hypermedia->{_url}, qr[$rest_base_path/ticket/$ticket_id/correspond$]);
+
+    my $correspond_url = $mech->url_for_hypermedia('correspond');
+    my $comment_url = $correspond_url;
+    $comment_url =~ s/correspond/comment/;
+
+    $res = $mech->post_json($correspond_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => 'foo at bar.example',
+                'Multi Member' => 'quux at cabbage.example',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    $content = $mech->json_response;
+
+    # Because CustomRoles are set in an unpredictable order, sort the
+    # responses so we have a predictable order.
+    @$content = sort { $a cmp $b } (@$content);
+    cmp_deeply($content, ["Added quux\@cabbage.example to 'Multi Member'", re(qr/Correspondence added|Message recorded/), "Set 'Single Member' to foo\@bar.example"]);
+    like($res->header('Location'), qr{$rest_base_path/transaction/\d+$});
+    $res = $mech->get($res->header('Location'),
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    $content = $mech->json_response;
+    is($content->{Type}, 'Correspond');
+    is($content->{TimeTaken}, 0);
+    is($content->{Object}{type}, 'ticket');
+    is($content->{Object}{id}, $ticket_id);
+
+    $res = $mech->get($mech->url_for_hypermedia('attachment'),
+        'Authorization' => $auth,
+    );
+    is($res->code, 200);
+    $content = $mech->json_response;
+    is($content->{Content}, 'Hello from hypermedia!');
+    is($content->{ContentType}, 'text/plain');
+
+    # Load the ticket and check the custom roles
+    my $ticket = RT::Ticket->new($user);
+    $ticket->Load($ticket_id);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo at bar.example',
+       "Single Member role set correctly");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'quux at cabbage.example',
+       "Multi Member role set correctly");
+
+    # Update again (this time as a comment)
+    $res = $mech->post_json($comment_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => 'foo-new at bar.example',
+                'Multi Member' => 'quux-new at cabbage.example',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role set correctly");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'quux-new at cabbage.example, quux at cabbage.example',
+       "Multi Member role updated correctly");
+
+    # Supply an array for multi-member role
+    $res = $mech->post_json($correspond_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Multi Member' => ['abacus at example.com', 'quux-new at cabbage.example'],
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role unchanged");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'abacus at example.com, quux-new at cabbage.example',
+       "Multi Member role set correctly");
+
+    # Add an existing user to multi-member role
+    $res = $mech->post_json($correspond_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Multi Member' => 'abacus at example.com',
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role unchanged");
+    is($ticket->RoleAddresses("RT::CustomRole-$multi_id"), 'abacus at example.com, quux-new at cabbage.example',
+       "Multi Member role unchanged");
+
+    # Supply an array for single-member role
+    $res = $mech->post_json($correspond_url,
+        {
+            Content => 'Hello from hypermedia!',
+            ContentType => 'text/plain',
+            CustomRoles => {
+                'Single Member' => ['abacus at example.com', 'quux-new at cabbage.example'],
+            },
+        },
+        'Authorization' => $auth,
+    );
+    is($res->code, 201);
+    $content = $mech->json_response;
+    cmp_deeply($content, ['Correspondence added', "Expecting a single string for single-valued role 'Single Member'"], "Got expected respose");
+    is($ticket->RoleAddresses("RT::CustomRole-$single_id"), 'foo-new at bar.example',
+       "Single Member role unchanged");
+
+}
+
+done_testing;

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


More information about the Bps-public-commit mailing list