diff options
author | Ivan Kohler <ivan@freeside.biz> | 2015-07-09 22:18:55 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2015-07-09 22:18:55 -0700 |
commit | 1c538bfabc2cd31f27067505f0c3d1a46cba6ef0 (patch) | |
tree | 96922ad4459eda1e649327fd391d60c58d454c53 /rt/lib/RT/Record/Role/Status.pm | |
parent | 4f5619288413a185e9933088d9dd8c5afbc55dfa (diff) |
RT 4.2.11, ticket#13852
Diffstat (limited to 'rt/lib/RT/Record/Role/Status.pm')
-rw-r--r-- | rt/lib/RT/Record/Role/Status.pm | 314 |
1 files changed, 314 insertions, 0 deletions
diff --git a/rt/lib/RT/Record/Role/Status.pm b/rt/lib/RT/Record/Role/Status.pm new file mode 100644 index 000000000..98f699cd0 --- /dev/null +++ b/rt/lib/RT/Record/Role/Status.pm @@ -0,0 +1,314 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Record::Role::Status; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Status - Common methods for records which have a Status column + +=head1 DESCRIPTION + +Lifecycles are generally set on container records, and Statuses on records +which belong to one of those containers. L<RT::Record::Role::Lifecycle> +handles the containers with the I<Lifecycle> column. This role is for the +records with a I<Status> column within those containers. It includes +convenience methods for grabbing an L<RT::Lifecycle> object as well setters for +validating I<Status> and the column which points to the container object. + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 LifecycleColumn + +Used as a role parameter. Must return a string of the column name which points +to the container object that consumes L<RT::Record::Role::Lifecycle> (or +conforms to it). The resulting string is used to construct two method names: +as-is to fetch the column value and suffixed with "Obj" to fetch the object. + +=head2 Status + +A Status method which returns a lifecycle name is required. Currently +unenforced at compile-time due to poor interactions with +L<DBIx::SearchBuilder::Record/AUTOLOAD>. You'll hit run-time errors if this +method isn't available in consuming classes, however. + +=cut + +with 'RT::Record::Role'; +requires 'LifecycleColumn'; + +=head1 PROVIDES + +=head2 Status + +Returns the Status for this record, in the canonical casing. + +=cut + +sub Status { + my $self = shift; + my $value = $self->_Value( 'Status' ); + my $lifecycle = $self->LifecycleObj; + return $value unless $lifecycle; + return $lifecycle->CanonicalCase( $value ); +} + +=head2 LifecycleObj + +Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>. If called +as a class method, returns an L<RT::Lifecycle> object which is an aggregation +of all lifecycles of the appropriate type. + +=cut + +sub LifecycleObj { + my $self = shift; + my $obj = $self->LifecycleColumn . "Obj"; + return $self->$obj->LifecycleObj; +} + +=head2 Lifecycle + +Returns the L<RT::Lifecycle/Name> of this record's L</LifecycleObj>. + +=cut + +sub Lifecycle { + my $self = shift; + return $self->LifecycleObj->Name; +} + +=head2 ValidateStatus + +Takes a status. Returns true if that status is a valid status for this record, +otherwise returns false. + +=cut + +sub ValidateStatus { + my $self = shift; + return $self->LifecycleObj->IsValid(@_); +} + +=head2 ValidateStatusChange + +Validates the new status with the current lifecycle. Returns a tuple of (OK, +message). + +Expected to be called from this role's L</SetStatus> or the consuming class' +equivalent. + +=cut + +sub ValidateStatusChange { + my $self = shift; + my $new = shift; + my $old = $self->Status; + + my $lifecycle = $self->LifecycleObj; + + unless ( $lifecycle->IsValid( $new ) ) { + return (0, $self->loc("Status '[_1]' isn't a valid status for this [_2].", $self->loc($new), $self->loc($lifecycle->Type))); + } + + unless ( $lifecycle->IsTransition( $old => $new ) ) { + return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new))); + } + + my $check_right = $lifecycle->CheckRight( $old => $new ); + unless ( $self->CurrentUser->HasRight( Right => $check_right, Object => $self ) ) { + return ( 0, $self->loc('Permission Denied') ); + } + + return 1; +} + +=head2 SetStatus + +Validates the status transition before updating the Status column. This method +may want to be overridden by a more specific method in the consuming class. + +=cut + +sub SetStatus { + my $self = shift; + my $new = shift; + + my ($valid, $error) = $self->ValidateStatusChange($new); + return ($valid, $error) unless $valid; + + return $self->_SetStatus( Status => $new ); +} + +=head2 _SetStatus + +Sets the Status column without validating the change. Intended to be used +as-is by methods provided by the role, or overridden in the consuming class to +take additional action. For example, L<RT::Ticket/_SetStatus> sets the Started +and Resolved dates on the ticket as necessary. + +Takes a paramhash where the only required key is Status. Other keys may +include Lifecycle and NewLifecycle when called from L</_SetLifecycleColumn>, +which may assist consuming classes. NewLifecycle defaults to Lifecycle if not +provided; this indicates the lifecycle isn't changing. + +=cut + +sub _SetStatus { + my $self = shift; + my %args = ( + Status => undef, + Lifecycle => $self->LifecycleObj, + @_, + ); + $args{Status} = lc $args{Status} if defined $args{Status}; + $args{NewLifecycle} ||= $args{Lifecycle}; + + return $self->_Set( + Field => 'Status', + Value => $args{Status}, + ); +} + +=head2 _SetLifecycleColumn + +Validates and updates the column named by L</LifecycleColumn>. The Status +column is also updated if necessary (via lifecycle transition maps). + +On success, returns a tuple of (1, I<message>, I<new status>) where I<new +status> is the status that was transitioned to, if any. On failure, returns +(0, I<error message>). + +Takes a paramhash with keys I<Value> and (optionally) I<RequireRight>. +I<RequireRight> is a right name which the current user must have on the new +L</LifecycleColumn> object in order for the method to succeed. + +This method is expected to be used from within another method such as +L<RT::Ticket/SetQueue>. + +=cut + +sub _SetLifecycleColumn { + my $self = shift; + my %args = @_; + + my $column = $self->LifecycleColumn; + my $column_obj = "${column}Obj"; + + my $current = $self->$column_obj; + my $class = blessed($current); + + my $new = $class->new( $self->CurrentUser ); + $new->Load($args{Value}); + + return (0, $self->loc("[_1] [_2] does not exist", $self->loc($column), $args{Value})) + unless $new->id; + + my $name = eval { $current->Name } || $current->id; + + return (0, $self->loc("[_1] [_2] is disabled", $self->loc($column), $name)) + if $new->Disabled; + + return (0, $self->loc("[_1] is already set to [_2]", $self->loc($column), $name)) + if $new->id == $current->id; + + return (0, $self->loc("Permission Denied")) + if $args{RequireRight} and not $self->CurrentUser->HasRight( + Right => $args{RequireRight}, + Object => $new, + ); + + my $new_status; + my $old_lifecycle = $current->LifecycleObj; + my $new_lifecycle = $new->LifecycleObj; + if ( $old_lifecycle->Name ne $new_lifecycle->Name ) { + unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) { + return ( 0, $self->loc("There is no mapping for statuses between lifecycle [_1] and [_2]. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) ); + } + $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status }; + return ( 0, $self->loc("Mapping between lifecycle [_1] and [_2] is incomplete. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) ) + unless $new_status; + } + + my ($ok, $msg) = $self->_Set( Field => $column, Value => $new->id ); + if ($ok) { + if ( $new_status and $new_status ne $self->Status ) { + my $as_system = blessed($self)->new( RT->SystemUser ); + $as_system->Load( $self->Id ); + unless ( $as_system->Id ) { + return ( 0, $self->loc("Couldn't load copy of [_1] #[_2]", blessed($self), $self->Id) ); + } + + my ($val, $msg) = $as_system->_SetStatus( + Lifecycle => $old_lifecycle, + NewLifecycle => $new_lifecycle, + Status => $new_status, + ); + + if ($val) { + # Pick up the change made by the clone above + $self->Load( $self->id ); + } else { + RT->Logger->error("Status change to $new_status failed on $column change: $msg"); + undef $new_status; + } + } + return (1, $msg, $new_status); + } else { + return (0, $msg); + } +} + +1; |