[Bps-public-commit] RT-Extension-MandatoryOnTransition branch, support-coreroles-and-customroles, created. 0.17-5-g999d738

Craig Kaiser craig at bestpractical.com
Wed Jan 23 16:31:47 EST 2019


The branch, support-coreroles-and-customroles has been created
        at  999d738b1eabdd6e112e9415421490864f2e3ab4 (commit)

- Log -----------------------------------------------------------------
commit 27e5c928a33556f6cc80d02adb4a95942708811b
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Fri Jan 4 11:11:15 2019 -0500

    Support customroles for mandatoryontransition conditions
    
    Use the syntax 'CustomRole.MyRole' to set mandatory conditions for
    customrole values.

diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index b00e7d7..0984f53 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,42 @@ 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($msg) unless $ret;
+        push @roles, $role_object if $ret;
+    }
+}
 </%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 ) {
+% my $count = 0;
+%     foreach my $role (@roles) {
+<input type='hidden' name="WatcherTypeEmail<% $count %>" value=<% $role->Name %>></input>
+        <tr>
+            <td class="label"><% $role->Name %>:</td>
+            <td class="entry">
+                <& /Elements/EmailInput,
+                    Name => 'RT::CustomRole-' . $role->Id,
+                    Autocomplete       => 1,
+                    AutocompleteNobody => 1,
+                    AutocompleteReturn => "Name",
+                    Size               => 20,
+                &>
+            </td>
+        </tr>
+%     $count++;
+%     }
+% }
diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/BeforeUpdate b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/BeforeUpdate
index 171893f..91be5c0 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/BeforeUpdate
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/BeforeUpdate
@@ -16,4 +16,11 @@ if (@$errors_ref) {
     $$skip_update = 1;
     push @$results, @$errors_ref;
 }
+
+# Allow mandatory roles to be set on update page
+foreach my $key (keys %{$ARGSRef}) {
+    if ( $key =~ /^WatcherAddressEmail(\d*)$/ && not $ARGSRef->{$key} ) {
+        $ARGSRef->{"WatcherTypeEmail$1"} = undef;
+    }
+}
 </%init>
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index d238c18..cd6b483 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,10 +146,11 @@ Category selection before resolving tickets in every other queue.
 
     Set( %MandatoryOnTransition,
         Helpdesk => {
-            '* -> resolved' => ['TimeWorked', 'CF.Resolution'],
+            '* -> resolved' => ['TimeWorked', 'CF.Resolution', 'CustomRole.Analyst'],
         },
         '*' => {
             '* -> resolved' => ['CF.Category'],
+            'CustomRole.Analyst' => {transition => '* -> open', group => 'Engineering'},
         },
     );
 
@@ -323,7 +324,7 @@ sub RequiredFields {
     my %config = ();
     %config = $self->Config($args{Queue});
 
-    return ([], []) unless %config;
+    return ([], [], []) unless %config;
 
    $to ||= '';
    $from ||= '';
@@ -340,6 +341,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\.|^AdminCc|^Cc|^Requestor|^Owner/i } @$required;
 
     # Pull out any must_be or must_not_be rules
     my %cf_must_values = ();
@@ -359,7 +362,8 @@ sub RequiredFields {
             }
         }
     }
-    return (\@core, \@cfs, \%cf_must_values);
+
+    return (\@core, \@cfs, \@roles, \%cf_must_values);
 }
 
 =head3 CheckMandatoryFields
@@ -412,13 +416,15 @@ sub CheckMandatoryFields {
     }
 
     # Some convenience variables set depending on what gets passed
