rt 4.2.15
[freeside.git] / rt / lib / RT / Record / Role / Status.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 use strict;
50 use warnings;
51
52 package RT::Record::Role::Status;
53 use Role::Basic;
54 use Scalar::Util qw(blessed);
55
56 =head1 NAME
57
58 RT::Record::Role::Status - Common methods for records which have a Status column
59
60 =head1 DESCRIPTION
61
62 Lifecycles are generally set on container records, and Statuses on records
63 which belong to one of those containers.  L<RT::Record::Role::Lifecycle>
64 handles the containers with the I<Lifecycle> column.  This role is for the
65 records with a I<Status> column within those containers.  It includes
66 convenience methods for grabbing an L<RT::Lifecycle> object as well setters for
67 validating I<Status> and the column which points to the container object.
68
69 =head1 REQUIRES
70
71 =head2 L<RT::Record::Role>
72
73 =head2 LifecycleColumn
74
75 Used as a role parameter.  Must return a string of the column name which points
76 to the container object that consumes L<RT::Record::Role::Lifecycle> (or
77 conforms to it).  The resulting string is used to construct two method names:
78 as-is to fetch the column value and suffixed with "Obj" to fetch the object.
79
80 =head2 Status
81
82 A Status method which returns a lifecycle name is required.  Currently
83 unenforced at compile-time due to poor interactions with
84 L<DBIx::SearchBuilder::Record/AUTOLOAD>.  You'll hit run-time errors if this
85 method isn't available in consuming classes, however.
86
87 =cut
88
89 with 'RT::Record::Role';
90 requires 'LifecycleColumn';
91
92 =head1 PROVIDES
93
94 =head2 Status
95
96 Returns the Status for this record, in the canonical casing.
97
98 =cut
99
100 sub Status {
101     my $self = shift;
102     my $value = $self->_Value( 'Status' );
103     my $lifecycle = $self->LifecycleObj;
104     return $value unless $lifecycle;
105     return $lifecycle->CanonicalCase( $value );
106 }
107
108 =head2 LifecycleObj
109
110 Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>.  If called
111 as a class method, returns an L<RT::Lifecycle> object which is an aggregation
112 of all lifecycles of the appropriate type.
113
114 =cut
115
116 sub LifecycleObj {
117     my $self = shift;
118     my $obj  = $self->LifecycleColumn . "Obj";
119     return $self->$obj->LifecycleObj;
120 }
121
122 =head2 Lifecycle
123
124 Returns the L<RT::Lifecycle/Name> of this record's L</LifecycleObj>.
125
126 =cut
127
128 sub Lifecycle {
129     my $self = shift;
130     return $self->LifecycleObj->Name;
131 }
132
133 =head2 ValidateStatus
134
135 Takes a status.  Returns true if that status is a valid status for this record,
136 otherwise returns false.
137
138 =cut
139
140 sub ValidateStatus {
141     my $self = shift;
142     return $self->LifecycleObj->IsValid(@_);
143 }
144
145 =head2 ValidateStatusChange
146
147 Validates the new status with the current lifecycle.  Returns a tuple of (OK,
148 message).
149
150 Expected to be called from this role's L</SetStatus> or the consuming class'
151 equivalent.
152
153 =cut
154
155 sub ValidateStatusChange {
156     my $self = shift;
157     my $new  = shift;
158     my $old  = $self->Status;
159
160     my $lifecycle = $self->LifecycleObj;
161
162     unless ( $lifecycle->IsValid( $new ) ) {
163         return (0, $self->loc("Status '[_1]' isn't a valid status for this [_2].", $self->loc($new), $self->loc($lifecycle->Type)));
164     }
165
166     unless ( $lifecycle->IsTransition( $old => $new ) ) {
167         return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
168     }
169
170     my $check_right = $lifecycle->CheckRight( $old => $new );
171     unless ( $self->CurrentUser->HasRight( Right => $check_right, Object => $self ) ) {
172         return ( 0, $self->loc('Permission Denied') );
173     }
174
175     return 1;
176 }
177
178 =head2 SetStatus
179
180 Validates the status transition before updating the Status column.  This method
181 may want to be overridden by a more specific method in the consuming class.
182
183 =cut
184
185 sub SetStatus {
186     my $self = shift;
187     my $new  = shift;
188
189     my ($valid, $error) = $self->ValidateStatusChange($new);
190     return ($valid, $error) unless $valid;
191
192     return $self->_SetStatus( Status => $new );
193 }
194
195 =head2 _SetStatus
196
197 Sets the Status column without validating the change.  Intended to be used
198 as-is by methods provided by the role, or overridden in the consuming class to
199 take additional action.  For example, L<RT::Ticket/_SetStatus> sets the Started
200 and Resolved dates on the ticket as necessary.
201
202 Takes a paramhash where the only required key is Status.  Other keys may
203 include Lifecycle and NewLifecycle when called from L</_SetLifecycleColumn>,
204 which may assist consuming classes.  NewLifecycle defaults to Lifecycle if not
205 provided; this indicates the lifecycle isn't changing.
206
207 =cut
208
209 sub _SetStatus {
210     my $self = shift;
211     my %args = (
212         Status      => undef,
213         Lifecycle   => $self->LifecycleObj,
214         @_,
215     );
216     $args{Status} = lc $args{Status} if defined $args{Status};
217     $args{NewLifecycle} ||= $args{Lifecycle};
218
219     return $self->_Set(
220         Field   => 'Status',
221         Value   => $args{Status},
222     );
223 }
224
225 =head2 _SetLifecycleColumn
226
227 Validates and updates the column named by L</LifecycleColumn>.  The Status
228 column is also updated if necessary (via lifecycle transition maps).
229
230 On success, returns a tuple of (1, I<message>, I<new status>) where I<new
231 status> is the status that was transitioned to, if any.  On failure, returns
232 (0, I<error message>).
233
234 Takes a paramhash with keys I<Value> and (optionally) I<RequireRight>.
235 I<RequireRight> is a right name which the current user must have on the new
236 L</LifecycleColumn> object in order for the method to succeed.
237
238 This method is expected to be used from within another method such as
239 L<RT::Ticket/SetQueue>.
240
241 =cut
242
243 sub _SetLifecycleColumn {
244     my $self = shift;
245     my %args = @_;
246
247     my $column     = $self->LifecycleColumn;
248     my $column_obj = "${column}Obj";
249
250     my $current = $self->$column_obj;
251     my $class   = blessed($current);
252
253     my $new = $class->new( $self->CurrentUser );
254     $new->Load($args{Value});
255
256     return (0, $self->loc("[_1] [_2] does not exist", $self->loc($column), $args{Value}))
257         unless $new->id;
258
259     my $name = eval { $current->Name } || $current->id;
260
261     return (0, $self->loc("[_1] [_2] is disabled", $self->loc($column), $name))
262         if $new->Disabled;
263
264     return (0, $self->loc("[_1] is already set to [_2]", $self->loc($column), $name))
265         if $new->id == $current->id;
266
267     return (0, $self->loc("Permission Denied"))
268         if $args{RequireRight} and not $self->CurrentUser->HasRight(
269             Right   => $args{RequireRight},
270             Object  => $new,
271         );
272
273     my $new_status;
274     my $old_lifecycle = $current->LifecycleObj;
275     my $new_lifecycle = $new->LifecycleObj;
276     if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
277         unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
278             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) );
279         }
280         $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status };
281         return ( 0, $self->loc("Mapping between lifecycle [_1] and [_2] is incomplete. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) )
282             unless $new_status;
283     }
284
285     my ($ok, $msg) = $self->_Set( Field => $column, Value => $new->id );
286     if ($ok) {
287         if ( $new_status and $new_status ne $self->Status ) {
288             my $as_system = blessed($self)->new( RT->SystemUser );
289             $as_system->Load( $self->Id );
290             unless ( $as_system->Id ) {
291                 return ( 0, $self->loc("Couldn't load copy of [_1] #[_2]", blessed($self), $self->Id) );
292             }
293
294             my ($val, $msg) = $as_system->_SetStatus(
295                 Lifecycle       => $old_lifecycle,
296                 NewLifecycle    => $new_lifecycle,
297                 Status          => $new_status,
298             );
299
300             if ($val) {
301                 # Pick up the change made by the clone above
302                 $self->Load( $self->id );
303             } else {
304                 RT->Logger->error("Status change to $new_status failed on $column change: $msg");
305                 undef $new_status;
306             }
307         }
308         return (1, $msg, $new_status);
309     } else {
310         return (0, $msg);
311     }
312 }
313
314 1;