]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
5 | # This software is Copyright (c) 1996-2012 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 | ||
53 | package RT::Lifecycle; | |
54 | ||
55 | our %LIFECYCLES; | |
56 | our %LIFECYCLES_CACHE; | |
57 | __PACKAGE__->RegisterRights; | |
58 | ||
59 | # cache structure: | |
60 | # { | |
61 | # '' => { # all valid statuses | |
62 | # '' => [...], | |
63 | # initial => [...], | |
64 | # active => [...], | |
65 | # inactive => [...], | |
66 | # }, | |
67 | # lifecycle_x => { | |
68 | # '' => [...], # all valid in lifecycle | |
69 | # initial => [...], | |
70 | # active => [...], | |
71 | # inactive => [...], | |
72 | # transitions => { | |
73 | # status_x => [status_next1, status_next2,...], | |
74 | # }, | |
75 | # rights => { | |
76 | # 'status_y -> status_y' => 'right', | |
77 | # .... | |
78 | # } | |
79 | # actions => [ | |
80 | # { from => 'a', to => 'b', label => '...', update => '...' }, | |
81 | # .... | |
82 | # ] | |
83 | # } | |
84 | # } | |
85 | ||
86 | =head1 NAME | |
87 | ||
88 | RT::Lifecycle - class to access and manipulate lifecycles | |
89 | ||
90 | =head1 DESCRIPTION | |
91 | ||
92 | A lifecycle is a list of statuses that a ticket can have. There are three | |
93 | groups of statuses: initial, active and inactive. A lifecycle also defines | |
94 | possible transitions between statuses. For example, in the 'default' lifecycle, | |
95 | you may only change status from 'stalled' to 'open'. | |
96 | ||
97 | It is also possible to define user-interface labels and the action a user | |
98 | should perform during a transition. For example, the "open -> stalled" | |
99 | transition would have a 'Stall' label and the action would be Comment. The | |
100 | action only defines what form is showed to the user, but actually performing | |
101 | the action is not required. The user can leave the comment box empty yet still | |
102 | Stall a ticket. Finally, the user can also just use the Basics or Jumbo form to | |
103 | change the status with the usual dropdown. | |
104 | ||
105 | =head1 METHODS | |
106 | ||
107 | =head2 new | |
108 | ||
109 | Simple constructor, takes no arguments. | |
110 | ||
111 | =cut | |
112 | ||
113 | sub new { | |
114 | my $proto = shift; | |
115 | my $self = bless {}, ref($proto) || $proto; | |
116 | ||
117 | $self->FillCache unless keys %LIFECYCLES_CACHE; | |
118 | ||
119 | return $self; | |
120 | } | |
121 | ||
122 | =head2 Load | |
123 | ||
124 | Takes a name of the lifecycle and loads it. If name is empty or undefined then | |
125 | loads the global lifecycle with statuses from all named lifecycles. | |
126 | ||
127 | Can be called as class method, returns a new object, for example: | |
128 | ||
129 | my $lifecycle = RT::Lifecycle->Load('default'); | |
130 | ||
131 | =cut | |
132 | ||
133 | sub Load { | |
134 | my $self = shift; | |
135 | my $name = shift || ''; | |
136 | return $self->new->Load( $name, @_ ) | |
137 | unless ref $self; | |
138 | ||
139 | return unless exists $LIFECYCLES_CACHE{ $name }; | |
140 | ||
141 | $self->{'name'} = $name; | |
142 | $self->{'data'} = $LIFECYCLES_CACHE{ $name }; | |
143 | ||
144 | return $self; | |
145 | } | |
146 | ||
147 | =head2 List | |
148 | ||
149 | Returns sorted list of the lifecycles' names. | |
150 | ||
151 | =cut | |
152 | ||
153 | sub List { | |
154 | my $self = shift; | |
155 | ||
156 | $self->FillCache unless keys %LIFECYCLES_CACHE; | |
157 | ||
158 | return sort grep length && $_ ne '__maps__', keys %LIFECYCLES_CACHE; | |
159 | } | |
160 | ||
161 | =head2 Name | |
162 | ||
163 | Returns name of the laoded lifecycle. | |
164 | ||
165 | =cut | |
166 | ||
167 | sub Name { return $_[0]->{'name'} } | |
168 | ||
169 | =head2 Queues | |
170 | ||
171 | Returns L<RT::Queues> collection with queues that use this lifecycle. | |
172 | ||
173 | =cut | |
174 | ||
175 | sub Queues { | |
176 | my $self = shift; | |
177 | require RT::Queues; | |
178 | my $queues = RT::Queues->new( RT->SystemUser ); | |
179 | $queues->Limit( FIELD => 'Lifecycle', VALUE => $self->Name ); | |
180 | return $queues; | |
181 | } | |
182 | ||
183 | =head2 Getting statuses and validating. | |
184 | ||
185 | Methods to get statuses in different sets or validating them. | |
186 | ||
187 | =head3 Valid | |
188 | ||
189 | Returns an array of all valid statuses for the current lifecycle. | |
190 | Statuses are not sorted alphabetically, instead initial goes first, | |
191 | then active and then inactive. | |
192 | ||
193 | Takes optional list of status types, from 'initial', 'active' or | |
194 | 'inactive'. For example: | |
195 | ||
196 | $lifecycle->Valid('initial', 'active'); | |
197 | ||
198 | =cut | |
199 | ||
200 | sub Valid { | |
201 | my $self = shift; | |
202 | my @types = @_; | |
203 | unless ( @types ) { | |
204 | return @{ $self->{'data'}{''} || [] }; | |
205 | } | |
206 | ||
207 | my @res; | |
208 | push @res, @{ $self->{'data'}{ $_ } || [] } foreach @types; | |
209 | return @res; | |
210 | } | |
211 | ||
212 | =head3 IsValid | |
213 | ||
214 | Takes a status and returns true if value is a valid status for the current | |
215 | lifecycle. Otherwise, returns false. | |
216 | ||
217 | Takes optional list of status types after the status, so it's possible check | |
218 | validity in particular sets, for example: | |
219 | ||
220 | # returns true if status is valid and from initial or active set | |
221 | $lifecycle->IsValid('some_status', 'initial', 'active'); | |
222 | ||
223 | See also </valid>. | |
224 | ||
225 | =cut | |
226 | ||
227 | sub IsValid { | |
228 | my $self = shift; | |
229 | my $value = shift or return 0; | |
230 | return 1 if grep lc($_) eq lc($value), $self->Valid( @_ ); | |
231 | return 0; | |
232 | } | |
233 | ||
234 | =head3 StatusType | |
235 | ||
236 | Takes a status and returns its type, one of 'initial', 'active' or | |
237 | 'inactive'. | |
238 | ||
239 | =cut | |
240 | ||
241 | sub StatusType { | |
242 | my $self = shift; | |
243 | my $status = shift; | |
244 | foreach my $type ( qw(initial active inactive) ) { | |
245 | return $type if $self->IsValid( $status, $type ); | |
246 | } | |
247 | return ''; | |
248 | } | |
249 | ||
250 | =head3 Initial | |
251 | ||
252 | Returns an array of all initial statuses for the current lifecycle. | |
253 | ||
254 | =cut | |
255 | ||
256 | sub Initial { | |
257 | my $self = shift; | |
258 | return $self->Valid('initial'); | |
259 | } | |
260 | ||
261 | =head3 IsInitial | |
262 | ||
263 | Takes a status and returns true if value is a valid initial status. | |
264 | Otherwise, returns false. | |
265 | ||
266 | =cut | |
267 | ||
268 | sub IsInitial { | |
269 | my $self = shift; | |
270 | my $value = shift or return 0; | |
271 | return 1 if grep lc($_) eq lc($value), $self->Valid('initial'); | |
272 | return 0; | |
273 | } | |
274 | ||
275 | ||
276 | =head3 Active | |
277 | ||
278 | Returns an array of all active statuses for this lifecycle. | |
279 | ||
280 | =cut | |
281 | ||
282 | sub Active { | |
283 | my $self = shift; | |
284 | return $self->Valid('active'); | |
285 | } | |
286 | ||
287 | =head3 IsActive | |
288 | ||
289 | Takes a value and returns true if value is a valid active status. | |
290 | Otherwise, returns false. | |
291 | ||
292 | =cut | |
293 | ||
294 | sub IsActive { | |
295 | my $self = shift; | |
296 | my $value = shift or return 0; | |
297 | return 1 if grep lc($_) eq lc($value), $self->Valid('active'); | |
298 | return 0; | |
299 | } | |
300 | ||
301 | =head3 inactive | |
302 | ||
303 | Returns an array of all inactive statuses for this lifecycle. | |
304 | ||
305 | =cut | |
306 | ||
307 | sub Inactive { | |
308 | my $self = shift; | |
309 | return $self->Valid('inactive'); | |
310 | } | |
311 | ||
312 | =head3 is_inactive | |
313 | ||
314 | Takes a value and returns true if value is a valid inactive status. | |
315 | Otherwise, returns false. | |
316 | ||
317 | =cut | |
318 | ||
319 | sub IsInactive { | |
320 | my $self = shift; | |
321 | my $value = shift or return 0; | |
322 | return 1 if grep lc($_) eq lc($value), $self->Valid('inactive'); | |
323 | return 0; | |
324 | } | |
325 | ||
326 | ||
327 | =head2 Default statuses | |
328 | ||
329 | In some cases when status is not provided a default values should | |
330 | be used. | |
331 | ||
332 | =head3 DefaultStatus | |
333 | ||
334 | Takes a situation name and returns value. Name should be | |
335 | spelled following spelling in the RT config file. | |
336 | ||
337 | =cut | |
338 | ||
339 | sub DefaultStatus { | |
340 | my $self = shift; | |
341 | my $situation = shift; | |
342 | return $self->{data}{defaults}{ $situation }; | |
343 | } | |
344 | ||
345 | =head3 DefaultOnCreate | |
346 | ||
347 | Returns the status that should be used by default | |
348 | when ticket is created. | |
349 | ||
350 | =cut | |
351 | ||
352 | sub DefaultOnCreate { | |
353 | my $self = shift; | |
354 | return $self->DefaultStatus('on_create'); | |
355 | } | |
356 | ||
357 | ||
358 | =head3 DefaultOnMerge | |
359 | ||
360 | Returns the status that should be used when tickets | |
361 | are merged. | |
362 | ||
363 | =cut | |
364 | ||
365 | sub DefaultOnMerge { | |
366 | my $self = shift; | |
367 | return $self->DefaultStatus('on_merge'); | |
368 | } | |
369 | ||
370 | =head2 Transitions, rights, labels and actions. | |
371 | ||
372 | =head3 Transitions | |
373 | ||
374 | Takes status and returns list of statuses it can be changed to. | |
375 | ||
376 | Is status is empty or undefined then returns list of statuses for | |
377 | a new ticket. | |
378 | ||
379 | If argument is ommitted then returns a hash with all possible | |
380 | transitions in the following format: | |
381 | ||
382 | status_x => [ next_status, next_status, ... ], | |
383 | status_y => [ next_status, next_status, ... ], | |
384 | ||
385 | =cut | |
386 | ||
387 | sub Transitions { | |
388 | my $self = shift; | |
389 | return %{ $self->{'data'}{'transitions'} || {} } | |
390 | unless @_; | |
391 | ||
392 | my $status = shift; | |
393 | return @{ $self->{'data'}{'transitions'}{ $status || '' } || [] }; | |
394 | } | |
395 | ||
396 | =head1 IsTransition | |
397 | ||
398 | Takes two statuses (from -> to) and returns true if it's valid | |
399 | transition and false otherwise. | |
400 | ||
401 | =cut | |
402 | ||
403 | sub IsTransition { | |
404 | my $self = shift; | |
405 | my $from = shift; | |
406 | my $to = shift or return 0; | |
407 | return 1 if grep lc($_) eq lc($to), $self->Transitions($from); | |
408 | return 0; | |
409 | } | |
410 | ||
411 | =head3 CheckRight | |
412 | ||
413 | Takes two statuses (from -> to) and returns the right that should | |
414 | be checked on the ticket. | |
415 | ||
416 | =cut | |
417 | ||
418 | sub CheckRight { | |
419 | my $self = shift; | |
420 | my $from = shift; | |
421 | my $to = shift; | |
422 | if ( my $rights = $self->{'data'}{'rights'} ) { | |
423 | my $check = | |
424 | $rights->{ $from .' -> '. $to } | |
425 | || $rights->{ '* -> '. $to } | |
426 | || $rights->{ $from .' -> *' } | |
427 | || $rights->{ '* -> *' }; | |
428 | return $check if $check; | |
429 | } | |
430 | return $to eq 'deleted' ? 'DeleteTicket' : 'ModifyTicket'; | |
431 | } | |
432 | ||
433 | =head3 RegisterRights | |
434 | ||
435 | Registers all defined rights in the system, so they can be addigned | |
436 | to users. No need to call it, as it's called when module is loaded. | |
437 | ||
438 | =cut | |
439 | ||
440 | sub RegisterRights { | |
441 | my $self = shift; | |
442 | ||
443 | my %rights = $self->RightsDescription; | |
444 | ||
445 | require RT::ACE; | |
446 | ||
447 | require RT::Queue; | |
448 | my $RIGHTS = $RT::Queue::RIGHTS; | |
449 | ||
450 | while ( my ($right, $description) = each %rights ) { | |
451 | next if exists $RIGHTS->{ $right }; | |
452 | ||
453 | $RIGHTS->{ $right } = $description; | |
454 | RT::Queue->AddRightCategories( $right => 'Status' ); | |
455 | $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right; | |
456 | } | |
457 | } | |
458 | ||
459 | =head3 RightsDescription | |
460 | ||
461 | Returns hash with description of rights that are defined for | |
462 | particular transitions. | |
463 | ||
464 | =cut | |
465 | ||
466 | sub RightsDescription { | |
467 | my $self = shift; | |
468 | ||
469 | $self->FillCache unless keys %LIFECYCLES_CACHE; | |
470 | ||
471 | my %tmp; | |
472 | foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { | |
473 | next unless exists $lifecycle->{'rights'}; | |
474 | while ( my ($transition, $right) = each %{ $lifecycle->{'rights'} } ) { | |
475 | push @{ $tmp{ $right } ||=[] }, $transition; | |
476 | } | |
477 | } | |
478 | ||
479 | my %res; | |
480 | while ( my ($right, $transitions) = each %tmp ) { | |
481 | my (@from, @to); | |
482 | foreach ( @$transitions ) { | |
483 | ($from[@from], $to[@to]) = split / -> /, $_; | |
484 | } | |
485 | my $description = 'Change status' | |
486 | . ( (grep $_ eq '*', @from)? '' : ' from '. join ', ', @from ) | |
487 | . ( (grep $_ eq '*', @to )? '' : ' to '. join ', ', @to ); | |
488 | ||
489 | $res{ $right } = $description; | |
490 | } | |
491 | return %res; | |
492 | } | |
493 | ||
494 | =head3 Actions | |
495 | ||
496 | Takes a status and returns list of defined actions for the status. Each | |
497 | element in the list is a hash reference with the following key/value | |
498 | pairs: | |
499 | ||
500 | =over 4 | |
501 | ||
502 | =item from - either the status or * | |
503 | ||
504 | =item to - next status | |
505 | ||
506 | =item label - label of the action | |
507 | ||
508 | =item update - 'Respond', 'Comment' or '' (empty string) | |
509 | ||
510 | =back | |
511 | ||
512 | =cut | |
513 | ||
514 | sub Actions { | |
515 | my $self = shift; | |
516 | my $from = shift || return (); | |
517 | ||
518 | $self->FillCache unless keys %LIFECYCLES_CACHE; | |
519 | ||
520 | my @res = grep $_->{'from'} eq $from || ( $_->{'from'} eq '*' && $_->{'to'} ne $from ), | |
521 | @{ $self->{'data'}{'actions'} }; | |
522 | ||
523 | # skip '* -> x' if there is '$from -> x' | |
524 | foreach my $e ( grep $_->{'from'} eq '*', @res ) { | |
525 | $e = undef if grep $_->{'from'} ne '*' && $_->{'to'} eq $e->{'to'}, @res; | |
526 | } | |
527 | return grep defined, @res; | |
528 | } | |
529 | ||
530 | =head2 Moving tickets between lifecycles | |
531 | ||
532 | =head3 MoveMap | |
533 | ||
534 | Takes lifecycle as a name string or an object and returns a hash reference with | |
535 | move map from this cycle to provided. | |
536 | ||
537 | =cut | |
538 | ||
539 | sub MoveMap { | |
540 | my $from = shift; # self | |
541 | my $to = shift; | |
542 | $to = RT::Lifecycle->Load( $to ) unless ref $to; | |
543 | return $LIFECYCLES{'__maps__'}{ $from->Name .' -> '. $to->Name } || {}; | |
544 | } | |
545 | ||
546 | =head3 HasMoveMap | |
547 | ||
548 | Takes a lifecycle as a name string or an object and returns true if move map | |
549 | defined for move from this cycle to provided. | |
550 | ||
551 | =cut | |
552 | ||
553 | sub HasMoveMap { | |
554 | my $self = shift; | |
555 | my $map = $self->MoveMap( @_ ); | |
556 | return 0 unless $map && keys %$map; | |
557 | return 0 unless grep defined && length, values %$map; | |
558 | return 1; | |
559 | } | |
560 | ||
561 | =head3 NoMoveMaps | |
562 | ||
563 | Takes no arguments and returns hash with pairs that has no | |
564 | move maps. | |
565 | ||
566 | =cut | |
567 | ||
568 | sub NoMoveMaps { | |
569 | my $self = shift; | |
570 | my @list = $self->List; | |
571 | my @res; | |
572 | foreach my $from ( @list ) { | |
573 | foreach my $to ( @list ) { | |
574 | next if $from eq $to; | |
575 | push @res, $from, $to | |
576 | unless RT::Lifecycle->Load( $from )->HasMoveMap( $to ); | |
577 | } | |
578 | } | |
579 | return @res; | |
580 | } | |
581 | ||
582 | =head2 Localization | |
583 | ||
584 | =head3 ForLocalization | |
585 | ||
586 | A class method that takes no arguments and returns list of strings | |
587 | that require translation. | |
588 | ||
589 | =cut | |
590 | ||
591 | sub ForLocalization { | |
592 | my $self = shift; | |
593 | $self->FillCache unless keys %LIFECYCLES_CACHE; | |
594 | ||
595 | my @res = (); | |
596 | ||
597 | push @res, @{ $LIFECYCLES_CACHE{''}{''} || [] }; | |
598 | foreach my $lifecycle ( values %LIFECYCLES ) { | |
599 | push @res, | |
600 | grep defined && length, | |
601 | map $_->{'label'}, | |
602 | grep ref($_), | |
603 | @{ $lifecycle->{'actions'} || [] }; | |
604 | } | |
605 | ||
606 | push @res, $self->RightsDescription; | |
607 | ||
608 | my %seen; | |
609 | return grep !$seen{lc $_}++, @res; | |
610 | } | |
611 | ||
612 | sub loc { return RT->SystemUser->loc( @_ ) } | |
613 | ||
614 | sub FillCache { | |
615 | my $self = shift; | |
616 | ||
617 | my $map = RT->Config->Get('Lifecycles') or return; | |
618 | ||
619 | %LIFECYCLES_CACHE = %LIFECYCLES = %$map; | |
620 | $_ = { %$_ } foreach values %LIFECYCLES_CACHE; | |
621 | ||
622 | my %all = ( | |
623 | '' => [], | |
624 | initial => [], | |
625 | active => [], | |
626 | inactive => [], | |
627 | ); | |
628 | foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { | |
629 | my @res; | |
630 | foreach my $type ( qw(initial active inactive) ) { | |
631 | push @{ $all{ $type } }, @{ $lifecycle->{ $type } || [] }; | |
632 | push @res, @{ $lifecycle->{ $type } || [] }; | |
633 | } | |
634 | ||
635 | my %seen; | |
636 | @res = grep !$seen{ lc $_ }++, @res; | |
637 | $lifecycle->{''} = \@res; | |
638 | ||
639 | unless ( $lifecycle->{'transitions'}{''} ) { | |
640 | $lifecycle->{'transitions'}{''} = [ grep $_ ne 'deleted', @res ]; | |
641 | } | |
642 | } | |
643 | foreach my $type ( qw(initial active inactive), '' ) { | |
644 | my %seen; | |
645 | @{ $all{ $type } } = grep !$seen{ lc $_ }++, @{ $all{ $type } }; | |
646 | push @{ $all{''} }, @{ $all{ $type } } if $type; | |
647 | } | |
648 | $LIFECYCLES_CACHE{''} = \%all; | |
649 | ||
650 | foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { | |
651 | my @res; | |
652 | if ( ref $lifecycle->{'actions'} eq 'HASH' ) { | |
653 | foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) { | |
654 | push @res, $k, $lifecycle->{'actions'}{ $k }; | |
655 | } | |
656 | } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) { | |
657 | @res = @{ $lifecycle->{'actions'} }; | |
658 | } | |
659 | ||
660 | my @tmp = splice @res; | |
661 | while ( my ($transition, $info) = splice @tmp, 0, 2 ) { | |
662 | my ($from, $to) = split /\s*->\s*/, $transition, 2; | |
663 | push @res, { %$info, from => $from, to => $to }; | |
664 | } | |
665 | $lifecycle->{'actions'} = \@res; | |
666 | } | |
667 | return; | |
668 | } | |
669 | ||
670 | 1; |