[Bps-public-commit] RT-Extension-MandatoryOnTransition branch, roles, created. 0.17-15-g0d4a1d2

? sunnavy sunnavy at bestpractical.com
Fri Mar 1 16:38:49 EST 2019


The branch, roles has been created
        at  0d4a1d2f1c58ddfea65f1528ec976d5c9b38afed (commit)

- Log -----------------------------------------------------------------
commit 7e53f384837acc6c10338ff1d8a10bd76578eff6
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Thu Feb 14 15:29:39 2019 -0500

    Support custom roles as manadatory on transition
    
    Can specify if a custom role needs to have a value in order for a
    ticket to transition from one status to another.

diff --git a/README b/README
index ad7e034..10ec6f6 100644
--- a/README
+++ b/README
@@ -96,7 +96,7 @@ CONFIGURATION
 
         Set( %MandatoryOnTransition,
             'QueueName' => {
-                'from -> to' => [ 'BasicField', 'CF.MyField', ],
+                'from -> to' => [ 'BasicField', 'CF.MyField', 'CustomRole.MyRole' ],
             },
         );
 
@@ -115,7 +115,8 @@ CONFIGURATION
 
         Set( %MandatoryOnTransition,
             Helpdesk => {
-                '* -> resolved' => ['TimeWorked', 'CF.Resolution'],
+                '* -> resolved'      => ['TimeWorked', 'CF.Resolution', 'CustomRole.Analyst'],
+                'CustomRole.Analyst' => {transition => '* -> open', group => 'Engineering'},
             },
             '*' => {
                 '* -> resolved' => ['CF.Category'],
diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index b00e7d7..82c3b0f 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
@@ -2,11 +2,11 @@
 $Ticket
 </%args>
 <%init>
-my ($core, $cfs) = RT::Extension::MandatoryOnTransition->RequiredFields(
+my ($core, $cfs, $roles) = RT::Extension::MandatoryOnTransition->RequiredFields(
     Ticket  => $Ticket,
     To      => $ARGS{'Status'} || $ARGS{'DefaultStatus'},
 );
-return unless @$cfs;
+return unless @$cfs or @$roles;
 
 my $comp = '/Elements/EditCustomFields';
 my %obj_args = ( Object => $Ticket );
@@ -17,11 +17,47 @@ if (!$m->comp_exists('/Elements/EditCustomFields')) {
     %obj_args = ( TicketObj => $Ticket );
 }
 
+my @roles;
+foreach my $role (@{$roles}) {
+
+    if ( $role =~ s/^CustomRole\.//i ) {
+        my $role_object = RT::CustomRole->new($session{CurrentUser});
+        my ($ret, $msg) = $role_object->Load($role);
+        RT::Logger->error("Unable to load custom role $role: $msg") unless $ret;
+
+        ($ret, $msg) = $role_group->LoadRoleGroup(
+            Object => $Ticket->QueueObj, Name => $role_object->GroupType);
+        RT::Logger->error("Unable to load role group: " . $role_object->GroupType . " $msg")
+            unless $role_group->Id;
+        $crs{$role_group->Name} = { Id => $role_object->Id, Name => $role_object->Name };
+
+        push @roles, $role_group if $role_group->Id;
+    }
+}
 </%init>
 %# 'Named' is handled by this extension in the MassageCustomFields callback
-<& $comp,
-    %ARGS,
-    %obj_args,
-    InTable     => 1,
-    Named       => $cfs,
-    &>
+% if ( @$cfs ) {
+    <& $comp,
+        %ARGS,
+        %obj_args,
+        InTable     => 1,
+        Named       => $cfs,
+        &>
+% }
+% if ( @$roles ) {
+%     foreach my $role (@roles) {
+        <tr>
+            <td class="label">Add <% $crs{$role->Name}->{Name} %>:</td>
+            <td class="entry">
+                <& /Elements/EmailInput,
+                    Name               => 'RT::CustomRole-' . $crs{$role->Name}->{Id},
+                    Autocomplete       => 1,
+                    AutocompleteNobody => 0,
+                    AutocompleteReturn => "Email",
+                    Size               => 20,
+                    Default            => $ARGS{'RT::CustomRole-' . $crs{$role->Name}->{Id}},
+                &>
+            </td>
+        </tr>
+%     }
+% }
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index d238c18..8cfd4e3 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -127,7 +127,7 @@ config option.  This option takes the generic form of:
 
     Set( %MandatoryOnTransition,
         'QueueName' => {
-            'from -> to' => [ 'BasicField', 'CF.MyField', ],
+            'from -> to' => [ 'BasicField', 'CF.MyField', 'CustomRole.MyRole' ],
         },
     );
 
@@ -146,7 +146,8 @@ Category selection before resolving tickets in every other queue.
 
     Set( %MandatoryOnTransition,
         Helpdesk => {
-            '* -> resolved' => ['TimeWorked', 'CF.Resolution'],
+            '* -> resolved'      => ['TimeWorked', 'CF.Resolution', 'CustomRole.Analyst'],
+            'CustomRole.Analyst' => {transition => '* -> open', group => 'Engineering'},
         },
         '*' => {
             '* -> resolved' => ['CF.Category'],
@@ -275,9 +276,10 @@ our %CORE_FOR_CREATE = (
 
 =head3 RequiredFields
 
-Returns two array refs of required fields for the described status transition.
-The first is core fields, the second is CF names.  Returns empty array refs
-on error or if nothing is required.
+Returns three array refs of required fields for the described status transition.
+The first is core fields, the second is CF names, the third is roles.  Returns
+empty array refs on error or if nothing is required. A forth parameter is a
+hashref of must-have values for custom fields.
 
 Takes a paramhash with the keys Ticket, Queue, From, and To.  Ticket should be
 an object.  Queue should be a name.  From and To should be statuses.  If you
@@ -340,6 +342,8 @@ sub RequiredFields {
     my @core = grep { !/^CF\./i && $core_supported{$_} } @$required;
     my @cfs  =  map { /^CF\.(.+)$/i; $1; }
                grep { /^CF\./i } @$required;
+    my @roles = map { /^(:?[CustomRole\.]?.+)$/i; $1; }
+               grep { /^CustomRole\./i } @$required;
 
     # Pull out any must_be or must_not_be rules
     my %cf_must_values = ();
@@ -359,7 +363,8 @@ sub RequiredFields {
             }
         }
     }
-    return (\@core, \@cfs, \%cf_must_values);
+
+    return (\@core, \@cfs, \@roles, \%cf_must_values);
 }
 
 =head3 CheckMandatoryFields
@@ -426,15 +431,14 @@ sub CheckMandatoryFields {
         return \@errors;
     }
 
-    my ($core, $cfs, $must_values) = $self->RequiredFields(
+    my ($core, $cfs, $roles, $must_values) = $self->RequiredFields(
         Ticket  => $args{'Ticket'},
         Queue   => $args{'Queue'} ? $args{'Queue'}->Name : undef,
         From    => $args{'From'},
         To      => $args{'To'},
         NewQueue => $$ARGSRef{'Queue'},
     );
-
-    return \@errors unless @$core or @$cfs;
+    return \@errors unless @$core or @$cfs or @$roles;
 
     my $transition =  ($args{'From'} ||'') ne ($args{'To'} || '') ? 'Status' : 'Queue';
 
@@ -500,6 +504,44 @@ sub CheckMandatoryFields {
             $label, $CurrentUser->loc($transition),  $CurrentUser->loc($field_label{$transition}));
     }
 
+    if (@$roles and $args{'To'}) {
+        foreach my $role (@$roles) {
+            my $role_values;
+            my ( $role_arg, $role_full ) = ( $role, $role );
+
+            if ( $role =~ s/^CustomRole\.//i ) {
+                my $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
+
+                my ( $ret, $msg ) = $role_object->Load($role);
+                push @errors, $CurrentUser->loc("Could not load object for [_1]", $role) unless $ret;
+                RT::Logger->error("Unable to load custom role $role: $msg") unless $ret;
+                next unless $role_object->Id;
+
+                $role_arg = 'RT::CustomRole-' . $role_object->Id;
+
+                $role_values = $args{Ticket}->RoleGroup( $role_object->GroupType );
+                RT::Logger->error("Unable to load role group for " . $role_object->GroupType)
+                    unless $role_values;
+            }
+
+            my @role_values;
+            if ( ref $role_values eq 'RT::Group' ) {
+                push @role_values, $role_values->MemberEmailAddresses
+                    if $role_values->MemberEmailAddresses;
+            }
+            push @role_values, $ARGSRef->{$role_arg} if $ARGSRef->{$role_arg};
+
+            if ( not scalar @role_values ) {
+                push @errors, $CurrentUser->loc("[_1] is required when changing [_2] to [_3]",
+                    $role,
+                    $CurrentUser->loc($transition),
+                    $CurrentUser->loc( $args{'To'} )
+                );
+                next;
+            }
+        }
+    }
+
     return \@errors unless @$cfs;
 
     if ( not $CFs ){

commit 17728a4bb85907e195ebb897b7bc285f100db420
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Feb 28 06:47:55 2019 +0800

    Refactor and clean up code
    
    Note that to render custom role inputs, there is no need to load ticket
    role groups beforehand, and it might not work either considering some
    old tickets probably don't have new role groups created yet.

diff --git a/README b/README
index 10ec6f6..f7f16bc 100644
--- a/README
+++ b/README
@@ -116,7 +116,6 @@ CONFIGURATION
         Set( %MandatoryOnTransition,
             Helpdesk => {
                 '* -> resolved'      => ['TimeWorked', 'CF.Resolution', 'CustomRole.Analyst'],
-                'CustomRole.Analyst' => {transition => '* -> open', group => 'Engineering'},
             },
             '*' => {
                 '* -> resolved' => ['CF.Category'],
@@ -215,9 +214,10 @@ IMPLEMENTATION DETAILS
 
   Methods
    RequiredFields
-    Returns two array refs of required fields for the described status
-    transition. The first is core fields, the second is CF names. Returns
-    empty array refs on error or if nothing is required.
+    Returns three array refs of required fields for the described status
+    transition. The first is core fields, the second is CF names, the third
+    is roles. Returns empty array refs on error or if nothing is required. A
+    forth parameter is a hashref of must-have values for custom fields.
 
     Takes a paramhash with the keys Ticket, Queue, From, and To. Ticket
     should be an object. Queue should be a name. From and To should be
diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index 82c3b0f..ea1305b 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
@@ -23,15 +23,12 @@ foreach my $role (@{$roles}) {
     if ( $role =~ s/^CustomRole\.//i ) {
         my $role_object = RT::CustomRole->new($session{CurrentUser});
         my ($ret, $msg) = $role_object->Load($role);
-        RT::Logger->error("Unable to load custom role $role: $msg") unless $ret;
+        unless ( $ret ) {
+            RT::Logger->error("Unable to load custom role $role: $msg");
+            next;
+        }
 
-        ($ret, $msg) = $role_group->LoadRoleGroup(
-            Object => $Ticket->QueueObj, Name => $role_object->GroupType);
-        RT::Logger->error("Unable to load role group: " . $role_object->GroupType . " $msg")
-            unless $role_group->Id;
-        $crs{$role_group->Name} = { Id => $role_object->Id, Name => $role_object->Name };
-
-        push @roles, $role_group if $role_group->Id;
+        push @roles, $role_object;
     }
 }
 </%init>
@@ -47,15 +44,15 @@ foreach my $role (@{$roles}) {
 % if ( @$roles ) {
 %     foreach my $role (@roles) {
         <tr>
-            <td class="label">Add <% $crs{$role->Name}->{Name} %>:</td>
+            <td class="label">Add <% $role->Name %>:</td>
             <td class="entry">
                 <& /Elements/EmailInput,
-                    Name               => 'RT::CustomRole-' . $crs{$role->Name}->{Id},
+                    Name               => $role->GroupType,
                     Autocomplete       => 1,
                     AutocompleteNobody => 0,
                     AutocompleteReturn => "Email",
                     Size               => 20,
-                    Default            => $ARGS{'RT::CustomRole-' . $crs{$role->Name}->{Id}},
+                    Default            => $ARGS{$role->GroupType},
                 &>
             </td>
         </tr>
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index 8cfd4e3..dcdc135 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -147,7 +147,6 @@ Category selection before resolving tickets in every other queue.
     Set( %MandatoryOnTransition,
         Helpdesk => {
             '* -> resolved'      => ['TimeWorked', 'CF.Resolution', 'CustomRole.Analyst'],
-            'CustomRole.Analyst' => {transition => '* -> open', group => 'Engineering'},
         },
         '*' => {
             '* -> resolved' => ['CF.Category'],
@@ -320,12 +319,12 @@ sub RequiredFields {
 
     my ($from_queue, $to_queue) = ($args{Queue}, $args{ToQueue} || $args{Queue});
 
-    return ([], []) unless ($from and $to) or ($from_queue and $to_queue );
+    return ([], [], []) unless ($from and $to) or ($from_queue and $to_queue );
 
     my %config = ();
     %config = $self->Config($args{Queue});
 
-    return ([], []) unless %config;
+    return ([], [], []) unless %config;
 
    $to ||= '';
    $from ||= '';
@@ -507,22 +506,27 @@ sub CheckMandatoryFields {
     if (@$roles and $args{'To'}) {
         foreach my $role (@$roles) {
             my $role_values;
-            my ( $role_arg, $role_full ) = ( $role, $role );
+            my $role_arg = $role;
 
             if ( $role =~ s/^CustomRole\.//i ) {
                 my $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
 
                 my ( $ret, $msg ) = $role_object->Load($role);
                 push @errors, $CurrentUser->loc("Could not load object for [_1]", $role) unless $ret;
-                RT::Logger->error("Unable to load custom role $role: $msg") unless $ret;
-                next unless $role_object->Id;
+                unless ( $ret ) {
+                    RT::Logger->error("Unable to load custom role $role: $msg");
+                    next;
+                }
 
-                $role_arg = 'RT::CustomRole-' . $role_object->Id;
+                $role_arg = $role_object->GroupType;
 
                 $role_values = $args{Ticket}->RoleGroup( $role_object->GroupType );
                 RT::Logger->error("Unable to load role group for " . $role_object->GroupType)
                     unless $role_values;
             }
+            else {
+                next;
+            }
 
             my @role_values;
             if ( ref $role_values eq 'RT::Group' ) {

commit 22bbbed513ff045ef50a23fa63e8839478c3bb53
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Feb 28 06:54:07 2019 +0800

    No need to render single-member custom roles as they already exist

diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index ea1305b..0b1ea15 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
@@ -28,6 +28,9 @@ foreach my $role (@{$roles}) {
             next;
         }
 
+        # Update page already contains single member role groups
+        next if $role_object->SingleValue;
+
         push @roles, $role_object;
     }
 }

commit 09476d7cc2ea3884645360cf49be63c9c37ee748
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Feb 28 07:17:55 2019 +0800

    Localize "Add ..."

diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index 0b1ea15..1566ae3 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
@@ -47,7 +47,7 @@ foreach my $role (@{$roles}) {
 % if ( @$roles ) {
 %     foreach my $role (@roles) {
         <tr>
-            <td class="label">Add <% $role->Name %>:</td>
+            <td class="label"><&|/l, loc($role->Name) &>Add [_1]</&>:</td>
             <td class="entry">
                 <& /Elements/EmailInput,
                     Name               => $role->GroupType,

commit e9c0920827dc735273dfd44617af170c406f2589
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Feb 28 07:33:49 2019 +0800

    Store user objects in case some user may not have email addresses
    
    Nobody is quite special, let's explicitly ignore it here.

diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index dcdc135..7ef426a 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -530,8 +530,7 @@ sub CheckMandatoryFields {
 
             my @role_values;
             if ( ref $role_values eq 'RT::Group' ) {
-                push @role_values, $role_values->MemberEmailAddresses
-                    if $role_values->MemberEmailAddresses;
+                push @role_values, grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
             }
             push @role_values, $ARGSRef->{$role_arg} if $ARGSRef->{$role_arg};
 

commit 28a916328d7478523d013411539954c691735fb7
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Feb 28 07:47:58 2019 +0800

    Validate passed custom role values
    
    Otherwise we could add "Nobody" to multi-members custom roles.
    Single-member custom roles are also taken care of.

diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index 7ef426a..c3ce5f4 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -508,8 +508,11 @@ sub CheckMandatoryFields {
             my $role_values;
             my $role_arg = $role;
 
+            my $role_object;
+            my @role_values;
+
             if ( $role =~ s/^CustomRole\.//i ) {
-                my $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
+                $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
 
                 my ( $ret, $msg ) = $role_object->Load($role);
                 push @errors, $CurrentUser->loc("Could not load object for [_1]", $role) unless $ret;
@@ -520,19 +523,56 @@ sub CheckMandatoryFields {
 
                 $role_arg = $role_object->GroupType;
 
-                $role_values = $args{Ticket}->RoleGroup( $role_object->GroupType );
-                RT::Logger->error("Unable to load role group for " . $role_object->GroupType)
-                    unless $role_values;
+                # No need to load current value for single-member custom roles
+                # as new passed value will override current one
+                if ( !$role_object->SingleValue ) {
+                    $role_values = $args{Ticket}->RoleGroup( $role_object->GroupType );
+                    if ( $role_values ) {
+                        push @role_values, grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
+                    }
+                    else {
+                        RT::Logger->error( "Unable to load role group for " . $role_object->GroupType );
+                    }
+                }
             }
             else {
                 next;
             }
 
-            my @role_values;
-            if ( ref $role_values eq 'RT::Group' ) {
-                push @role_values, grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
+            if ( my $value = $ARGSRef->{$role_arg} ) {
+
+                # RT can automatically create users with email addresses.
+                if ( $value =~ /@/ ) {
+                    push @role_values, $value;
+                }
+                else {
+                    my $user = RT::User->new( RT->SystemUser );
+                    $user->Load($value);
+
+                    if ( $user->id ) {
+                        if ( $role_object->SingleValue ) {
+                            if ( $user->id == $RT::Nobody->id ) {
+                                undef @role_values if @role_values;
+                            }
+                            else {
+                                push @role_values, $user;
+                            }
+                        }
+                        else {
+                            push @role_values, $user unless $user->id == $RT::Nobody->id;
+                        }
+                    }
+                    else {
+                        push @errors, $CurrentUser->loc( "Could not load user: [_1]", $value );
+                    }
+                }
+            }
+            else {
+                if ( $role_object->SingleValue ) {
+                    undef @role_values if @role_values;
+                }
+                delete $ARGSRef->{$role_arg};
             }
-            push @role_values, $ARGSRef->{$role_arg} if $ARGSRef->{$role_arg};
 
             if ( not scalar @role_values ) {
                 push @errors, $CurrentUser->loc("[_1] is required when changing [_2] to [_3]",

commit 6fa3fb132d0b5d79cf8906e23462ffe1bdd13e04
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Feb 15 12:02:09 2019 -0500

    Support making all core roles mandatory on transition
    
    Note that for multiple-members roles, input names are like
    "AddRequestors", "AddCc", "AddRT::CustomRole-3", etc.
    
    For custom roles, either "RT::CustomRole-3" or "AddRT::CustomRole-3"
    could work, changing to "Add..." is just for consistency.

diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index 1566ae3..42483c2 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
@@ -19,6 +19,7 @@ if (!$m->comp_exists('/Elements/EditCustomFields')) {
 
 my @roles;
 foreach my $role (@{$roles}) {
+    next if $role eq 'Owner';
 
     if ( $role =~ s/^CustomRole\.//i ) {
         my $role_object = RT::CustomRole->new($session{CurrentUser});
@@ -32,6 +33,16 @@ foreach my $role (@{$roles}) {
         next if $role_object->SingleValue;
 
         push @roles, $role_object;
+    } else {
+        # Handle core roles like Requestor, Cc, AdminCc
+        my $role_group = RT::Group->new($session{CurrentUser});
+        my ($ret, $msg) = $role_group->LoadRoleGroup(Object => $Ticket, Name => $role);
+        if ( $ret ) {
+            push @roles, $role_group;
+        }
+        else {
+            RT::Logger->error("Unable to load role $role: $msg");
+        }
     }
 }
 </%init>
@@ -46,16 +57,17 @@ foreach my $role (@{$roles}) {
 % }
 % if ( @$roles ) {
 %     foreach my $role (@roles) {
+%       my $input = 'Add' . ( $role->isa('RT::Group') ? $role->Name : $role->GroupType );
         <tr>
             <td class="label"><&|/l, loc($role->Name) &>Add [_1]</&>:</td>
             <td class="entry">
                 <& /Elements/EmailInput,
-                    Name               => $role->GroupType,
+                    Name               => $input,
                     Autocomplete       => 1,
                     AutocompleteNobody => 0,
                     AutocompleteReturn => "Email",
                     Size               => 20,
-                    Default            => $ARGS{$role->GroupType},
+                    Default            => $ARGS{$input},
                 &>
             </td>
         </tr>
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index c3ce5f4..1934e4f 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -257,18 +257,16 @@ pair to %CORE_FOR_UPDATE and/or %CORE_FOR_CREATE.
 
 =cut
 
-our @CORE_SUPPORTED  = qw(Content TimeWorked TimeTaken Owner);
-our @CORE_TICKET     = qw(TimeWorked Owner);
+our @CORE_SUPPORTED  = qw(Content TimeWorked TimeTaken);
+our @CORE_TICKET     = qw(TimeWorked);
 our %CORE_FOR_UPDATE = (
     TimeWorked  => 'UpdateTimeWorked',
     TimeTaken   => 'UpdateTimeWorked',
     Content     => 'UpdateContent',
-    Owner       => 'Owner',
 );
 our %CORE_FOR_CREATE = (
     TimeWorked  => 'TimeWorked',
     Content     => 'Content',
-    Owner       => 'Owner',
 );
 
 =head2 Methods
@@ -342,7 +340,7 @@ sub RequiredFields {
     my @cfs  =  map { /^CF\.(.+)$/i; $1; }
                grep { /^CF\./i } @$required;
     my @roles = map { /^(:?[CustomRole\.]?.+)$/i; $1; }
-               grep { /^CustomRole\./i } @$required;
+               grep { /^CustomRole\.|^AdminCc|^Cc|^Requestor|^Owner/i } @$required;
 
     # Pull out any must_be or must_not_be rules
     my %cf_must_values = ();
@@ -460,36 +458,6 @@ sub CheckMandatoryFields {
             : $CORE_FOR_CREATE{$field};
         next unless $arg;
 
-        if ($field eq 'Owner') {
-            my $value;
-
-            # There are 2 Owner fields on Jumbo page, copied the same handling from it.
-            if (ref $ARGSRef->{$arg}) {
-                foreach my $owner (@{$ARGSRef->{$arg}}) {
-                    if (defined($owner) && $owner =~ /\D/) {
-                        $value = $owner unless ($args{'Ticket'}->OwnerObj->Name eq $owner);
-                    }
-                    elsif (length $owner) {
-                        $value = $owner unless ($args{'Ticket'}->OwnerObj->id == $owner);
-                    }
-                }
-            }
-            else {
-                $value = $ARGSRef->{$arg};
-            }
-
-            if (($value || $args{'Ticket'}->$field()) == $RT::Nobody->id) {
-                push @errors,
-                  $CurrentUser->loc(
-                    "[_1] is required when changing [_2] to [_3]",
-                    $field,
-                    $CurrentUser->loc($transition),
-                    $CurrentUser->loc($field_label{$transition})
-                  );
-                next;
-            }
-        }
-
         next if defined $ARGSRef->{$arg} and length $ARGSRef->{$arg};
 
         # Do we have a value currently?
@@ -511,6 +479,8 @@ sub CheckMandatoryFields {
             my $role_object;
             my @role_values;
 
+            my $value;
+
             if ( $role =~ s/^CustomRole\.//i ) {
                 $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
 
@@ -526,6 +496,8 @@ sub CheckMandatoryFields {
                 # No need to load current value for single-member custom roles
                 # as new passed value will override current one
                 if ( !$role_object->SingleValue ) {
+
+                    $role_arg = "Add$role_arg";
                     $role_values = $args{Ticket}->RoleGroup( $role_object->GroupType );
                     if ( $role_values ) {
                         push @role_values, grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
@@ -536,39 +508,73 @@ sub CheckMandatoryFields {
                 }
             }
             else {
-                next;
-            }
+                if ( $role eq 'Owner' ) {
 
-            if ( my $value = $ARGSRef->{$role_arg} ) {
+                    # There are 2 Owner fields on Jumbo page, copied the same handling from it.
+                    if ( ref $ARGSRef->{$role} ) {
+                        foreach my $owner ( @{ $ARGSRef->{$role} } ) {
+                            if ( defined($owner) && $owner =~ /\D/ ) {
+                                $value = $owner unless ( $args{'Ticket'}->OwnerObj->Name eq $owner );
+                            }
+                            elsif ( length $owner ) {
+                                $value = $owner unless ( $args{'Ticket'}->OwnerObj->id == $owner );
+                            }
+                        }
+                    }
+                    else {
+                        $value = $ARGSRef->{$role};
+                    }
+                }
+                else {
+                    $role_arg = "Add$role";
+                }
 
-                # RT can automatically create users with email addresses.
-                if ( $value =~ /@/ ) {
-                    push @role_values, $value;
+                $role_values = RT::Group->new( $args{Ticket}->CurrentUser );
+                my ( $ret, $msg ) = $role_values->LoadRoleGroup(
+                    Object => $args{Ticket},
+                    Name   => $role,
+                );
+                if ( $ret ) {
+                    push @role_values, grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
                 }
                 else {
-                    my $user = RT::User->new( RT->SystemUser );
-                    $user->Load($value);
+                    push @errors, $CurrentUser->loc("Failed to load role $role for ticket");
+                }
+            }
 
-                    if ( $user->id ) {
-                        if ( $role_object->SingleValue ) {
-                            if ( $user->id == $RT::Nobody->id ) {
-                                undef @role_values if @role_values;
-                            }
-                            else {
-                                push @role_values, $user;
-                            }
+            $value = $ARGSRef->{$role_arg} unless $role eq 'Owner';
+
+            if ($value) {
+
+                my $user = RT::User->new( RT->SystemUser );
+                $user->Load($value);
+                $user->LoadByEmail($value) unless $user->id;
+
+                if ( $user->id ) {
+                    if ( $role eq 'Owner' || ( $role_object && $role_object->SingleValue ) ) {
+                        if ( $user->id == $RT::Nobody->id ) {
+                            undef @role_values if @role_values;
                         }
                         else {
-                            push @role_values, $user unless $user->id == $RT::Nobody->id;
+                            @role_values = $user;
                         }
                     }
+                    else {
+                        push @role_values, $user unless $user->id == $RT::Nobody->id;
+                    }
+                }
+                else {
+                    # RT can automatically create users with email addresses.
+                    if ( $value =~ /@/ ) {
+                        push @role_values, $value;
+                    }
                     else {
                         push @errors, $CurrentUser->loc( "Could not load user: [_1]", $value );
                     }
                 }
             }
             else {
-                if ( $role_object->SingleValue ) {
+                if ( $role_object && $role_object->SingleValue ) {
                     undef @role_values if @role_values;
                 }
                 delete $ARGSRef->{$role_arg};

commit 6372befe64924122ddd3673390177d5db37a7a2c
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Feb 15 13:22:26 2019 -0500

    Support requiring a role to be a member of a group
    
    Provide a group for the role specific key in the
    configuration hash to require a role to have
    a member in at least one of the groups.

diff --git a/README b/README
index f7f16bc..571d26d 100644
--- a/README
+++ b/README
@@ -216,8 +216,12 @@ IMPLEMENTATION DETAILS
    RequiredFields
     Returns three array refs of required fields for the described status
     transition. The first is core fields, the second is CF names, the third
-    is roles. Returns empty array refs on error or if nothing is required. A
-    forth parameter is a hashref of must-have values for custom fields.
+    is roles. Returns empty array refs on error or if nothing is required.
+
+    A fourth returned parameter is a hashref of must-have values for custom
+    fields.
+
+    The fifth parameter is a hashref of groups a role member must be in.
 
     Takes a paramhash with the keys Ticket, Queue, From, and To. Ticket
     should be an object. Queue should be a name. From and To should be
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index 1934e4f..677f304 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -275,8 +275,11 @@ our %CORE_FOR_CREATE = (
 
 Returns three array refs of required fields for the described status transition.
 The first is core fields, the second is CF names, the third is roles.  Returns
-empty array refs on error or if nothing is required. A forth parameter is a
-hashref of must-have values for custom fields.
+empty array refs on error or if nothing is required.
+
+A fourth returned parameter is a hashref of must-have values for custom fields.
+
+The fifth parameter is a hashref of groups a role member must be in.
 
 Takes a paramhash with the keys Ticket, Queue, From, and To.  Ticket should be
 an object.  Queue should be a name.  From and To should be statuses.  If you
@@ -361,7 +364,38 @@ sub RequiredFields {
         }
     }
 
-    return (\@core, \@cfs, \@roles, \%cf_must_values);
+    my %role_group_values;
+    my $cr = RT::CustomRole->new(RT->SystemUser);
+    foreach my $role (@roles){
+        if ( $role =~ /^CustomRole\.(.*)/i ) {
+            my $role_name = $1;
+            my ($ret, $msg) = $cr->Load($role_name);
+            if ( not $cr and $cr->Id ) {
+                RT::Logger->error("Could not load Custom role $role_name: $msg");
+                @roles = grep { $_ ne $role } @roles;
+                next;
+            } elsif ( not $cr->IsAdded($args{Ticket}->QueueObj->Id) or $cr->Disabled ) {
+                RT::Logger->error("Custom role $role_name is not applied to: " . $args{Ticket}->QueueObj->Name );
+                @roles = grep { $_ ne $role } @roles;
+                next;
+            }
+        }
+        if ( $config{$role} ){
+            my $transition = $config{$role}->{'transition'};
+            unless ( $transition ){
+                RT->Logger->error("No transition defined in group rules for $role");
+                next;
+            }
+
+            if ( $transition eq "$from -> $to"
+                || $transition eq "* -> $to"
+                || $transition eq "$from -> *" ) {
+
+                $role_group_values{$role} = $config{$role};
+            }
+        }
+    }
+    return (\@core, \@cfs, \@roles, \%cf_must_values, \%role_group_values);
 }
 
 =head3 CheckMandatoryFields
@@ -428,7 +462,7 @@ sub CheckMandatoryFields {
         return \@errors;
     }
 
-    my ($core, $cfs, $roles, $must_values) = $self->RequiredFields(
+    my ($core, $cfs, $roles, $must_values, $role_group_values) = $self->RequiredFields(
         Ticket  => $args{'Ticket'},
         Queue   => $args{'Queue'} ? $args{'Queue'}->Name : undef,
         From    => $args{'From'},
@@ -475,19 +509,21 @@ sub CheckMandatoryFields {
         foreach my $role (@$roles) {
             my $role_values;
             my $role_arg = $role;
+            my $role_name = $role;
 
             my $role_object;
             my @role_values;
 
             my $value;
 
-            if ( $role =~ s/^CustomRole\.//i ) {
+            if ( $role =~ /^CustomRole\.(.+)/ ) {
+                $role_name = $1;
                 $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
 
-                my ( $ret, $msg ) = $role_object->Load($role);
-                push @errors, $CurrentUser->loc("Could not load object for [_1]", $role) unless $ret;
+                my ( $ret, $msg ) = $role_object->Load($role_name);
+                push @errors, $CurrentUser->loc("Could not load object for [_1]", $role_name) unless $ret;
                 unless ( $ret ) {
-                    RT::Logger->error("Unable to load custom role $role: $msg");
+                    RT::Logger->error("Unable to load custom role $role_name: $msg");
                     next;
                 }
 
@@ -580,9 +616,41 @@ sub CheckMandatoryFields {
                 delete $ARGSRef->{$role_arg};
             }
 
+            # Check for mandatory group configuration, supports multiple groups where only
+            # one true case needs to be found.
+            if ( $role_group_values->{$role}->{group} ) {
+                my $has_valid_member;
+
+                foreach my $group_name ( @{ $role_group_values->{$role}->{group} } ) {
+                    my $group = RT::Group->new( RT->SystemUser );
+
+                    my ( $ret, $msg ) = $group->LoadUserDefinedGroup($group_name);
+                    unless ( $ret ) {
+                        RT::Logger->error("Failed to load group: $group_name : $msg");
+                        next;
+                    }
+
+                    foreach my $member (@role_values) {
+                        next unless ref $member; # Only check users alrady exist
+
+                        $has_valid_member = $group->HasMemberRecursively( $member->Id );
+                        last if $has_valid_member;
+                    }
+                    last if $has_valid_member;
+                }
+
+                unless ( $has_valid_member ) {
+                    my $roles = join( ' or ', @{ $role_group_values->{$role}->{group} } );
+                    push @errors,
+                        $CurrentUser->loc( "A member of group [_1] is required for role: [_2]", $roles, $role_name );
+                    next;
+                }
+            }
+
+
             if ( not scalar @role_values ) {
                 push @errors, $CurrentUser->loc("[_1] is required when changing [_2] to [_3]",
-                    $role,
+                    $role_name,
                     $CurrentUser->loc($transition),
                     $CurrentUser->loc( $args{'To'} )
                 );

commit 9f9ba4d1e7d77d304d7cd538f91f669e8de94fc4
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Fri Jan 11 11:37:10 2019 -0500

    Update documentation

diff --git a/README b/README
index 571d26d..7a9a1d1 100644
--- a/README
+++ b/README
@@ -36,10 +36,6 @@ DESCRIPTION
     TimeTaken
         Requires that the Worked field on the update page is non-zero.
 
-    Owner
-        Requires that the ticket has a real Owner or the real Owner will be
-        set on the update page.
-
     A larger set of basic fields may be supported in future releases. If
     you'd like to see additional fields added, please email your request to
     the bug address at the bottom of this documentation.
@@ -125,6 +121,31 @@ CONFIGURATION
     The transition syntax is similar to that found in RT's Lifecycles. See
     perldoc /opt/rt4/etc/RT_Config.pm.
 
+  Requiring role values
+    You can require any core or custom role on a RT::Ticket object, below is
+    an example of requiring a custom role "customer" be set on transition
+    from open and the owner also be set for the ticket on transition from a
+    status of open.
+
+        Set( %MandatoryOnTransition,
+            'General' => {
+                '* -> resolved' => ['CustomRole.customer', 'Owner'],
+            },
+        );
+
+  Role Membership in a Group
+    Roles can require the members of the role to also be a member of a group
+    before satisfying to mandatory condition. Below we require that the
+    Owner role be set and that the member it is set to is a member of the
+    group 'SupportReps' or 'Admins'.
+
+        Set( %MandatoryOnTransition,
+            'General' => {
+                'open -> *' => ['Owner'],
+                'Owner' => { transition => 'open -> *', group => ['SupportReps', 'Admins'] },
+            }
+        );
+
   Restrictions on Queue Transitions
     The default behavior for MandatoryOnTransition operates on status
     transitions, so a change from new to open or from open to resolved. It
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index 677f304..22dba40 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -50,11 +50,6 @@ field on the update page.
 
 Requires that the Worked field on the update page is non-zero.
 
-=item Owner
-
-Requires that the ticket has a real Owner or the real Owner will be set on
-the update page.
-
 =back
 
 A larger set of basic fields may be supported in future releases.  If you'd
@@ -156,6 +151,32 @@ Category selection before resolving tickets in every other queue.
 The transition syntax is similar to that found in RT's Lifecycles.  See
 C<perldoc /opt/rt4/etc/RT_Config.pm>.
 
+=head2 Requiring role values
+
+You can require any core or custom role on a RT::Ticket object, below is an
+example of requiring a custom role "customer" be set on transition from open
+and the owner also be set for the ticket on transition from a status of open.
+
+    Set( %MandatoryOnTransition,
+        'General' => {
+            '* -> resolved' => ['CustomRole.customer', 'Owner'],
+        },
+    );
+
+=head2 Role Membership in a Group
+
+Roles can require the members of the role to also be a member of a group
+before satisfying to mandatory condition. Below we require that the Owner
+role be set and that the member it is set to is a member of the group
+'SupportReps' or 'Admins'.
+
+    Set( %MandatoryOnTransition,
+        'General' => {
+            'open -> *' => ['Owner'],
+            'Owner' => { transition => 'open -> *', group => ['SupportReps', 'Admins'] },
+        }
+    );
+
 =head2 Restrictions on Queue Transitions
 
 The default behavior for C<MandatoryOnTransition> operates on status transitions,

commit da4a3c672b6899172edf7514f0a2fedeb953f18f
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Mar 1 23:33:01 2019 +0800

    Rename require_owner_for_resolve.t to roles.t as we are gonna add more role tests there

diff --git a/xt/require_owner_for_resolve.t b/xt/roles.t
similarity index 100%
rename from xt/require_owner_for_resolve.t
rename to xt/roles.t

commit ab3ccd16b921fae3d908dce89eecba41a051fde9
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Fri Jan 11 16:59:07 2019 -0500

    Add tests for mandatory AdminCc and multi-member cusotm roles

diff --git a/xt/roles.t b/xt/roles.t
index 20fd377..cd76970 100644
--- a/xt/roles.t
+++ b/xt/roles.t
@@ -1,11 +1,15 @@
 use strict;
 use warnings;
+use Test::Warn;
 
 use RT::Extension::MandatoryOnTransition::Test tests => undef, config => <<CONFIG
 Set( %MandatoryOnTransition,
      '*' => {
-         '* -> resolved' => ['Owner',],
-     }
+         '* -> resolved'  => ['Owner'],
+         '* -> stalled'   => ['AdminCc'],
+         '* -> deleted'   => ['CustomRole.vip'],
+         'AdminCc'        => { transition => '* -> stalled', group => ['Admins'] },
+        }
     );
 CONFIG
   ;
@@ -118,5 +122,130 @@ diag "Resolve ticket through Jumbo with required Owner";
     $m->text_contains("Status changed from 'new' to 'resolved'");
 }
 
+diag "Test core role fields";
+{
+    my $role = qw/AdminCc/;
+    my $t = RT::Test->create_ticket(
+        Queue   => 'General',
+        Subject => 'Test Mandatory AdminCc',
+        Content => 'Testing',
+    );
+    ok($t->id, 'Created test ticket: ' . $t->id);
+    my ($ret, $msg) = $t->SetStatus('open');
+    ok $ret, $msg;
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Basics' }, 'Get Modify.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'stalled', },
+            button    => 'SubmitTicket',
+        },
+        "Submit stalled with no $role member"
+    );
+    $m->text_contains("A member of group Admins is required for role: $role");
+    $m->warning_like(qr/Failed to load group: Admins : Couldn't find row/);
+
+    my $role_group = $t->$role;
+    ok $role_group->Id;
+
+    my $root = RT::User->new(RT->SystemUser);
+    $root->Load('root');
+    ok($root->id, 'Loaded root');
+
+    ($ret, $msg) = $role_group->AddMember($root->PrincipalId);
+    ok $ret, $msg;
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Basics' }, 'Get Modify.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'stalled', },
+            button    => 'SubmitTicket',
+        },
+        "Try to stall ticket with no Admins group created"
+    );
+    $m->text_contains("A member of group Admins is required for role: $role");
+    $m->warning_like(qr/Failed to load group: Admins : Couldn't find row/);
+
+    my $group = RT::Group->new(RT->SystemUser);
+    ($ret, $msg) = $group->CreateUserDefinedGroup(Name => 'Admins');
+    ok $ret, "Failed to create Admins group: $msg";
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Basics' }, 'Get Modify.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'stalled', },
+            button    => 'SubmitTicket',
+        },
+        "Try to stall ticket with no $role but not a member of required group"
+    );
+    $m->text_contains("A member of group Admins is required for role: $role");
+
+    ($ret, $msg) = $group->AddMember($root->PrincipalId);
+    ok $ret, $msg;
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Basics' }, 'Get Modify.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'stalled', },
+            button    => 'SubmitTicket',
+        },
+        "Try to stall ticket with $role and group required"
+    );
+    $m->text_contains("Status changed from 'open' to 'stalled'");
+}
+
+diag "Test custom role mandatory fields";
+{
+    my $t = RT::Test->create_ticket(
+        Queue   => 'General',
+        Subject => 'Test Mandatory Custom Role',
+        Content => 'Testing',
+    );
+    ok($t->id, 'Created test ticket: ' . $t->id);
+    my ($ret, $msg) = $t->SetStatus('open');
+    ok $ret, $msg;
+
+    my $id = $t->id;
+
+    my $customrole = RT::CustomRole->new(RT->SystemUser);
+    ($ret, $msg) = $customrole->Create(Name => 'vip');
+    ok $ret, $msg;
+
+    ($ret, $msg) = $customrole->Load('vip');
+    ok $ret, $msg;
+
+    ($ret, $msg) = $customrole->AddToObject($t->Queue);
+    ok $ret, $msg;
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Basics' }, 'Get Modify.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'deleted', },
+            button    => 'SubmitTicket',
+        },
+        "Submit deleted with no value for required custom role"
+    );
+    $m->text_contains("vip is required when changing Status to deleted");
+
+    ($ret, $msg) = $t->AddWatcher(Type => $customrole->GroupType, Principal => $root->PrincipalObj);
+    ok $ret, $msg;
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Basics' }, 'Get Modify.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'deleted', },
+            button    => 'SubmitTicket',
+        },
+        "Submit deleted with manatory custom role requirements met."
+    );
+    $m->text_contains("Ticket deleted");
+}
+
 undef $m;
 done_testing;

commit 494255868a6de2f3db5b0977500b614d507a74d5
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Mar 2 02:01:51 2019 +0800

    Handle mandatory multi-members roles on Jumbo page

diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index 22dba40..cff8c42 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -637,6 +637,48 @@ sub CheckMandatoryFields {
                 delete $ARGSRef->{$role_arg};
             }
 
+            # Handle multi-members roles on Jumbo page
+            my @values;
+
+            if ( $role =~ /^(AdminCc|Cc|Requestors)$/i || ( $role_object && !$role_object->SingleValue ) ) {
+                my $type = $role_object ? $role_object->GroupType : $role;
+
+                for my $arg ( keys %$ARGSRef ) {
+                    if ( $arg =~ /^WatcherTypeEmail(\d+)/ ) {
+                        my $num = $1;
+                        next unless $ARGSRef->{$arg} eq $type;
+                        my $address = $ARGSRef->{ 'WatcherAddressEmail' . $num };
+                        next unless $address;
+
+                        push @values, $address;
+                    }
+                    elsif ( $arg =~ /^Ticket-DeleteWatcher-Type-$type-Principal-(\d+)$/ ) {
+                        my $del_id = $1;
+                        @role_values = grep { ref $_ ? $_->id != $del_id : $_ } @role_values;
+                    }
+                }
+            }
+
+            for my $value (@values) {
+                my $user = RT::User->new( RT->SystemUser );
+                $user->Load($value);
+                $user->LoadByEmail($value) unless $user->id;
+
+                if ( $user->id ) {
+                    push @role_values, $user unless $user->id == $RT::Nobody->id;
+                }
+                else {
+                    # RT can automatically create users with email addresses.
+                    if ( $value =~ /@/ ) {
+                        push @role_values, $value;
+                    }
+                    else {
+                        push @errors, $CurrentUser->loc( "Could not load user: [_1]", $value );
+                    }
+                }
+            }
+
+
             # Check for mandatory group configuration, supports multiple groups where only
             # one true case needs to be found.
             if ( $role_group_values->{$role}->{group} ) {

commit c56963525fb7a0bdc6f08f645c9232e298ac12ca
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Mar 2 02:51:35 2019 +0800

    Show multi-members roles if required on Basics page

diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Elements/EditBasics/MassageFields b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Elements/EditBasics/MassageFields
new file mode 100644
index 0000000..01bb55e
--- /dev/null
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Elements/EditBasics/MassageFields
@@ -0,0 +1,66 @@
+<%init>
+return if $ExcludeCustomRoles || !$defaults{Status};
+
+my ($core, $cfs, $roles) = RT::Extension::MandatoryOnTransition->RequiredFields(
+    Ticket  => $TicketObj,
+    To      => $defaults{Status},
+);
+return unless @$roles;
+
+my @roles;
+foreach my $role ( @{$roles} ) {
+    next if $role eq 'Owner';
+
+    if ( $role =~ s/^CustomRole\.//i ) {
+        my $role_object = RT::CustomRole->new( $session{CurrentUser} );
+        my ( $ret, $msg ) = $role_object->Load($role);
+        unless ($ret) {
+            RT::Logger->error("Unable to load custom role $role: $msg");
+            next;
+        }
+
+        # Modify page already contains single member role groups
+        next if $role_object->SingleValue;
+
+        push @roles, $role_object;
+    }
+    else {
+        my $role_group = RT::Group->new( $session{CurrentUser} );
+        # Handle core roles like Requestor, Cc, AdminCc
+        my ( $ret, $msg ) = $role_group->LoadRoleGroup( Object => $TicketObj, Name => $role );
+        if ($ret) {
+            push @roles, $role_group;
+        }
+        else {
+            RT::Logger->error("Unable to load role $role: $msg");
+        }
+    }
+
+}
+
+for my $role (@roles) {
+    my $input = 'Add' . ( $role->isa('RT::Group') ? $role->Name : $role->GroupType );
+
+    push @$Fields, {
+        name => loc( 'Add [_1]', $role->Name ),
+        comp => '/Elements/EmailInput',
+        args => {
+            Name               => $input,
+            Autocomplete       => 1,
+            AutocompleteNobody => 0,
+            AutocompleteReturn => "Email",
+            Size               => 20,
+            Default            => $defaults{$input},
+
+        }
+    };
+}
+
+</%init>
+
+<%args>
+$TicketObj
+$Fields
+$ExcludeCustomRoles => 0
+%defaults => ()
+</%args>

commit fca3b705968a5b4e53ecf374ea2aa53516127583
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Mar 2 03:37:18 2019 +0800

    Support mandatory roles on ticket create

diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index cff8c42..1f47ae0 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -387,6 +387,16 @@ sub RequiredFields {
 
     my %role_group_values;
     my $cr = RT::CustomRole->new(RT->SystemUser);
+    my $queue_id;
+    if ( $args{Ticket} ) {
+        $queue_id = $args{Ticket}->Queue;
+    }
+    else {
+        my $queue = RT::Queue->new( RT->SystemUser );
+        $queue->Load($args{Queue});
+        $queue_id = $queue->id;
+    }
+
     foreach my $role (@roles){
         if ( $role =~ /^CustomRole\.(.*)/i ) {
             my $role_name = $1;
@@ -395,7 +405,7 @@ sub RequiredFields {
                 RT::Logger->error("Could not load Custom role $role_name: $msg");
                 @roles = grep { $_ ne $role } @roles;
                 next;
-            } elsif ( not $cr->IsAdded($args{Ticket}->QueueObj->Id) or $cr->Disabled ) {
+            } elsif ( not $cr->IsAdded($queue_id) or $cr->Disabled ) {
                 RT::Logger->error("Custom role $role_name is not applied to: " . $args{Ticket}->QueueObj->Name );
                 @roles = grep { $_ ne $role } @roles;
                 next;
@@ -539,7 +549,7 @@ sub CheckMandatoryFields {
 
             if ( $role =~ /^CustomRole\.(.+)/ ) {
                 $role_name = $1;
-                $role_object = RT::CustomRole->new( $args{Ticket}->CurrentUser );
+                $role_object = RT::CustomRole->new( $CurrentUser );
 
                 my ( $ret, $msg ) = $role_object->Load($role_name);
                 push @errors, $CurrentUser->loc("Could not load object for [_1]", $role_name) unless $ret;
@@ -552,7 +562,7 @@ sub CheckMandatoryFields {
 
                 # No need to load current value for single-member custom roles
                 # as new passed value will override current one
-                if ( !$role_object->SingleValue ) {
+                if ( !$role_object->SingleValue && $args{Ticket} ) {
 
                     $role_arg = "Add$role_arg";
                     $role_values = $args{Ticket}->RoleGroup( $role_object->GroupType );
@@ -586,16 +596,19 @@ sub CheckMandatoryFields {
                     $role_arg = "Add$role";
                 }
 
-                $role_values = RT::Group->new( $args{Ticket}->CurrentUser );
-                my ( $ret, $msg ) = $role_values->LoadRoleGroup(
-                    Object => $args{Ticket},
-                    Name   => $role,
-                );
-                if ( $ret ) {
-                    push @role_values, grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
-                }
-                else {
-                    push @errors, $CurrentUser->loc("Failed to load role $role for ticket");
+                if ( $args{Ticket} ) {
+                    $role_values = RT::Group->new( $args{Ticket}->CurrentUser );
+                    my ( $ret, $msg ) = $role_values->LoadRoleGroup(
+                        Object => $args{Ticket},
+                        Name   => $role,
+                    );
+                    if ($ret) {
+                        push @role_values,
+                          grep { $_->id != $RT::Nobody->id } @{ $role_values->UserMembersObj->ItemsArrayRef };
+                    }
+                    else {
+                        push @errors, $CurrentUser->loc("Failed to load role $role for ticket");
+                    }
                 }
             }
 
@@ -678,6 +691,31 @@ sub CheckMandatoryFields {
                 }
             }
 
+            # Handle multi-members roles on Create page
+            if ( $role =~ /^(AdminCc|Cc|Requestors)$/i || ( $role_object && !$role_object->SingleValue ) ) {
+                my $type = $role_object ? $role_object->GroupType : $role;
+
+                if ( $ARGSRef->{$role} ) {
+                    for my $value ( RT::EmailParser->ParseEmailAddress( $ARGSRef->{$role} ) ) {
+                        my $user = RT::User->new( RT->SystemUser );
+                        $user->LoadByEmail($value);
+
+                        if ( $user->id ) {
+                            push @role_values, $user unless $user->id == $RT::Nobody->id;
+                        }
+                        else {
+                            # RT can automatically create users with email addresses.
+                            if ( $value =~ /@/ ) {
+                                push @role_values, $value;
+                            }
+                            else {
+                                push @errors, $CurrentUser->loc( "Could not load user: [_1]", $value );
+                            }
+                        }
+                    }
+                }
+            }
+
 
             # Check for mandatory group configuration, supports multiple groups where only
             # one true case needs to be found.

commit 0d4a1d2f1c58ddfea65f1528ec976d5c9b38afed
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Mar 2 04:14:00 2019 +0800

    Update required_fields.t for new added roles

diff --git a/xt/required_fields.t b/xt/required_fields.t
index 4f2135c..f2a826c 100644
--- a/xt/required_fields.t
+++ b/xt/required_fields.t
@@ -15,7 +15,7 @@ diag "Test RequiredFields without a ticket";
     is( $core->[0], 'TimeWorked', 'Got TimeWorked for required core');
 
     my $must_values;
-    ($core, $cf, $must_values) = RT::Extension::MandatoryOnTransition->RequiredFields(
+    ($core, $cf, my $roles, $must_values, my $role_groups ) = RT::Extension::MandatoryOnTransition->RequiredFields(
                            From => "''",
                            To   => 'resolved',
                            Queue => 'General',
@@ -23,12 +23,15 @@ diag "Test RequiredFields without a ticket";
 
     is( $core->[0], 'TimeWorked', 'Got TimeWorked for required core');
     is( $cf->[0], 'Test Field', 'Got Test Field for required custom field');
+    is_deeply( $roles, [], 'Empty roles');
 
     is( (ref $must_values->{'Test Field3'}), 'HASH', 'Got a hash for Test Field3 must values');
     is( (ref $must_values->{'Test Field3'}{'must_be'}), 'ARRAY', 'Got an array for must be values');
     is( (ref $must_values->{'Test Field4'}{'must_not_be'}), 'ARRAY', 'Got an array for must not be values');
     is( $must_values->{'Test Field3'}{'must_be'}->[0], 'normal', "First must be value is 'normal'");
     is( $must_values->{'Test Field4'}{'must_not_be'}->[0], 'down', "First must not be value is 'down'");
+
+    is_deeply( $role_groups, {}, "Empty role group hash");
 }
 
 diag "Test RequiredFields with a ticket";
@@ -41,19 +44,21 @@ diag "Test RequiredFields with a ticket";
 
     ok( $t->id, 'Created test ticket: ' . $t->id);
 
-    my ($core, $cf, $must_values) = RT::Extension::MandatoryOnTransition->RequiredFields(
+    my ($core, $cf, $roles, $must_values, $role_groups) = RT::Extension::MandatoryOnTransition->RequiredFields(
                            Ticket => $t,
                            To   => 'resolved',
                        );
 
     is( $core->[0], 'TimeWorked', 'Got TimeWorked for required core');
     is( $cf->[0], 'Test Field', 'Got Test Field for required custom field');
+    is_deeply( $roles, [], 'Empty roles');
 
     is( (ref $must_values->{'Test Field3'}), 'HASH', 'Got a hash for Test Field3 must values');
     is( (ref $must_values->{'Test Field3'}{'must_be'}), 'ARRAY', 'Got an array for must be values');
     is( (ref $must_values->{'Test Field4'}{'must_not_be'}), 'ARRAY', 'Got an array for must not be values');
     is( $must_values->{'Test Field3'}{'must_be'}->[0], 'normal', "First must be value is 'normal'");
     is( $must_values->{'Test Field4'}{'must_not_be'}->[0], 'down', "First must not be value is 'down'");
+    is_deeply( $role_groups, {}, "Empty role group hash");
 }
 
 done_testing;

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


More information about the Bps-public-commit mailing list