[Rt-commit] rt branch, 4.4/username-and-groupname-in-role-input, created. rt-4.4.3-45-g901b59900

? sunnavy sunnavy at bestpractical.com
Wed Aug 29 17:22:47 EDT 2018


The branch, 4.4/username-and-groupname-in-role-input has been created
        at  901b59900759c8bd3317a2fef61b7e793b003343 (commit)

- Log -----------------------------------------------------------------
commit 015dee4c2653f6bfb4ae51152007553d1a5dd04f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed Jan 10 16:40:40 2018 -0500

    Accept usernames for email input fields on ticket create/update
    
    In addition to being able to add an RT user to a role with an email
    address, also accept a valid RT username via the web UI.

diff --git a/lib/RT/EmailParser.pm b/lib/RT/EmailParser.pm
index ad26a291b..7f6a65a65 100644
--- a/lib/RT/EmailParser.pm
+++ b/lib/RT/EmailParser.pm
@@ -560,6 +560,37 @@ use Email::Address::List;
 
 sub ParseEmailAddress {
     my $self = shift;
+
+    my @list = $self->_ParseEmailAddress( @_ );
+
+    my @addresses;
+    foreach my $e ( @list ) {
+        if ( $e->{'type'} eq 'mailbox' ) {
+            push @addresses, $e->{'value'};
+        }
+        elsif ( $e->{'value'} =~ /^\s*(\w+)\s*$/ ) {
+            my $user = RT::User->new( RT->SystemUser );
+            $user->Load( $1 );
+            if ( $user->id ) {
+                push @addresses, Email::Address->new( $user->Name, $user->EmailAddress );
+            }
+            else {
+                RT->Logger->error( $e->{'value'} . " is not a valid email address and is not user name" );
+            }
+        }
+        else {
+            # should never reach here.
+        }
+    }
+
+    $self->CleanupAddresses( @addresses );
+
+    return @addresses;
+}
+
+# Returns a list of hashes, like what C<Email::Address::List::parse> returns
+sub _ParseEmailAddress {
+    my $self           = shift;
     my $address_string = shift;
 
     # Some broken mailers send:  ""Vincent, Jesse"" <jesse at fsck.com>. Hate
@@ -574,30 +605,22 @@ sub ParseEmailAddress {
         "Unable to parse an email address from $address_string: ". shift
     ) };
 
-    my @addresses;
-    foreach my $e ( @list ) {
+    my @entries;
+    foreach my $e (@list) {
         if ($e->{'type'} eq 'mailbox') {
             if ($e->{'not_ascii'}) {
                 $logger->($e->{'value'} ." contains not ASCII values");
                 next;
             }
-            push @addresses, $e->{'value'}
-        } elsif ( $e->{'value'} =~ /^\s*(\w+)\s*$/ ) {
-            my $user = RT::User->new( RT->SystemUser );
-            $user->Load( $1 );
-            if ($user->id) {
-                push @addresses, Email::Address->new($user->Name, $user->EmailAddress);
-            } else {
-                $logger->($e->{'value'} ." is not a valid email address and is not user name");
-            }
+            push @entries, $e;
+        } elsif ($e->{'value'} =~ /^\s*(\w+)\s*$/) {
+            push @entries, $e;
         } else {
             $logger->($e->{'value'} ." is not a valid email address");
         }
     }
 
-    $self->CleanupAddresses(@addresses);
-
-    return @addresses;
+    return @entries;
 }
 
 =head2 CleanupAddresses ARRAY
diff --git a/lib/RT/Record/Role/Roles.pm b/lib/RT/Record/Role/Roles.pm
index 1a88793ae..328914601 100644
--- a/lib/RT/Record/Role/Roles.pm
+++ b/lib/RT/Record/Role/Roles.pm
@@ -621,21 +621,10 @@ sub _ResolveRoles {
                             $self->loc("Couldn't load principal: [_1]", $msg);
                     }
                 } else {
-                    my @addresses = RT::EmailParser->ParseEmailAddress( $value );
-                    for my $address ( @addresses ) {
-                        my $user = RT::User->new( RT->SystemUser );
-                        my ($id, $msg) = $user->LoadOrCreateByEmail( $address );
-                        if ( $id ) {
-                            # Load it back as us, not as the system
-                            # user, to be completely safe.
-                            $user = RT::User->new( $self->CurrentUser );
-                            $user->Load( $id );
-                            push @{ $roles->{$role} }, $user->PrincipalObj;
-                        } else {
-                            push @errors,
-                                $self->loc("Couldn't load or create user: [_1]", $msg);
-                        }
-                    }
+                    my ($users, $errors) = $self->ParseInputPrincipals( $value );
+
+                    push @{ $roles->{$role} }, map { $_->PrincipalObj } @{$users};
+                    push @errors, @$errors if @$errors;
                 }
             }
         }
@@ -643,6 +632,64 @@ sub _ResolveRoles {
     return (@errors);
 }
 