-    my ($CFs, $CurrentUser);
+    my ($CFs, $CRs, $CurrentUser);
     if ( $args{'Ticket'} ){
         $CFs = $args{'Ticket'}->CustomFields;
+        $CRs = $args{'Ticket'}->QueueObj->CustomRoles;
         $CurrentUser = $args{'Ticket'}->CurrentUser();
     }
     elsif ( $args{'Queue'} ){
         $CFs = $args{'Queue'}->TicketCustomFields;
+        $CRs = $args{'Queue'}->CustomRoles;
         $CurrentUser = $args{'Queue'}->CurrentUser();
     }
     else{
@@ -426,15 +432,14 @@ sub CheckMandatoryFields {
         return \@errors;
     }
 
-    my ($core, $cfs, $must_values) = $self->RequiredFields(
+    my ($core, $cfs, $roles, $cf_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 +505,79 @@ sub CheckMandatoryFields {
             $label, $CurrentUser->loc($transition),  $CurrentUser->loc($field_label{$transition}));
     }
 
+    if (@$roles) {
+        foreach my $role (@$roles) {
+            my ( $role_values, $owner_value );
+            my ( $role_arg, $role_full ) = ( $role, $role );
+
+            if ( $role =~ s/^CustomRole\.//i ) {
+                push @errors,
+                  $CurrentUser->loc(
+    'Custom Roles object required to process mandatory custom roles'
+                  ) unless $CRs;
+
+                my $role_object =
+                  RT::CustomRole->new( $args{Ticket}->CurrentUser );
+
+                my ( $ret, $msg ) = $role_object->Load($role);
+                push @errors,
+                  $CurrentUser->loc("Failed to load customrole $role:  $msg")
+                  unless $ret;
+                next unless $role_object->Id;
+
+                $role_arg = 'RT::CustomRole-' . $role_object->Id;
+
+                ( $ret, $msg ) = $role_values =
+                  $args{Ticket}->RoleGroup( $role_object->GroupType );
+                push @errors, $CurrentUser->loc("Could not load current user")
+                  unless $ret;
+            }
+
+            my $role_object = RT::CustomRole->new($args{Ticket}->CurrentUser);
+
+            my ($ret, $msg) = $role_object->Load($role);
+            push @errors, $CurrentUser->loc("Failed to load customrole $role:  $msg") unless $ret;
+            return \@errors unless $ret;
+
+            $role_arg = 'RT::CustomRole-' . $role_object->Id;
+
+            ($ret, $msg) = $role_values = $args{Ticket}->RoleGroup($role_object->GroupType);
+            push @errors, $CurrentUser->loc("$msg") unless $ret;
+            return \@errors unless $ret;
+
+            my @role_values;
+
+            # Use this to keep track of input fields that use a count increment
+            # example: WatcherAddressEmail1 or WatcherAddressEmail2
+            my @row_input_num = grep $role_arg eq $ARGSRef->{$_},
+              keys %{$ARGSRef};
+            map { push @role_values, $ARGSRef->{$_} if $ARGSRef->{$_} }
+              grep /$role_arg/, keys %{$ARGSRef};
+
+            # Grab our arguments for current role
+            if (@row_input_num) {
+                map {
+                    push @role_values, $ARGSRef->{"WatcherAddressEmail$_"}
+                      if $ARGSRef->{"WatcherAddressEmail$_"}
+                  }
+                  map { $_ =~ /^WatcherTypeEmail(\d*)$/; $1 } @row_input_num;
+            }
+            my @temp_array = $role_values->MemberEmailAddresses;
+            push @role_values, @temp_array if scalar @temp_array;
+
+            if ( $args{'To'} && ( 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 ){
@@ -565,7 +643,7 @@ sub CheckMandatoryFields {
         }
 
         # Check for specific values
-        if ( exists $must_values->{$cf->Name} ){
+        if ( exists $cf_must_values->{$cf->Name} ){
             my $cf_value = $value;
 
             if ( not defined $cf_value and $args{'Ticket'} ){
@@ -574,8 +652,8 @@ sub CheckMandatoryFields {
                 $cf_value = $args{'Ticket'}->FirstCustomFieldValue($cf->Name);
             }
 
-            if ( exists $must_values->{$cf->Name}{'must_be'} ){
-                my @must_be = @{$must_values->{$cf->Name}{'must_be'}};
+            if ( exists $cf_must_values->{$cf->Name}{'must_be'} ){
+                my @must_be = @{$cf_must_values->{$cf->Name}{'must_be'}};
 
                 # OK if it's defined and is one of the specified values
                 next if defined $cf_value and grep { $cf_value eq $_ } @must_be;
@@ -593,8 +671,8 @@ sub CheckMandatoryFields {
                 next;
             }
 
-            if ( exists $must_values->{$cf->Name}{'must_not_be'} ){
-                my @must_not_be = @{$must_values->{$cf->Name}{'must_not_be'}};
+            if ( exists $cf_must_values->{$cf->Name}{'must_not_be'} ){
+                my @must_not_be = @{$cf_must_values->{$cf->Name}{'must_not_be'}};
 
                 # OK if it's defined and _not_ in the list
                 next if defined $cf_value and !grep { $cf_value eq $_ } @must_not_be;

commit 77e91ad46f63bb1a8be29418b6bee665501d3ac6
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Wed Jan 23 15:47:31 2019 -0500

    Support all core role fields as mandatory on transition fields
    
    Support the core RT roles Requestor, AdminCc, Owner and Cc along with
    customroles. Moved the existing Owner code into the roles code block as
    it make more sense to be there.

diff --git a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
index 0984f53..edc69aa 100644
--- a/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
+++ b/html/Callbacks/RT-Extension-MandatoryOnTransition/Ticket/Update.html/AfterWorked
@@ -25,6 +25,12 @@ foreach my $role (@{$roles}) {
 
         RT::Logger->error($msg) unless $ret;
         push @roles, $role_object if $ret;
+    } else {
+        my $role_group = RT::Group->new($session{CurrentUser});
+        my ($ret, $msg) = $role_group->LoadRoleGroup(Object => $Ticket, Name => $role);
+
+        RT::Logger->error($msg) unless $ret;
+        push @roles, $role_group if $ret;
     }
 }
 </%init>
@@ -40,12 +46,13 @@ foreach my $role (@{$roles}) {
 % if ( @$roles ) {
 % my $count = 0;
 %     foreach my $role (@roles) {
+%     next if $role->Name eq 'Owner';
 <input type='hidden' name="WatcherTypeEmail<% $count %>" value=<% $role->Name %>></input>
         <tr>
             <td class="label"><% $role->Name %>:</td>
             <td class="entry">
                 <& /Elements/EmailInput,
-                    Name => 'RT::CustomRole-' . $role->Id,
+                    Name => grep ($role->Name eq $_, qw/Requestor AdminCc Cc/) ? "WatcherAddressEmail$count" : 'RT::CustomRole-' . $role->Id,
                     Autocomplete       => 1,
                     AutocompleteNobody => 1,
                     AutocompleteReturn => "Name",
diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index cd6b483..4648e40 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -258,18 +258,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
@@ -462,36 +460,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?
@@ -532,19 +500,50 @@ sub CheckMandatoryFields {
                 push @errors, $CurrentUser->loc("Could not load current user")
                   unless $ret;
             }
+            elsif ( $role eq 'Owner' ) {
+
+                # 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/ ) {
+                            $owner_value = $owner
+                              unless (
+                                $args{'Ticket'}->OwnerObj->Name eq $owner );
+                        }
+                        elsif ( length $owner ) {
+                            $owner_value = $owner
+                              unless (
+                                $args{'Ticket'}->OwnerObj->id == $owner );
+                        }
+                    }
+                }
+                else {
+                    $owner_value = $ARGSRef->{$role};
+                }
 
-            my $role_object = RT::CustomRole->new($args{Ticket}->CurrentUser);
-
-            my ($ret, $msg) = $role_object->Load($role);
-            push @errors, $CurrentUser->loc("Failed to load customrole $role:  $msg") unless $ret;
-            return \@errors unless $ret;
+                # If no value provided in ARGS then grab the current value of the ticket
+                $owner_value = $args{Ticket}->Owner unless $owner_value;
 
-            $role_arg = 'RT::CustomRole-' . $role_object->Id;
+                my $user = RT::User->new( $args{Ticket}->CurrentUser );
+                $user->Load($owner_value);
+                push @errors,
+                  $CurrentUser->loc("Could not load user: $owner_value")
+                  unless $user->Id;
 
-            ($ret, $msg) = $role_values = $args{Ticket}->RoleGroup($role_object->GroupType);
-            push @errors, $CurrentUser->loc("$msg") unless $ret;
-            return \@errors unless $ret;
+                $role_values = $user->EmailAddress if $user->Id;
+            }
+            else {
+                $role_values = RT::Group->new( $args{Ticket}->CurrentUser );
+                my ( $ret, $msg ) = $role_values->LoadRoleGroup(
+                    Object => $args{Ticket},
+                    Name   => $role
+                );
 
+                push @errors,
+                  $CurrentUser->loc(
+                    "Failed to load role $role for ticket: $msg")
+                  unless $ret;
+            }
             my @role_values;
 
             # Use this to keep track of input fields that use a count increment
@@ -562,10 +561,22 @@ sub CheckMandatoryFields {
                   }
                   map { $_ =~ /^WatcherTypeEmail(\d*)$/; $1 } @row_input_num;
             }
-            my @temp_array = $role_values->MemberEmailAddresses;
-            push @role_values, @temp_array if scalar @temp_array;
 
-            if ( $args{'To'} && ( not scalar @role_values ) ) {
+            # We could end up with a RT::Group option or a single value in the case of Owner
+            if ( ref $role_values eq 'RT::Group' ) {
+                my @temp_array = $role_values->MemberEmailAddresses;
+                push @role_values, @temp_array if scalar @temp_array;
+            }
+            else {
+                push @role_values, $role_values;
+            }
+
+            if (
+                $args{'To'}
+                && ( not scalar @role_values
+                    or ( $owner_value && $owner_value == $RT::Nobody->id ) )
+              )
+            {
                 push @errors,
                   $CurrentUser->loc(
                     "[_1] is required when changing [_2] to [_3]",

commit e516c760f7c3aa5d7f38ff358996acb4cf112352
Author: Craig Kaiser <craig at bestpractical.com>
Date:   Wed Jan 23 15:48:24 2019 -0500

    Support requiring a role to be a member of a group
    
    Provide a group key for the role specific key in the
    mandatory-on-transition config hash to require at least one
    member of the role be a member of at least one of the groups
    in the groups array.

diff --git a/lib/RT/Extension/MandatoryOnTransition.pm b/lib/RT/Extension/MandatoryOnTransition.pm
index 4648e40..11ef246 100644
--- a/lib/RT/Extension/MandatoryOnTransition.pm
+++ b/lib/RT/Extension/MandatoryOnTransition.pm
@@ -361,7 +361,24 @@ sub RequiredFields {
         }
     }
 
-    return (\@core, \@cfs, \@roles, \%cf_must_values);
+    my %role_group_values;
+    foreach my $role (@roles){
+        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
@@ -430,7 +447,7 @@ sub CheckMandatoryFields {
         return \@errors;
     }
 
-    my ($core, $cfs, $roles, $cf_must_values) = $self->RequiredFields(
+    my ($core, $cfs, $roles, $cf_must_values, $role_group_values) = $self->RequiredFields(
         Ticket  => $args{'Ticket'},
         Queue   => $args{'Queue'} ? $args{'Queue'}->Name : undef,
         From    => $args{'From'},
@@ -571,6 +588,47 @@ sub CheckMandatoryFields {
                 push @role_values, $role_values;
             }
 
+            # Check for mandatory group configuration, supports multiple groups where only
+            # one true case needs to be found.
+            if ( $role_group_values->{$role_full}->{group} ) {
+                my $has_valid_member;
+
+                foreach my $group_name (
+                    @{ $role_group_values->{$role_full}->{group} } )
+                {
+                    next if $has_valid_member;
+                    my $group = RT::Group->new( $args{Ticket}->CurrentUser );
+
+                    my ( $ret, $msg ) =
+                      $group->LoadUserDefinedGroup($group_name);
+                    push @errors,
+                      $CurrentUser->loc(
+                        "Failed to load group: '$group_name' $msg")
+                      unless $ret;
+                    next unless $ret;
+
+                    my $user = RT::User->new( $args{Ticket}->CurrentUser );
+                    foreach my $member (@role_values) {
+                        next unless $member;
+
+                        $user->Load($member);
+                        $user->LoadByEmail($member) unless $user->Id;
+                        RT::Logger->error("Failed to load: $member")
+                          unless $user->Id;
+                        next unless $user->Id;
+
+                        $has_valid_member =
+                          $group->HasMemberRecursively( $user->Id );
+                    }
+                }
+                my $roles = join( ' or ',
+                    @{ $role_group_values->{$role_full}->{group} } );
+                push @errors,
+                  $CurrentUser->loc(
+                    "A member of group $roles is required for role: $role")
+                  unless $has_valid_member;
+                next unless $has_valid_member;
+            }
             if (
                 $args{'To'}
                 && ( not scalar @role_values

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

    Create test file for roles

diff --git a/xt/require_owner_for_resolve.t b/xt/require_owner_for_resolve.t
deleted file mode 100644
index 20fd377..0000000
--- a/xt/require_owner_for_resolve.t
+++ /dev/null
@@ -1,122 +0,0 @@
-use strict;
-use warnings;
-
-use RT::Extension::MandatoryOnTransition::Test tests => undef, config => <<CONFIG
-Set( %MandatoryOnTransition,
-     '*' => {
-         '* -> resolved' => ['Owner',],
-     }
-    );
-CONFIG
-  ;
-
-use_ok('RT::Extension::MandatoryOnTransition');
-
-my ($baseurl, $m) = RT::Test->started_ok();
-
-ok($m->login('root', 'password'), 'logged in');
-$m->get_ok($m->rt_base_url);
-
-my $root = RT::User->new(RT->SystemUser);
-$root->Load('root');
-ok($root->id, 'Loaded root');
-
-diag "Resolve ticket through Update with required Owner";
-{
-    my $t = RT::Test->create_ticket(
-        Queue   => 'General',
-        Subject => 'Test Mandatory Owner On Resolve',
-        Content => 'Testing',
-    );
-
-    ok($t->id, 'Created test ticket: ' . $t->id);
-
-    $m->goto_ticket($t->id);
-
-    $m->follow_link_ok({ text => 'Resolve' }, 'Try to resolve ticket');
-    $m->text_contains('Test Mandatory Owner On Resolve');
-    $m->submit_form_ok(
-        {   form_name => 'TicketUpdate',
-            button    => 'SubmitTicket',
-        },
-        'Submit resolve with no Owner set'
-    );
-    $m->text_contains('Owner is required when changing Status to resolved');
-    $m->submit_form_ok(
-        {   form_name => 'TicketUpdate',
-            button    => 'SubmitTicket',
-            fields    => { Owner => $root->id }
-        },
-        'Submit resolve with Owner set'
-    );
-    $m->text_lacks('Owner is required when changing Status to resolved');
-    $m->text_contains('Owner changed from Nobody to root');
-    $m->text_contains("Status changed from 'new' to 'resolved'");
-}
-
-diag "Resolve ticket through Basics with required Owner";
-{
-    my $t = RT::Test->create_ticket(
-        Queue   => 'General',
-        Subject => 'Test Mandatory Owner On Resolve',
-        Content => 'Testing',
-    );
-    ok($t->id, 'Created ticket to resolve through Modify.html: ' . $t->id);
-
-    $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 => 'resolved', },
-            button    => 'SubmitTicket',
-        },
-        'Submit resolve with no Owner set'
-    );
-    $m->text_contains('Owner is required when changing Status to resolved');
-
-    $m->submit_form_ok(
-        {   form_name => 'TicketModify',
-            fields    => { Status => 'resolved', Owner => $root->id, },
-            button    => 'SubmitTicket',
-        },
-        'Submit resolve with no Owner set'
-    );
-    $m->text_lacks('Owner is required when changing Status to resolved');
-    $m->text_contains('Owner changed from Nobody to root');
-    $m->text_contains("Status changed from 'new' to 'resolved'");
-}
-
-diag "Resolve ticket through Jumbo with required Owner";
-{
-    my $t = RT::Test->create_ticket(
-        Queue   => 'General',
-        Subject => 'Test Mandatory Owner On Resolve',
-        Content => 'Testing',
-    );
-    ok($t->id, 'Created ticket to resolve through ModifyAll.html: ' . $t->id);
-
-    $m->goto_ticket($t->id);
-    $m->follow_link_ok({ text => 'Jumbo' }, 'Get ModifyAll.html of ticket');
-    $m->submit_form_ok(
-        {   form_name => 'TicketModifyAll',
-            fields    => { Status => 'resolved', },
-            button    => 'SubmitTicket',
-        },
-        'Submit resolve with no Owner set'
-    );
-    $m->text_contains('Owner is required when changing Status to resolved');
-
-    $m->submit_form_ok(
-        {   form_name => 'TicketModifyAll',
-            fields    => { Status => 'resolved', Owner => $root->id, },
-            button    => 'SubmitTicket',
-        },
-        'Submit resolve with no Owner set'
-    );
-    $m->text_lacks('Owner is required when changing Status to resolved');
-    $m->text_contains('Owner changed from Nobody to root');
-    $m->text_contains("Status changed from 'new' to 'resolved'");
-}
-
-undef $m;
-done_testing;
diff --git a/xt/required_fields.t b/xt/required_fields.t
index 4f2135c..cd1fda7 100644
--- a/xt/required_fields.t
+++ b/xt/required_fields.t
@@ -14,8 +14,8 @@ 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(
+    my ($must_values, $role, $role_group_values);
+    ($core, $cf, $role, $must_values, $role_group_values) = RT::Extension::MandatoryOnTransition->RequiredFields(
                            From => "''",
                            To   => 'resolved',
                            Queue => 'General',
@@ -41,7 +41,7 @@ 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_group_values) = RT::Extension::MandatoryOnTransition->RequiredFields(
                            Ticket => $t,
                            To   => 'resolved',
                        );
diff --git a/xt/roles.t b/xt/roles.t
new file mode 100644
index 0000000..1519a5c
--- /dev/null
+++ b/xt/roles.t
@@ -0,0 +1,260 @@
+use strict;
+use warnings;
+
+use RT::Extension::MandatoryOnTransition::Test tests => undef, config => <<CONFIG
+Set( %MandatoryOnTransition,
+     '*' => {
+         '* -> resolved'  => ['Owner'],
+         '* -> stalled'   => ['AdminCc'],
+         '* -> deleted'   => ['CustomRole.vip'],
+         'AdminCc'        => { transition => '* -> stalled', group => ['Admins'] },
+         'CustomRole.vip' => { transition => '* -> deleted' },
+        }
+    );
+CONFIG
+  ;
+
+use_ok('RT::Extension::MandatoryOnTransition');
+
+my ($baseurl, $m) = RT::Test->started_ok();
+
+ok($m->login('root', 'password'), 'logged in');
+$m->get_ok($m->rt_base_url);
+
+my $root = RT::User->new(RT->SystemUser);
+$root->Load('root');
+ok($root->id, 'Loaded root');
+
+diag "Resolve ticket through Update with required Owner";
+{
+    my $t = RT::Test->create_ticket(
+        Queue   => 'General',
+        Subject => 'Test Mandatory Owner On Resolve',
+        Content => 'Testing',
+    );
+
+    ok($t->id, 'Created test ticket: ' . $t->id);
+
+    $m->goto_ticket($t->id);
+
+    $m->follow_link_ok({ text => 'Resolve' }, 'Try to resolve ticket');
+    $m->text_contains('Test Mandatory Owner On Resolve');
+    $m->submit_form_ok(
+        {   form_name => 'TicketUpdate',
+            button    => 'SubmitTicket',
+        },
+        'Submit resolve with no Owner set'
+    );
+    $m->text_contains('Owner is required when changing Status to resolved');
+    $m->submit_form_ok(
+        {   form_name => 'TicketUpdate',
+            button    => 'SubmitTicket',
+            fields    => { Owner => $root->id }
+        },
+        'Submit resolve with Owner set'
+    );
+    $m->text_lacks('Owner is required when changing Status to resolved');
+    $m->text_contains('Owner changed from Nobody to root');
+    $m->text_contains("Status changed from 'new' to 'resolved'");
+}
+
+diag "Resolve ticket through Basics with required Owner";
+{
+    my $t = RT::Test->create_ticket(
+        Queue   => 'General',
+        Subject => 'Test Mandatory Owner On Resolve',
+        Content => 'Testing',
+    );
+    ok($t->id, 'Created ticket to resolve through Modify.html: ' . $t->id);
+
+    $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 => 'resolved', },
+            button    => 'SubmitTicket',
+        },
+        'Submit resolve with no Owner set'
+    );
+    $m->text_contains('Owner is required when changing Status to resolved');
+
+    $m->submit_form_ok(
+        {   form_name => 'TicketModify',
+            fields    => { Status => 'resolved', Owner => $root->id, },
+            button    => 'SubmitTicket',
+        },
+        'Submit resolve with no Owner set'
+    );
+    $m->text_lacks('Owner is required when changing Status to resolved');
+    $m->text_contains('Owner changed from Nobody to root');
+    $m->text_contains("Status changed from 'new' to 'resolved'");
+}
+
+diag "Resolve ticket through Jumbo with required Owner";
+{
+    my $t = RT::Test->create_ticket(
+        Queue   => 'General',
+        Subject => 'Test Mandatory Owner On Resolve',
+        Content => 'Testing',
+    );
+    ok($t->id, 'Created ticket to resolve through ModifyAll.html: ' . $t->id);
+
+    $m->goto_ticket($t->id);
+    $m->follow_link_ok({ text => 'Jumbo' }, 'Get ModifyAll.html of ticket');
+    $m->submit_form_ok(
+        {   form_name => 'TicketModifyAll',
+            fields    => { Status => 'resolved', },
+            button    => 'SubmitTicket',
+        },
+        'Submit resolve with no Owner set'
+    );
+    $m->text_contains('Owner is required when changing Status to resolved');
+
+    $m->submit_form_ok(
+        {   form_name => 'TicketModifyAll',
+            fields    => { Status => 'resolved', Owner => $root->id, },
+            button    => 'SubmitTicket',
+        },
+        'Submit resolve with no Owner set'
+    );
+    $m->text_lacks('Owner is required when changing Status to resolved');
+    $m->text_contains('Owner changed from Nobody to root');
+    $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 Owner On Resolve',
+        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");
+
+    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");
+
+    my $group = RT::Group->new(RT->SystemUser);
+    ($ret, $msg) = $group->CreateUserDefinedGroup(Name => 'Admins');
+    ok $ret, "Failed to create Amdins 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 customrole mandatory fields";
+{
+    my $t = RT::Test->create_ticket(
+        Queue   => 'General',
+        Subject => 'Test Mandatory Owner On Resolve',
+        Content => 'Testing',
+    );
+    ok($t->id, 'Created test ticket: ' . $t->id);
+    my ($ret, $msg) = $t->SetStatus('open');
+    ok $ret, $msg;
+
+    my $id = $t->id;
+
+    $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 customrole 'vip' created"
+    );
+    $m->text_contains("Failed to load customrole vip: Couldn't find row");
+
+    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 customrole"
+    );
+    $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 customrole requirements met."
+    );
+    $m->text_contains("Ticket deleted");
+}
+
+undef $m;
+done_testing;

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

    Update README and Changelog

diff --git a/Changes b/Changes
index 5eb32fc..3f54ca7 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,8 @@
+0.17 2019-01-23
+ - Add support for custom roles as a mandatory field
+ - Add support for all RT core roles as a mandatory field
+ - Add support for requiring group membership for roles
+
 0.17 2018-10-26
  - Add support for Owner as a mandatory field
 
diff --git a/README b/README
index ad7e034..9fa5c66 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.
@@ -96,7 +92,7 @@ CONFIGURATION
 
         Set( %MandatoryOnTransition,
             'QueueName' => {
-                'from -> to' => [ 'BasicField', 'CF.MyField', ],
+                'from -> to' => [ 'BasicField', 'CF.MyField', 'CustomRole.MyRole' ],
             },
         );
 
@@ -125,6 +121,26 @@ 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 customrole "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' => { '*' =>
+    ['CustomRole.customer', 'Owner'] 'CustomRole.customer' => { transition
+    => 'open -> *' }, 'Owner' => { transition => 'open -> *' }, }, );
+
+  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 11ef246..da3af4c 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
@@ -146,17 +141,44 @@ Category selection before resolving tickets in every other queue.
 
     Set( %MandatoryOnTransition,
         Helpdesk => {
-            '* -> resolved' => ['TimeWorked', 'CF.Resolution', 'CustomRole.Analyst'],
+            '* -> resolved' => ['TimeWorked', 'CF.Resolution'],
         },
         '*' => {
             '* -> resolved' => ['CF.Category'],
-            'CustomRole.Analyst' => {transition => '* -> open', group => 'Engineering'},
         },
     );
 
 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 customrole "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' => {
+        '*' => ['CustomRole.customer', 'Owner']
+        'CustomRole.customer' => { transition => 'open -> *' },
+        'Owner' => { transition => 'open -> *' },
+    },
+);
+
+=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,

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


More information about the Bps-public-commit mailing list