Commit bd147bde authored by Thomas Sibley's avatar Thomas Sibley
Browse files

Fully functional implementation for core fields and CFs on Update.html

parent 97048d87
$Named => []
return unless @$Named;
$CustomFields->Limit( FIELD => 'Name', VALUE => $_, SUBCLAUSE => 'names', ENTRYAGGREGRATOR => 'OR' )
for @$Named;
my ($core, $cfs) = RT::Extension::MandatoryOnTransition->RequiredFields(
Ticket => $Ticket,
To => $ARGS{'Status'} || $ARGS{'DefaultStatus'},
return unless @$cfs;
%# 'Named' is handled by this extension in the MassageCustomFields callback
<& /Ticket/Elements/EditCustomFields,
TicketObj => $Ticket,
InTable => 1,
Named => $cfs,
my ($core, $cfs) = RT::Extension::MandatoryOnTransition->RequiredFields(
Ticket => $TicketObj,
To => $ARGSRef->{'Status'},
return unless @$core or @$cfs;
my @errors;
my @core_allowed = qw(TimeWorked TimeTaken Content);
my @core_ticket = qw(TimeWorked);
my %core_for_update = (
TimeWorked => 'UpdateTimeWorked',
TimeTaken => 'UpdateTimeWorked',
Content => 'UpdateContent',
# Check core fields, after canonicalization for update
for my $field (@$core) {
unless (grep { $_ eq $field } @core_allowed) {
RT->Logger->warning("Skipping unsupported core field '$field' during mandatory checks");
# Will we have a value on update?
my $arg = $core_for_update{$field} || $field;
next if defined $ARGSRef->{$arg} and length $ARGSRef->{$arg};
# Do we have a value currently?
next if grep { $_ eq $field } @core_ticket and $TicketObj->$field();
(my $label = $field) =~ s/(?<=[a-z])(?=[A-Z])/ /g; # /
push @errors, loc("[_1] is required when changing Status to [_2]", $label, $ARGSRef->{Status});
# Find the CFs we want
my $CFs = $TicketObj->CustomFields;
$CFs->Limit( FIELD => 'Name', VALUE => $_, SUBCLAUSE => 'names', ENTRYAGGREGRATOR => 'OR' )
for @$cfs;
# Validate them
my $ValidCFs = $m->comp(
CustomFields => $CFs,
NamePrefix => "Object-RT::Ticket-".$TicketObj->Id."-CustomField-",
# Check validation results and mandatory-ness
while (my $cf = $CFs->Next) {
# Is there a validation error?
if ( not $ValidCFs and my $msg = $m->notes('InvalidField-' . $cf->Id)) {
push @errors, loc($cf->Name) . ': ' . $msg;
# Do we have a submitted value for update?
my $arg = "Object-RT::Ticket-".$TicketObj->Id."-CustomField-".$cf->Id."-Value";
my $value = ($ARGSRef->{"${arg}s-Magic"} and exists $ARGSRef->{"${arg}s"})
? $ARGSRef->{$arg . "s"}
: $ARGSRef->{$arg};
next if defined $value and length $value;
# Is there a current value? (Particularly important for Date/Datetime CFs
# since they don't submit a value on update.)
next if $cf->ValuesForObject($TicketObj)->Count;
push @errors, loc("[_1] is required when changing Status to [_2]", $cf->Name, $ARGSRef->{Status});
if (@errors) {
RT->Logger->debug("Preventing update because of missing mandatory fields");
$$skip_update = 1;
push @$results, @errors;
......@@ -6,18 +6,68 @@ our $VERSION = '0.01';
=head1 NAME
RT-Extension-MandatoryOnTransition - Require core and custom fields on status transitions
RT-Extension-MandatoryOnTransition - Require core fields and ticket custom fields on status transitions
This RT extension enforces that certain fields have values before tickets are
explicitly moved to or from specified statuses. If you list custom fields
which must have a value before a ticket is resolved, those custom fields will
automatically show up on the "Resolve" page. The reply/comment won't be
allowed until a value is provided.
See the configuration example under L</INSTALLATION>.
=head2 Supported fields
This extension only enforces mandatory-ness on defined status transitions.
=head3 Basics
Currently the following are supported:
=over 4
=item Content
Requires an update message (reply/comment text) before the transition.
=item TimeWorked
Requires the ticket has a non-zero amount of Time Worked recorded already B<or>
that time worked will be recorded with the current reply/comment in the Worked
field on the update page.
=item TimeTaken
Requires that the Worked field on the update page is non-zero.
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.
=head3 Custom fields
Ticket custom fields of all types are supported.
=head1 CAVEATS
=head2 Custom field validation (I<Input must match [Mandatory]>)
The custom fields enforced by this extension are validated by the standard RT
rules. If you've set Validation patterns for your custom fields, those will be
checked before mandatory-ness is checked. B<< Setting a CFs Validation to
C<(?#Mandatory).> will not magically make it enforced by this extension. >>
=head2 Actions menu
This extension does B<not> affect "quick actions" (those without an update
type) configured in your lifecycle (and appearing in the ticket Actions menu).
If you're requiring fields on resolve, for example, and don't want folks to
have a "Quick Resolve" button that skips the required fields, adjust your
lifecycle config to provide an update type (i.e make it a non-quick action).
Quick actions may be supported in a future release.
......@@ -32,24 +82,42 @@ Quick actions may be supported in a future release.
May need root permissions
=item Edit your /opt/rt4/etc/
=item Enable and configure this extension
Add this line:
Add this line to </opt/rt4/etc/>:
Set(@Plugins, qw(RT::Extension::MandatoryOnTransition));
or add C<RT::Extension::MandatoryOnTransition> to your existing C<@Plugins> line.
Then configure which fields should be mandatory on certain status changes
(either globally or in a specific queue). An example making two custom fields
mandatory before resolving a ticket in the Helpdesk queue:
(either globally or in a specific queue) using the C<%MandatoryOnTransition>
config option. This option takes the generic form of:
Set( %MandatoryOnTransition,
'QueueName' => {
'from -> to' => [ 'BasicField', 'CF.MyField', ],
The fallback for queues without specific rules is specified with C<'*'> where
the queue name would normally be.
Below is an example which requires 1) time worked and filling in a custom field
named Resolution before resolving tickets in the Helpdesk queue and 2) a
Category selection before resolving tickets in every other queue.
Set( %MandatoryOnTransition,
Helpdesk => {
'* -> resolved' => ['CF.{Resolution}', 'CF.{Problem area}'],
'* -> resolved' => ['TimeWorked', 'CF.Resolution'],
'*' => {
'* -> resolved' => 'CF.Category',
The transition syntax is similar to that found in RT's Lifecycles.
The transition syntax is similar to that found in RT's Lifecycles. See
C<perldoc /opt/rt4/etc/>.
=item Clear your mason cache
......@@ -59,6 +127,106 @@ The transition syntax is similar to that found in RT's Lifecycles.
$RT::Config::META{'MandatoryOnTransition'} = {
Type => 'HASH',
PostLoadCheck => sub {
# Normalize field list to always be arrayref
my $self = shift;
my %config = $self->Get('MandatoryOnTransition');
for my $transitions (values %config) {
for (keys %$transitions) {
next if ref $transitions->{$_} eq 'ARRAY';
if (ref $transitions->{$_}) {
RT->Logger->error("%MandatoryOnTransition definition '$_' must be a single field name or an array ref of field names. Ignoring.");
delete $transitions->{$_};
$transitions->{$_} = [ $transitions->{$_} ];
$self->Set(MandatoryOnTransition => %config);
=head2 RequiredFields
Returns two array refs of required fields for the described status transition.
The first is core fields, the second is CF names. Returns nothing (C<return;>)
on error or if nothing is required.
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
specify Ticket, only To is otherwise necessary. If you omit Ticket, From, To,
and Queue are all necessary.
The first transition found in the order below is used:
from -> to
* -> to
from -> *
sub RequiredFields {
my $self = shift;
my %args = (
Ticket => undef,
Queue => undef,
From => undef,
To => undef,
if ($args{Ticket}) {
$args{Queue} ||= $args{Ticket}->QueueObj->Name;
$args{From} ||= $args{Ticket}->Status;
my ($from, $to) = @args{qw(From To)};
return unless $from and $to;
my %config = $self->Config($args{Queue});
return unless %config;
# No transition.
return if $from eq $to;
my $required = $config{"$from -> $to"}
|| $config{"* -> $to"}
|| $config{"$from -> *"}
|| [];
my @core = grep { !/^CF\./i } @$required;
my @cfs = map { /^CF\.(.+)$/i; $1; }
grep { /^CF\./i } @$required;
return (\@core, \@cfs);
=head2 Config
Takes a queue name. Returns a hashref for the given queue (possibly using the
fallback rules) which contains keys of transitions and values of arrayrefs of
You shouldn't need to use this directly.
sub Config {
my $self = shift;
my $queue = shift || '*';
my %config = RT->Config->Get('MandatoryOnTransition');
return %{$config{$queue}} if $config{$queue};
return %{$config{'*'}} if $config{'*'};
=head1 AUTHOR
Thomas Sibley <>
......@@ -69,7 +237,6 @@ All bugs should be reported via
or L<>.
This software is Copyright (c) 2012 by Best Practical Solutions
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment