--- /dev/null
+# 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;