]>
Commit | Line | Data |
---|---|---|
af59614d MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
5 | # This software is Copyright (c) 1996-2013 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 ) { | |
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; |