+=head2 ParseInputPrincipals
+
+In the RT web UI, some watcher input fields can accept RT users
+identified by email address or RT username. On the ticket Create
+and Update pages, these fields can have multiple values submitted
+as a comma-separated list. This method parses such lists and returns
+an array of user objects found or created for each parsed value.
+
+C<ParseEmailAddress> in L<RT::EmailParser> provides a similar
+function, but only handles email addresses, filtering out
+usernames. It also returns a list of L<Email::Address> objects
+rather than RT objects.
+
+Accepts: a string with usernames and email addresses
+
+Returns: arrayref of RT::User objects, arrayref of any error strings
+
+=cut
+
+sub ParseInputPrincipals {
+    my $self = shift;
+    my @list = RT::EmailParser->_ParseEmailAddress( @_ );
+
+    my @principals;    # Collect user or group objects
+    my @errors;
+
+    foreach my $e ( @list ) {
+        my $user = RT::User->new( RT->SystemUser );
+
+        if ( $e->{'type'} eq 'mailbox' ) {
+            my ( $id, $msg ) = $user->LoadOrCreateByEmail( $e->{'value'} );
+            if ( $id ) {
+                push @principals, $user;
+            }
+            else {
+                push @errors, $self->loc( "Couldn't load or create user: [_1]", $msg );
+                RT::Logger->error( "Couldn't load or create user from email address " . $e->{'value'} . ", " . $msg );
+            }
+        }
+        elsif ( $e->{'value'} =~ /^\s*(\w+)\s*$/ ) {
+
+            my ( $id, $msg ) = $user->Load( $1 );
+            if ( $id ) {
+                push @principals, $user;
+            }
+            else {
+                push @errors, $self->loc( "Couldn't load user: [_1]", $msg );
+                RT::Logger->error( "Couldn't load user from value " . $e->{'value'} . ", " . $msg );
+            }
+        }
+        else {
+            # should never reach here.
+        }
+    }
+
+    return ( \@principals, \@errors );
+}
+
 sub _CreateRoleGroup {
     my $self = shift;
     my $name = shift;
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index 3335a1c00..dc3bf5712 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -1686,10 +1686,12 @@ sub _RecordNote {
 
     foreach my $type (qw/Cc Bcc/) {
         if ( defined $args{ $type . 'MessageTo' } ) {
-
-            my $addresses = join ', ', (
-                map { RT::User->CanonicalizeEmailAddress( $_->address ) }
-                    Email::Address->parse( $args{ $type . 'MessageTo' } ) );
+            my ( $users ) = $self->ParseInputPrincipals( $args{ $type . 'MessageTo' } );
+            my $addresses = join ', ',
+              (
+                map { RT::User->CanonicalizeEmailAddress( $_ ) }
+                map { $_->EmailAddress || () } @$users
+              );
             $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
         }
     }
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index c8b73f4e1..638838eb7 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -488,13 +488,24 @@ if ( !exists $ARGS{'AddMoreAttach'} && ($ARGS{'id'}||'') eq 'new' ) {
         my $value = $ARGS{ $field };
         next unless defined $value && length $value;
 
-        my @emails = Email::Address->parse( $value );
-        foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
-            push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc($field =~ /^(.*?)s?$/) );
-            $checks_failure = 1;
-            $email = undef;
+        my @emails;
+        my @list = RT::EmailParser->_ParseEmailAddress( $value );
+        foreach my $entry ( @list ) {
+            if ( $entry->{type} eq 'mailbox' ) {
+                my $email = $entry->{value};
+                if ( RT::EmailParser->IsRTAddress($email->address) ) {
+                    push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc($field =~ /^(.*?)s?$/) );
+                    $checks_failure ||= 1;
+                }
+                else {
+                    push @emails, $email->format;
+                }
+            }
+            else {
+                push @emails, $entry->{value};
+            }
         }
-        $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+        $ARGS{ $field } = join ', ', grep defined, @emails;
     }
 }
 
diff --git a/share/html/Ticket/Update.html b/share/html/Ticket/Update.html
index 6e73b50a8..00653ab44 100644
--- a/share/html/Ticket/Update.html
+++ b/share/html/Ticket/Update.html
@@ -339,13 +339,24 @@ if ( $ARGS{'SubmitTicket'} ) {
         my $value = $ARGS{ $field };
         next unless defined $value && length $value;
 
-        my @emails = Email::Address->parse( $value );
-        foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
-            push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc(substr($field, 6)) );
-            $checks_failure = 1;
-            $email = undef;
+        my @emails;
+        my @list = RT::EmailParser->_ParseEmailAddress( $value );
+        foreach my $entry ( @list ) {
+            if ( $entry->{type} eq 'mailbox' ) {
+                my $email = $entry->{value};
+                if ( RT::EmailParser->IsRTAddress($email->address) ) {
+                    push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc(substr($field, 6)) );
+                    $checks_failure ||= 1;
+                }
+                else {
+                    push @emails, $email->format;
+                }
+            }
+            else {
+                push @emails, $entry->{value};
+            }
         }
-        $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+        $ARGS{ $field } = join ', ', grep defined, @emails;
     }
 }
 
diff --git a/share/html/m/ticket/create b/share/html/m/ticket/create
index 5fbfecff3..70b640a78 100644
--- a/share/html/m/ticket/create
+++ b/share/html/m/ticket/create
@@ -184,13 +184,24 @@ if ( !exists $ARGS{'AddMoreAttach'} && ($ARGS{'id'}||'') eq 'new' ) {
         my $value = $ARGS{ $field };
         next unless defined $value && length $value;
 
-        my @emails = Email::Address->parse( $value );
-        foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
-            push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc($field =~ /^(.*?)s?$/) );
-            $checks_failure = 1;
-            $email = undef;
+        my @emails;
+        my @list = RT::EmailParser->_ParseEmailAddress( $value );
+        foreach my $entry ( @list ) {
+            if ( $entry->{type} eq 'mailbox' ) {
+                my $email = $entry->{value};
+                if ( RT::EmailParser->IsRTAddress($email->address) ) {
+                    push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc($field =~ /^(.*?)s?$/) );
+                    $checks_failure ||= 1;
+                }
+                else {
+                    push @emails, $email->format;
+                }
+            }
+            else {
+                push @emails, $entry->{value};
+            }
         }
-        $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+        $ARGS{ $field } = join ', ', grep defined, @emails;
     }
 }
 
diff --git a/share/html/m/ticket/reply b/share/html/m/ticket/reply
index 0a65091f5..5d884c74c 100644
--- a/share/html/m/ticket/reply
+++ b/share/html/m/ticket/reply
@@ -202,13 +202,24 @@ ProcessAttachments(ARGSRef => \%ARGS);
         my $value = $ARGS{ $field };
         next unless defined $value && length $value;
 
-        my @emails = Email::Address->parse( $value );
-        foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
-            push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc(substr($field, 6)) );
-            $checks_failure = 1;
-            $email = undef;
+        my @emails;
+        my @list = RT::EmailParser->_ParseEmailAddress( $value );
+        foreach my $entry ( @list ) {
+            if ( $entry->{type} eq 'mailbox' ) {
+                my $email = $entry->{value};
+                if ( RT::EmailParser->IsRTAddress($email->address) ) {
+                    push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc(substr($field, 6)) );
+                    $checks_failure ||= 1;
+                }
+                else {
+                    push @emails, $email->format;
+                }
+            }
+            else {
+                push @emails, $entry->{value};
+            }
         }
-        $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+        $ARGS{ $field } = join ', ', grep defined, @emails;
     }
 }
 

commit df2dd2e3bf8b20a474fccd712bc8712acde0986d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu May 18 15:34:25 2017 +0000

    Reuse CanonicalizePrincipal in DeleteRoleMember
    
    This matches the behavior of AddRoleMember. It's important for followup
    commits to be able to support, say, remove "group:Foo" as AdminCc in
    bulk update.
    
    This also fixes a bug where the internal "RT::CustomRole-#" syntax may
    show up in error messages when removing a watcher from a custom role
    fails.

diff --git a/lib/RT/Record/Role/Roles.pm b/lib/RT/Record/Role/Roles.pm
index 328914601..bd7fda2b6 100644
--- a/lib/RT/Record/Role/Roles.pm
+++ b/lib/RT/Record/Role/Roles.pm
@@ -507,14 +507,9 @@ Takes a set of key-value pairs:
 
 =over 4
 
-=item PrincipalId
-
-Optional.  The ID of the L<RT::Principal> object to remove.
-
-=item User
+=item Principal, PrincipalId, User, or Group
 
-Optional.  The Name or EmailAddress of an L<RT::User> to use as the
-principal
+Required. Canonicalized through L</CanonicalizePrincipal>.
 
 =item Type
 
@@ -528,8 +523,6 @@ status of "Permission denied".
 
 =back
 
-One, and only one, of I<PrincipalId> or I<User> is required.
-
 Returns a tuple of (principal object that was removed, message).
 
 =cut
@@ -541,20 +534,8 @@ sub DeleteRoleMember {
     return (0, $self->loc("That role is invalid for this object"))
         unless $args{Type} and $self->HasRole($args{Type});
 
-    if ($args{User}) {
-        my $user = RT::User->new( $self->CurrentUser );
-        $user->LoadByEmail( $args{User} );
-        $user->Load( $args{User} ) unless $user->id;
-        return (0, $self->loc("Could not load user '[_1]'", $args{User}) )
-            unless $user->id;
-        $args{PrincipalId} = $user->PrincipalId;
-    }
-
-    return (0, $self->loc("No valid PrincipalId"))
-        unless $args{PrincipalId};
-
-    my $principal = RT::Principal->new( $self->CurrentUser );
-    $principal->Load( $args{PrincipalId} );
+    my ($principal, $msg) = $self->CanonicalizePrincipal(%args);
+    return (0, $msg) if !$principal;
 
     my $acl = delete $args{ACL};
     return (0, $self->loc("Permission denied"))
@@ -568,12 +549,12 @@ sub DeleteRoleMember {
                             $principal->Object->Name, $self->loc($args{Type}) ) )
         unless $group->HasMember($principal);
 
-    my ($ok, $msg) = $group->_DeleteMember($args{PrincipalId}, RecordTransaction => !$args{Silent});
+    ((my $ok), $msg) = $group->_DeleteMember($principal->Id, RecordTransaction => !$args{Silent});
     unless ($ok) {
-        $RT::Logger->error("Failed to remove $args{PrincipalId} as a member of group ".$group->Id.": ".$msg);
+        $RT::Logger->error("Failed to remove ".$principal->Id." as a member of group ".$group->Id.": ".$msg);
 
         return ( 0, $self->loc('Could not remove [_1] as a [_2]',
-                    $principal->Object->Name, $self->loc($args{Type})) );
+                    $principal->Object->Name, $group->Label) );
     }
 
     return ($principal, $msg);

commit c8f4e9d865f670b20e1c8afac3da554b6979e46f
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 29 00:15:53 2018 +0800

    Support group:NAME and group:ID in non-single role input fields
    
    Fixes: #32311

diff --git a/lib/RT/EmailParser.pm b/lib/RT/EmailParser.pm
index 7f6a65a65..93f6d2129 100644
--- a/lib/RT/EmailParser.pm
+++ b/lib/RT/EmailParser.pm
@@ -568,9 +568,15 @@ sub ParseEmailAddress {
         if ( $e->{'type'} eq 'mailbox' ) {
             push @addresses, $e->{'value'};
         }
-        elsif ( $e->{'value'} =~ /^\s*(\w+)\s*$/ ) {
+        elsif ( $e->{'value'} =~ /^(group:)?(\w+)$/ ) {
+            my ( $is_group, $name ) = ( $1, $2 );
+            if ( $is_group ) {
+                RT->Logger->warning( $e->{'value'} . " is a group, skipping" );
+                next;
+            }
+
             my $user = RT::User->new( RT->SystemUser );
-            $user->Load( $1 );
+            $user->Load( $name );
             if ( $user->id ) {
                 push @addresses, Email::Address->new( $user->Name, $user->EmailAddress );
             }
@@ -599,13 +605,19 @@ sub _ParseEmailAddress {
     my @list = Email::Address::List->parse(
         $address_string,
         skip_comments => 1,
-        skip_groups => 1,
     );
     my $logger = sub { RT->Logger->error(
         "Unable to parse an email address from $address_string: ". shift
     ) };
 
     my @entries;
+
+    # If the string begins with group, e.g. "group:foo", then the first 2
+    # items returned from Email::Address::List are:
+    # { 'value' => 'group', 'type' => 'group start' },
+    # { 'value' => 'foo', 'type' => 'unknown' }
+    my $in_group;
+
     foreach my $e (@list) {
         if ($e->{'type'} eq 'mailbox') {
             if ($e->{'not_ascii'}) {
@@ -613,11 +625,20 @@ sub _ParseEmailAddress {
                 next;
             }
             push @entries, $e;
-        } elsif ($e->{'value'} =~ /^\s*(\w+)\s*$/) {
+        } elsif ($e->{'type'} eq 'group start') {
+            $in_group = 1;
+            next;
+        } elsif ($e->{'type'} eq 'group end') {
+            undef $in_group;
+            next;
+        } elsif ($e->{'value'} =~ /^\s*(group\s*:)?\s*(\w+)\s*$/i) {
+            my ( $is_group, $name ) = ( $1, $2 );
+            $e->{'value'} = $in_group || $is_group ? "group:$name" : $name;
             push @entries, $e;
         } else {
             $logger->($e->{'value'} ." is not a valid email address");
         }
+        undef $in_group;
     }
 
     return @entries;
diff --git a/lib/RT/Record/Role/Roles.pm b/lib/RT/Record/Role/Roles.pm
index bd7fda2b6..ec9eeac86 100644
--- a/lib/RT/Record/Role/Roles.pm
+++ b/lib/RT/Record/Role/Roles.pm
@@ -389,6 +389,11 @@ sub CanonicalizePrincipal {
                 if RT::EmailParser->IsRTAddress( $email );
         }
     } else {
+        if ( ( $args{User} || '' ) =~ /^\s*group\s*:\s*(\w+)\s*$/i ) {
+            $args{Group} = $1;
+            delete $args{User};
+        }
+
         if ($args{User}) {
             my $name = delete $args{User};
             # Sanity check the address
@@ -640,9 +645,8 @@ sub ParseInputPrincipals {
     my @errors;
 
     foreach my $e ( @list ) {
-        my $user = RT::User->new( RT->SystemUser );
-
         if ( $e->{'type'} eq 'mailbox' ) {
+            my $user = RT::User->new( RT->SystemUser );
             my ( $id, $msg ) = $user->LoadOrCreateByEmail( $e->{'value'} );
             if ( $id ) {
                 push @principals, $user;
@@ -652,15 +656,30 @@ sub ParseInputPrincipals {
                 RT::Logger->error( "Couldn't load or create user from email address " . $e->{'value'} . ", " . $msg );
             }
         }
-        elsif ( $e->{'value'} =~ /^\s*(\w+)\s*$/ ) {
-
-            my ( $id, $msg ) = $user->Load( $1 );
-            if ( $id ) {
-                push @principals, $user;
+        elsif ( $e->{'value'} =~ /^(group:)?(\w+)$/ ) {
+
+            my ( $is_group, $name ) = ( $1, $2 );
+            if ( $is_group ) {
+                my $group = RT::Group->new( RT->SystemUser );
+                my ( $id, $msg ) = $group->LoadUserDefinedGroup( $name );
+                if ( $id ) {
+                    push @principals, $group;
+                }
+                else {
+                    push @errors, $self->loc( "Couldn't load group: [_1]", $msg );
+                    RT::Logger->error( "Couldn't load group from value " . $e->{'value'} . ", " . $msg );
+                }
             }
             else {
-                push @errors, $self->loc( "Couldn't load user: [_1]", $msg );
-                RT::Logger->error( "Couldn't load user from value " . $e->{'value'} . ", " . $msg );
+                my $user = RT::User->new( RT->SystemUser );
+                my ( $id, $msg ) = $user->Load( $name );
+                if ( $id ) {
+                    push @principals, $user;
+                }
+                else {
+                    push @errors, $self->loc( "Couldn't load user: [_1]", $msg );
+                    RT::Logger->error( "Couldn't load user from value " . $e->{'value'} . ", " . $msg );
+                }
             }
         }
         else {

commit c5a121b8f3f3331594acb035e4b5a4df862ccf67
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 17 21:31:27 2017 +0000

    Autocompleter for Principals (users and groups)
    
    This is a combination of the autocompleter for users and groups. Both
    object types will be searched. The return value is hardcoded: for users,
    return their email address or name if email address is empty; for
    groups, return "group:Name" (or if "Name" matches any autocomplete
    delimiter, "group:ID"). This corresponds with what
    RT::Record::Role::Roles::_ResolveRoles accepts.

diff --git a/share/html/Elements/EmailInput b/share/html/Elements/EmailInput
index f07ede617..1f04ae640 100644
--- a/share/html/Elements/EmailInput
+++ b/share/html/Elements/EmailInput
@@ -60,7 +60,7 @@
 % }
 
 % if ($Autocomplete) {
-    data-autocomplete="Users"
+    data-autocomplete="<% $AutocompleteType %>"
 % }
 
 % if ($AutocompleteMultiple) {
@@ -91,6 +91,7 @@ $Name
 $Size    => 40
 $Default => ''
 $Autocomplete => 1
+$AutocompleteType => 'Users'
 $AutocompleteMultiple => 0
 $AutocompleteReturn => ''
 $AutocompleteNobody => 0
diff --git a/share/html/Helpers/Autocomplete/Principals b/share/html/Helpers/Autocomplete/Principals
new file mode 100644
index 000000000..c018d7ce8
--- /dev/null
+++ b/share/html/Helpers/Autocomplete/Principals
@@ -0,0 +1,136 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+% $r->content_type('application/json; charset=utf-8');
+<% JSON( \@suggestions ) |n %>
+% $m->abort;
+<%ARGS>
+$term => undef
+$delim => undef
+$max => undef
+$privileged => undef
+$exclude => ''
+$include_nobody => 0
+$include_system => 0
+</%ARGS>
+<%INIT>
+
+$m->abort unless defined $term
+             and length $term;
+
+# Use our delimeter if we have one
+if ( defined $delim and length $delim ) {
+    if ( $delim eq ',' ) {
+        $delim = qr/,\s*/;
+    } else {
+        $delim = qr/\Q$delim\E/;
+    }
+
+    # If the field handles multiple values, pop the last one off
+    $term = (split $delim, $term)[-1] if $term =~ $delim;
+}
+
+my $CurrentUser = $session{'CurrentUser'};
+
+# Require privileged users or overriding config
+$m->abort unless $CurrentUser->Privileged
+              or RT->Config->Get('AllowUserAutocompleteForUnprivileged');
+
+# the API wants a list of ids
+my @exclude = split /\s*,\s*/, $exclude;
+push @exclude, RT->SystemUser->id unless $include_system;
+push @exclude, RT->Nobody->id unless $include_nobody;
+
+$m->callback( CallbackName => 'ModifyMaxResults', max => \$max );
+$max //= 10;
+
+my $users = RT::Users->new($CurrentUser);
+$users->SimpleSearch( Privileged => $privileged,
+                      Return     => 'Name',
+                      Term       => $term,
+                      Max        => $max,
+                      Exclude    => \@exclude,
+                    );
+
+(my $group_term = $term) =~ s/^\s*group\s*:\s*//i;
+my $groups = RT::Groups->new( $CurrentUser );
+$groups->RowsPerPage( $max );
+$groups->LimitToUserDefinedGroups();
+$groups->Limit(
+    FIELD           => 'Name',
+    OPERATOR        => 'STARTSWITH',
+    VALUE           => $group_term,
+    CASESENSITIVE   => 0,
+);
+
+# Exclude groups we don't want
+foreach (@exclude) {
+    $groups->Limit(FIELD => 'id', VALUE => $_, OPERATOR => '!=', ENTRYAGGREGATOR => 'AND');
+}
+
+my @suggestions;
+
+while ( my $user = $users->Next ) {
+    my $suggestion = { id => $user->id, label => $user->Format, value => $user->EmailAddress || $user->Name };
+    $m->callback( CallbackName => "ModifySuggestion", suggestion => $suggestion, user => $user );
+    push @suggestions, $suggestion;
+}
+
+while ( my $group = $groups->Next ) {
+    my $label = 'group:' . $group->Name;
+
+    # if group name contains, say, a comma, then use its id instead to avoid
+    # parsing problems
+    my $value = $delim && $label =~ $delim ? 'group:' . $group->id : $label;
+
+    my $suggestion = { id => $group->id, label => $label, value => $value };
+    $m->callback( CallbackName => "ModifySuggestion", suggestion => $suggestion, group => $group );
+    push @suggestions, $suggestion;
+
+    last if @suggestions >= $max;
+}
+</%INIT>
diff --git a/share/static/js/autocomplete.js b/share/static/js/autocomplete.js
index cd8ab2b0d..34c44e7c5 100644
--- a/share/static/js/autocomplete.js
+++ b/share/static/js/autocomplete.js
@@ -5,7 +5,8 @@ window.RT.Autocomplete.Classes = {
     Users: 'user',
     Groups: 'group',
     Tickets: 'tickets',
-    Queues: 'queues'
+    Queues: 'queues',
+    Principals: 'principals'
 };
 
 window.RT.Autocomplete.bind = function(from) {

commit 917a486dd9e72d9d488a0483ee3617dc7b3e6adb
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Wed May 17 21:38:42 2017 +0000

    Switch to Principals autocompleter for non-single role input fields
    
    This lets users easily specify groups for watchers and custom roles,
    even on the ticket create page.
    
    This also fixes an inconsistency where managing custom role members on
    bulk update would accept multiple users; now it is consistent with core
    role fields that accept only a single member.

diff --git a/share/html/Asset/Elements/EditPeople b/share/html/Asset/Elements/EditPeople
index 98ebf67bb..51bdb7f70 100644
--- a/share/html/Asset/Elements/EditPeople
+++ b/share/html/Asset/Elements/EditPeople
@@ -52,7 +52,7 @@
 <% loc($role) %>:
 </td>
 <td class="value" colspan="5">
-<& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1 &>
+<& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1, ($AssetObj->Role($role)->{Single} ? () : (AutocompleteType => 'Principals', AutocompleteMultiple => 1)) &>
 </td>
 </tr>
 % }
diff --git a/share/html/Asset/Elements/EditPeople b/share/html/Elements/MultiUserRoleInput
similarity index 84%
copy from share/html/Asset/Elements/EditPeople
copy to share/html/Elements/MultiUserRoleInput
index 98ebf67bb..664adb2f2 100644
--- a/share/html/Asset/Elements/EditPeople
+++ b/share/html/Elements/MultiUserRoleInput
@@ -45,20 +45,16 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<table border="0" cellpadding="0" cellspacing="0">
-% for my $role ( $AssetObj->Roles ) {
-<tr class="asset-people-<% CSSClass($role) %>">
-<td class="label">
-<% loc($role) %>:
-</td>
-<td class="value" colspan="5">
-<& /Elements/EmailInput, Name => $role, Size => undef, Default => $ARGS{$role}, Autocomplete => 1 &>
-</td>
-</tr>
-% }
-
-</table>
-
-<%args>
-$AssetObj
-</%args>
+<& /Elements/EmailInput,
+    Name => $role->GroupType,
+    ($ShowEntryHint ? (EntryHint => $role->EntryHint) : ()),
+    Autocomplete => 1,
+    AutocompleteType => 'Principals',
+    AutocompleteMultiple => 1,
+    %ARGS,
+&>
+<%ARGS>
+$role
+$Ticket => undef
+$ShowEntryHint => 1
+</%ARGS>
diff --git a/share/html/Elements/QuickCreate b/share/html/Elements/QuickCreate
index e47355e90..d064eb4e4 100644
--- a/share/html/Elements/QuickCreate
+++ b/share/html/Elements/QuickCreate
@@ -69,7 +69,7 @@
 </tr>
 <tr class="input-row">
     <td class="label"><&|/l&>Requestors</&>:</td>
-    <td colspan="3" class="value"><& /Elements/EmailInput, Name => 'Requestors', Size => '40', Default => $args->{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &></td>
+    <td colspan="3" class="value"><& /Elements/EmailInput, Name => 'Requestors', AutocompleteType => 'Principals', Size => '40', Default => $args->{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &></td>
 </tr>
 <tr class="input-row">
 <td class="labeltop"><&|/l&>Content</&>:</td>
diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index 536614181..d92121828 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -83,17 +83,17 @@
 <label>(<input type="checkbox" class="checkbox" name="ForceOwnerChange"
 <% $ARGS{ForceOwnerChange} ? 'checked="checked"' : '' %> /> <&|/l&>Force change</&>)</label></td></tr>
 <tr><td class="label"> <&|/l&>Add Requestor</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "AddRequestor", Size=> 20, Default => $ARGS{AddRequestor} &> </td></tr>
+<td class="value"> <& /Elements/EmailInput, Name => "AddRequestor", Size=> 20, Default => $ARGS{AddRequestor}, AutocompleteType => 'Principals' &> </td></tr>
 <tr><td class="label"> <&|/l&>Remove Requestor</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "DeleteRequestor", Size=> 20, Default => $ARGS{DeleteRequestor} &> </td></tr>
+<td class="value"> <& /Elements/EmailInput, Name => "DeleteRequestor", Size=> 20, Default => $ARGS{DeleteRequestor}, AutocompleteType => 'Principals' &> </td></tr>
 <tr><td class="label"> <&|/l&>Add Cc</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "AddCc", Size=> 20, Default => $ARGS{AddCc} &> </td></tr>
+<td class="value"> <& /Elements/EmailInput, Name => "AddCc", Size=> 20, Default => $ARGS{AddCc}, AutocompleteType => 'Principals' &> </td></tr>
 <tr><td class="label"> <&|/l&>Remove Cc</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "DeleteCc", Size=> 20, Default => $ARGS{DeleteCc} &> </td></tr>
+<td class="value"> <& /Elements/EmailInput, Name => "DeleteCc", Size=> 20, Default => $ARGS{DeleteCc}, AutocompleteType => 'Principals' &> </td></tr>
 <tr><td class="label"> <&|/l&>Add AdminCc</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "AddAdminCc", Size=> 20, Default => $ARGS{AddAdminCc} &> </td></tr>
+<td class="value"> <& /Elements/EmailInput, Name => "AddAdminCc", Size=> 20, Default => $ARGS{AddAdminCc}, AutocompleteType => 'Principals' &> </td></tr>
 <tr><td class="label"> <&|/l&>Remove AdminCc</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "DeleteAdminCc", Size=> 20, Default => $ARGS{DeleteAdminCc} &> </td></tr>
+<td class="value"> <& /Elements/EmailInput, Name => "DeleteAdminCc", Size=> 20, Default => $ARGS{DeleteAdminCc}, AutocompleteType => 'Principals' &> </td></tr>
 
 % my $single_roles = RT::CustomRoles->new($session{CurrentUser});
 % $single_roles->LimitToSingleValue;
@@ -111,11 +111,11 @@
 % while (my $role = $multi_roles->Next) {
 <tr>
 <td class="label"> <&|/l, $role->Name &>Add [_1]</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "AddRT::CustomRole-" . $role->Id, Size=> 20, Default => $ARGS{"AddRT::CustomRole-" . $role->Id} &> </td>
+<td class="value"> <& /Elements/MultiUserRoleInput, role => $role, Name => "AddRT::CustomRole-" . $role->Id, Size => 20, Default => $ARGS{"AddRT::CustomRole-" . $role->Id}, AutocompleteMultiple => 0 &> </td>
 </tr>
 <tr>
 <td class="label"> <&|/l, $role->Name &>Remove [_1]</&>: </td>
-<td class="value"> <& /Elements/EmailInput, Name => "DeleteRT::CustomRole-" . $role->Id, Size=> 20, Default => $ARGS{"DeleteRT::CustomRole-" . $role->Id} &> </td>
+<td class="value"> <& /Elements/MultiUserRoleInput, role => $role, Name => "DeleteRT::CustomRole-" . $role->Id, Size => 20, Default => $ARGS{"DeleteRT::CustomRole-" . $role->Id}, AutocompleteMultiple => 0 &> </td>
 </tr>
 % }
 
diff --git a/share/html/SelfService/Create.html b/share/html/SelfService/Create.html
index 6bf192dc1..969c7eca9 100644
--- a/share/html/SelfService/Create.html
+++ b/share/html/SelfService/Create.html
@@ -71,7 +71,7 @@
 <&|/l&>Requestors</&>:
 </td>
 <td class="value">
-<& /Elements/EmailInput, Name => 'Requestors', Size => '20', Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &>
+<& /Elements/EmailInput, Name => 'Requestors', Size => '20', Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &>
 </td>
 </tr>
 <tr>
@@ -79,7 +79,7 @@
 <&|/l&>Cc</&>:
 </td>
 <td class="value">
-<& /Elements/EmailInput, Name => 'Cc', Size => '20', Default => $ARGS{Cc} || '', AutocompleteMultiple => 1 &>
+<& /Elements/EmailInput, Name => 'Cc', Size => '20', Default => $ARGS{Cc} || '', AutocompleteMultiple => 1, AutocompleteType => 'Principals' &>
 </td>
 </tr>
 <tr>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 638838eb7..4ef1b2f7a 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -149,7 +149,7 @@
 <&|/l&>Requestors</&>:
 </td>
 <td class="value" colspan="5">
-<& /Elements/EmailInput, Name => 'Requestors', Size => undef, Default => $ARGS{Requestors} // $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1 &>
+<& /Elements/EmailInput, Name => 'Requestors', Size => undef, Default => $ARGS{Requestors} // $session{CurrentUser}->EmailAddress, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &>
 % $m->callback( CallbackName => 'AfterRequestors', QueueObj => $QueueObj, ARGSRef => \%ARGS );
 </td>
 </tr>
@@ -157,7 +157,7 @@
 <td class="label">
 <&|/l&>Cc</&>:
 </td>
-<td class="value" colspan="5"><& /Elements/EmailInput, Name => 'Cc', Size => undef, Default => $ARGS{Cc}, AutocompleteMultiple => 1 &></td>
+<td class="value" colspan="5"><& /Elements/EmailInput, Name => 'Cc', Size => undef, Default => $ARGS{Cc}, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &></td>
 </tr>
 
 <tr>
@@ -173,7 +173,7 @@
 <td class="label">
 <&|/l&>Admin Cc</&>:
 </td>
-<td class="value" colspan="5"><& /Elements/EmailInput, Name => 'AdminCc', Size => undef, Default => $ARGS{AdminCc}, AutocompleteMultiple => 1 &></td>
+<td class="value" colspan="5"><& /Elements/EmailInput, Name => 'AdminCc', Size => undef, Default => $ARGS{AdminCc}, AutocompleteMultiple => 1, AutocompleteType => 'Principals' &></td>
 </tr>
 
 <tr>
@@ -192,7 +192,7 @@
 <td class="label">
 <% $role->Name %>:
 </td>
-<td class="value" colspan="5"><& /Elements/EmailInput, Name => $role->GroupType, Size => undef, Default => $ARGS{$role->GroupType}, AutocompleteMultiple => 1 &></td>
+<td class="value" colspan="5"><& /Elements/MultiUserRoleInput, role => $role, Default => $ARGS{$role->GroupType} &></td>
 </tr>
 
 % if ($role->EntryHint) {
diff --git a/share/html/Ticket/Elements/AddWatchers b/share/html/Ticket/Elements/AddWatchers
index fc57f2aae..606eeeea7 100644
--- a/share/html/Ticket/Elements/AddWatchers
+++ b/share/html/Ticket/Elements/AddWatchers
@@ -104,7 +104,7 @@
 <tr><td>
 <&/Elements/SelectWatcherType, Name => "WatcherTypeEmail" . $i, Queue => $Ticket->QueueObj &>
 </td><td>
-<& /Elements/EmailInput, Name => 'WatcherAddressEmail' . $i, Size => '20' &>
+<& /Elements/EmailInput, Name => 'WatcherAddressEmail' . $i, Size => '20', AutocompleteType => 'Principals' &>
 </td></tr>
 % }
 </table>

commit 9691d1313c52a36cae7834c3928bdcd16904a686
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 29 21:40:01 2018 +0800

    Support more characters for user/group names in non-single role input fields
    
    Previously, it's limited to \w+, now we support more non-word characters
    and even spaces between words like "Bilbo Baggins"

diff --git a/lib/RT/EmailParser.pm b/lib/RT/EmailParser.pm
index 93f6d2129..0eddb54dc 100644
--- a/lib/RT/EmailParser.pm
+++ b/lib/RT/EmailParser.pm
@@ -568,7 +568,7 @@ sub ParseEmailAddress {
         if ( $e->{'type'} eq 'mailbox' ) {
             push @addresses, $e->{'value'};
         }
-        elsif ( $e->{'value'} =~ /^(group:)?(\w+)$/ ) {
+        elsif ( $e->{'value'} =~ /^(group:)?(.+)$/ ) {
             my ( $is_group, $name ) = ( $1, $2 );
             if ( $is_group ) {
                 RT->Logger->warning( $e->{'value'} . " is a group, skipping" );
@@ -631,7 +631,7 @@ sub _ParseEmailAddress {
         } elsif ($e->{'type'} eq 'group end') {
             undef $in_group;
             next;
-        } elsif ($e->{'value'} =~ /^\s*(group\s*:)?\s*(\w+)\s*$/i) {
+        } elsif ($e->{'value'} =~ /^\s*(group\s*:)?\s*(\S.*?)\s*$/i) {
             my ( $is_group, $name ) = ( $1, $2 );
             $e->{'value'} = $in_group || $is_group ? "group:$name" : $name;
             push @entries, $e;
diff --git a/lib/RT/Record/Role/Roles.pm b/lib/RT/Record/Role/Roles.pm
index ec9eeac86..118752cd7 100644
--- a/lib/RT/Record/Role/Roles.pm
+++ b/lib/RT/Record/Role/Roles.pm
@@ -389,7 +389,7 @@ sub CanonicalizePrincipal {
                 if RT::EmailParser->IsRTAddress( $email );
         }
     } else {
-        if ( ( $args{User} || '' ) =~ /^\s*group\s*:\s*(\w+)\s*$/i ) {
+        if ( ( $args{User} || '' ) =~ /^\s*group\s*:\s*(\S.*?)\s*$/i ) {
             $args{Group} = $1;
             delete $args{User};
         }
@@ -656,7 +656,7 @@ sub ParseInputPrincipals {
                 RT::Logger->error( "Couldn't load or create user from email address " . $e->{'value'} . ", " . $msg );
             }
         }
-        elsif ( $e->{'value'} =~ /^(group:)?(\w+)$/ ) {
+        elsif ( $e->{'value'} =~ /^(group:)?(.+)$/ ) {
 
             my ( $is_group, $name ) = ( $1, $2 );
             if ( $is_group ) {

commit 901b59900759c8bd3317a2fef61b7e793b003343
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Aug 30 05:08:54 2018 +0800

    Test user/group names in non-single role input fields

diff --git a/t/web/ticket_role_input.t b/t/web/ticket_role_input.t
new file mode 100644
index 000000000..59bd31bf4
--- /dev/null
+++ b/t/web/ticket_role_input.t
@@ -0,0 +1,172 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+my ( $baseurl, $m ) = RT::Test->started_ok;
+ok $m->login, 'logged in as root';
+my $root = RT::User->new( RT->SystemUser );
+ok( $root->Load( 'root' ), 'load root user' );
+
+my $alice = RT::Test->load_or_create_user( Name => 'alice', EmailAddress => 'alice at example.com' );
+ok( $alice->id, 'created user alice' );
+
+my $bob = RT::Test->load_or_create_user( Name => 'bob', EmailAddress => 'bob at example.com' );
+ok( $bob->id, 'created user bob' );
+
+my $richard = RT::Test->load_or_create_user( Name => 'richard', EmailAddress => 'richard at example.com' );
+ok( $richard->id, 'created user richard' );
+
+my $group_foo = RT::Test->load_or_create_group( 'foo' );
+ok( $group_foo->id, 'created group foo' );
+
+my $group_admin_user = RT::Test->load_or_create_group( 'admin user' );
+ok( $group_admin_user->id, 'created group admin user' );
+
+my $queue = RT::Test->load_or_create_queue( Name => 'General' );
+ok $queue->id, 'loaded queue General';
+
+diag "Test ticket create page";
+{
+    $m->goto_create_ticket( $queue );
+    $m->submit_form_ok(
+        {
+            form_name => 'TicketCreate',
+            fields    => {
+                Subject    => 'test inputs on create',
+                Content    => 'test content',
+                Requestors => 'alice, root at localhost, group:' . $group_foo->id,
+                Cc         => 'richard at example.com, ' . $alice->id,
+                AdminCc    => 'group:admin user, bob',
+            },
+        },
+        'submit form TicketCreate'
+    );
+    $m->content_like( qr/Ticket \d+ created/, 'created ticket' );
+
+    my $ticket = RT::Test->last_ticket;
+    for my $member ( $root, $alice, $group_foo ) {
+        ok( $ticket->Requestor->HasMember( $member->PrincipalObj ), 'Requestor has member ' . $member->Name );
+    }
+
+    for my $member ( $alice, $richard ) {
+        ok( $ticket->Cc->HasMember( $member->PrincipalObj ), 'Cc has member ' . $member->Name );
+    }
+
+    for my $member ( $bob, $group_admin_user ) {
+        ok( $ticket->AdminCc->HasMember( $member->PrincipalObj ), 'AdminCc has member ' . $member->Name );
+    }
+}
+
+diag "Test ticket people page";
+{
+
+    my $ticket = RT::Test->create_ticket(
+        Queue   => $queue,
+        Subject => 'test inputs on people',
+        Content => 'test content',
+    );
+    $m->goto_ticket( $ticket->id, 'ModifyPeople' );
+
+    $m->submit_form_ok(
+        {
+            form_name => 'TicketPeople',
+            fields    => {
+                WatcherTypeEmail1    => 'Requestor',
+                WatcherAddressEmail1 => 'alice',
+                WatcherTypeEmail2    => 'AdminCc',
+                WatcherAddressEmail2 => 'group: foo',
+            },
+            button => 'SubmitTicket',
+        },
+        'submit form TicketPeople'
+    );
+
+    $m->text_contains( 'Added alice as Requestor for this ticket' );
+    $m->text_contains( 'Added foo as AdminCc for this ticket' );
+
+    ok( $ticket->Requestor->HasMember( $alice->PrincipalObj ),   'Requestor has member ' . $alice->Name );
+    ok( $ticket->AdminCc->HasMember( $group_foo->PrincipalObj ), 'AdminCc has member ' . $group_foo->Name );
+}
+
+diag "Test ticket update page";
+{
+
+    my $ticket = RT::Test->create_ticket(
+        Queue   => $queue,
+        Subject => 'test inputs on update',
+        Content => 'test content',
+    );
+    $m->goto_ticket( $ticket->id, 'Update' );
+
+    $m->submit_form_ok(
+        {
+            form_name => 'TicketUpdate',
+            fields    => {
+                UpdateContent => 'test content',
+                UpdateCc      => 'alice, bob at example.com',
+                UpdateBcc     => 'richard',
+            },
+            button => 'SubmitTicket',
+        },
+        'submit form TicketUpdate'
+    );
+    $m->content_contains('Comments added');
+
+    $m->follow_link_ok( { text => 'Show' }, 'get the outgoing email page' );
+    $m->content_contains( 'CC: alice at example.com, bob at example.com' );
+    $m->content_contains( 'BCC: richard at example.com' );
+}
+
+diag "Test ticket bulk update page";
+{
+
+    my @tickets = RT::Test->create_tickets(
+        {
+            Queue   => $queue,
+            Subject => 'test role inputs on bulk update',
+            Content => 'test content',
+        },
+        ( {} ) x 3
+    );
+
+    $m->get_ok( '/Search/Bulk.html?Rows=10&Query=Subject="test role inputs on bulk update"' );
+    $m->submit_form_ok(
+        {
+            form_name => 'BulkUpdate',
+            fields    => {
+                AddRequestor => 'alice',
+                AddAdminCc => 'group: admin user',
+            },
+        },
+        'submit form BulkUpdate'
+    );
+
+    $m->text_contains( 'Added alice as Requestor for this ticket' );
+    $m->text_contains( 'Added admin user as AdminCc for this ticket' );
+
+    for my $ticket ( @tickets ) {
+        ok( $ticket->Requestor->HasMember( $alice->PrincipalObj ), 'Requestor has member ' . $alice->Name );
+        ok( $ticket->AdminCc->HasMember( $group_admin_user->PrincipalObj ),
+            'AdminCc has member ' . $group_admin_user->Name );
+    }
+
+    $m->get_ok( '/Search/Bulk.html?Rows=10&Query=Subject="test role inputs on bulk update"' );
+    $m->submit_form_ok(
+        {
+            form_name => 'BulkUpdate',
+            fields    => {
+                DeleteRequestor => $alice->id,
+                DeleteAdminCc => 'group: ' . $group_admin_user->id,
+            },
+        },
+        'submit form BulkUpdate'
+    );
+    $m->text_contains( 'admin user is no longer AdminCc for this ticket' );
+    for my $ticket ( @tickets ) {
+        ok( !$ticket->AdminCc->HasMember( $group_admin_user->PrincipalObj ),
+            'AdminCc has no member ' . $group_admin_user->Name );
+    }
+}
+
+done_testing;

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


More information about the rt-commit mailing list