1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
53 package RT::Lifecycle;
56 our %LIFECYCLES_CACHE;
57 our %LIFECYCLES_TYPES;
62 # '' => [...], # all valid in lifecycle
67 # status_x => [status_next1, status_next2,...],
70 # 'status_y -> status_y' => 'right',
74 # { from => 'a', to => 'b', label => '...', update => '...' },
82 RT::Lifecycle - class to access and manipulate lifecycles
86 A lifecycle is a list of statuses that a ticket can have. There are three
87 groups of statuses: initial, active and inactive. A lifecycle also defines
88 possible transitions between statuses. For example, in the 'default' lifecycle,
89 you may only change status from 'stalled' to 'open'.
91 It is also possible to define user-interface labels and the action a user
92 should perform during a transition. For example, the "open -> stalled"
93 transition would have a 'Stall' label and the action would be Comment. The
94 action only defines what form is showed to the user, but actually performing
95 the action is not required. The user can leave the comment box empty yet still
96 Stall a ticket. Finally, the user can also just use the Basics or Jumbo form to
97 change the status with the usual dropdown.
103 Simple constructor, takes no arguments.
109 my $self = bless {}, ref($proto) || $proto;
111 $self->FillCache unless keys %LIFECYCLES_CACHE;
116 =head2 Load Name => I<NAME>, Type => I<TYPE>
118 Takes a name of the lifecycle and loads it. If only a Type is provided,
119 loads the global lifecycle with statuses from all named lifecycles of
122 Can be called as class method, returns a new object, for example:
124 my $lifecycle = RT::Lifecycle->Load( Name => 'default');
126 Returns an object which may be a subclass of L<RT::Lifecycle>
127 (L<RT::Lifecycle::Ticket>, for example) depending on the type of the
128 lifecycle in question.
134 return $self->new->Load( @_ )
137 unshift @_, Type => "ticket", "Name"
146 if (defined $args{Name} and exists $LIFECYCLES_CACHE{ $args{Name} }) {
147 $self->{'name'} = $args{Name};
148 $self->{'data'} = $LIFECYCLES_CACHE{ $args{Name} };
149 $self->{'type'} = $args{Type};
151 my $found_type = $self->{'data'}{'type'};
152 warn "Found type of $found_type ne $args{Type}" if $found_type ne $args{Type};
153 } elsif (not $args{Name} and exists $LIFECYCLES_TYPES{ $args{Type} }) {
154 $self->{'data'} = $LIFECYCLES_TYPES{ $args{Type} };
155 $self->{'type'} = $args{Type};
160 my $class = "RT::Lifecycle::".ucfirst($args{Type});
161 bless $self, $class if $class->require;
168 List available lifecycles. This list omits RT's default approvals
171 Takes: An optional parameter for lifecycle types other than tickets.
172 Defaults to 'ticket'.
174 Returns: A sorted list of available lifecycles.
180 my $for = shift || 'ticket';
182 return grep { $_ ne 'approvals' } $self->ListAll( $for );
187 Returns a list of all lifecycles, including approvals.
189 Takes: An optional parameter for lifecycle types other than tickets.
190 Defaults to 'ticket'.
192 Returns: A sorted list of all available lifecycles.
198 my $for = shift || 'ticket';
200 $self->FillCache unless keys %LIFECYCLES_CACHE;
202 return sort grep {$LIFECYCLES_CACHE{$_}{type} eq $for}
203 grep $_ ne '__maps__', keys %LIFECYCLES_CACHE;
208 Returns name of the loaded lifecycle.
212 sub Name { return $_[0]->{'name'} }
216 Returns the type of the loaded lifecycle.
220 sub Type { return $_[0]->{'type'} }
222 =head2 Getting statuses and validating.
224 Methods to get statuses in different sets or validating them.
228 Returns an array of all valid statuses for the current lifecycle.
229 Statuses are not sorted alphabetically, instead initial goes first,
230 then active and then inactive.
232 Takes optional list of status types, from 'initial', 'active' or
233 'inactive'. For example:
235 $lifecycle->Valid('initial', 'active');
243 return @{ $self->{'data'}{''} || [] };
247 push @res, @{ $self->{'data'}{ $_ } || [] } foreach @types;
253 Takes a status and returns true if value is a valid status for the current
254 lifecycle. Otherwise, returns false.
256 Takes optional list of status types after the status, so it's possible check
257 validity in particular sets, for example:
259 # returns true if status is valid and from initial or active set
260 $lifecycle->IsValid('some_status', 'initial', 'active');
268 my $value = shift or return 0;
269 return 1 if grep lc($_) eq lc($value), $self->Valid( @_ );
275 Takes a status and returns its type, one of 'initial', 'active' or
283 foreach my $type ( qw(initial active inactive) ) {
284 return $type if $self->IsValid( $status, $type );
291 Returns an array of all initial statuses for the current lifecycle.
297 return $self->Valid('initial');
302 Takes a status and returns true if value is a valid initial status.
303 Otherwise, returns false.
309 my $value = shift or return 0;
310 return 1 if grep lc($_) eq lc($value), $self->Valid('initial');
317 Returns an array of all active statuses for this lifecycle.
323 return $self->Valid('active');
328 Takes a value and returns true if value is a valid active status.
329 Otherwise, returns false.
335 my $value = shift or return 0;
336 return 1 if grep lc($_) eq lc($value), $self->Valid('active');
342 Returns an array of all inactive statuses for this lifecycle.
348 return $self->Valid('inactive');
353 Takes a value and returns true if value is a valid inactive status.
354 Otherwise, returns false.
360 my $value = shift or return 0;
361 return 1 if grep lc($_) eq lc($value), $self->Valid('inactive');
366 =head2 Default statuses
368 In some cases when status is not provided a default values should
373 Takes a situation name and returns value. Name should be
374 spelled following spelling in the RT config file.
380 my $situation = shift;
381 return $self->{data}{defaults}{ $situation };
384 =head3 DefaultOnCreate
386 Returns the status that should be used by default
387 when ticket is created.
391 sub DefaultOnCreate {
393 return $self->DefaultStatus('on_create');
396 =head2 Transitions, rights, labels and actions.
400 Takes status and returns list of statuses it can be changed to.
402 Is status is empty or undefined then returns list of statuses for
405 If argument is ommitted then returns a hash with all possible
406 transitions in the following format:
408 status_x => [ next_status, next_status, ... ],
409 status_y => [ next_status, next_status, ... ],
415 return %{ $self->{'data'}{'transitions'} || {} }
418 my $status = shift || '';
419 return @{ $self->{'data'}{'transitions'}{ lc $status } || [] };
424 Takes two statuses (from -> to) and returns true if it's valid
425 transition and false otherwise.
432 my $to = shift or return 0;
433 return 1 if grep lc($_) eq lc($to), $self->Transitions($from);
439 Takes two statuses (from -> to) and returns the right that should
440 be checked on the ticket.
448 if ( my $rights = $self->{'data'}{'rights'} ) {
450 $rights->{ $from .' -> '. $to }
451 || $rights->{ '* -> '. $to }
452 || $rights->{ $from .' -> *' }
453 || $rights->{ '* -> *' };
454 return $check if $check;
456 return $to eq 'deleted' ? 'DeleteTicket' : 'ModifyTicket';
459 =head3 RightsDescription [TYPE]
461 Returns hash with description of rights that are defined for
462 particular transitions.
466 sub RightsDescription {
470 $self->FillCache unless keys %LIFECYCLES_CACHE;
473 foreach my $lifecycle ( values %LIFECYCLES_CACHE ) {
474 next unless exists $lifecycle->{'rights'};
475 next if $type and $lifecycle->{type} ne $type;
476 while ( my ($transition, $right) = each %{ $lifecycle->{'rights'} } ) {
477 push @{ $tmp{ $right } ||=[] }, $transition;
482 while ( my ($right, $transitions) = each %tmp ) {
484 foreach ( @$transitions ) {
485 ($from[@from], $to[@to]) = split / -> /, $_;
487 my $description = 'Change status'
488 . ( (grep $_ eq '*', @from)? '' : ' from '. join ', ', @from )
489 . ( (grep $_ eq '*', @to )? '' : ' to '. join ', ', @to );
491 $res{ $right } = $description;
498 Takes a status and returns list of defined actions for the status. Each
499 element in the list is a hash reference with the following key/value
504 =item from - either the status or *
506 =item to - next status
508 =item label - label of the action
510 =item update - 'Respond', 'Comment' or '' (empty string)
518 my $from = shift || return ();
521 $self->FillCache unless keys %LIFECYCLES_CACHE;
523 my @res = grep lc $_->{'from'} eq $from || ( $_->{'from'} eq '*' && lc $_->{'to'} ne $from ),
524 @{ $self->{'data'}{'actions'} };
526 # skip '* -> x' if there is '$from -> x'
527 foreach my $e ( grep $_->{'from'} eq '*', @res ) {
528 $e = undef if grep $_->{'from'} ne '*' && $_->{'to'} eq $e->{'to'}, @res;
530 return grep defined, @res;
533 =head2 Moving tickets between lifecycles
537 Takes lifecycle as a name string or an object and returns a hash reference with
538 move map from this cycle to provided.
543 my $from = shift; # self
545 $to = RT::Lifecycle->Load( Name => $to, Type => $from->Type ) unless ref $to;
546 return $LIFECYCLES{'__maps__'}{ $from->Name .' -> '. $to->Name } || {};
551 Takes a lifecycle as a name string or an object and returns true if move map
552 defined for move from this cycle to provided.
558 my $map = $self->MoveMap( @_ );
559 return 0 unless $map && keys %$map;
560 return 0 unless grep defined && length, values %$map;
566 Takes no arguments and returns hash with pairs that has no
573 my $type = $self->Type;
574 my @list = $self->List( $type );
576 foreach my $from ( @list ) {
577 foreach my $to ( @list ) {
578 next if $from eq $to;
579 push @res, $from, $to
580 unless RT::Lifecycle->Load( Name => $from, Type => $type )->HasMoveMap( $to );
588 =head3 ForLocalization
590 A class method that takes no arguments and returns list of strings
591 that require translation.
595 sub ForLocalization {
597 $self->FillCache unless keys %LIFECYCLES_CACHE;
601 push @res, @{$_->{''}} for values %LIFECYCLES_TYPES;
602 foreach my $lifecycle ( values %LIFECYCLES ) {
604 grep defined && length,
607 @{ $lifecycle->{'actions'} || [] };
610 push @res, $self->RightsDescription;
613 return grep !$seen{lc $_}++, @res;
616 sub loc { return RT->SystemUser->loc( @_ ) }
621 return undef unless defined $status;
622 return($self->{data}{canonical_case}{lc $status} || lc $status);
628 my $map = RT->Config->Get('Lifecycles') or return;
630 %LIFECYCLES_CACHE = %LIFECYCLES = %$map;
631 $_ = { %$_ } foreach values %LIFECYCLES_CACHE;
633 foreach my $name ( keys %LIFECYCLES_CACHE ) {
634 next if $name eq "__maps__";
635 my $lifecycle = $LIFECYCLES_CACHE{$name};
637 my $type = $lifecycle->{type} ||= 'ticket';
638 $LIFECYCLES_TYPES{$type} ||= {
647 $lifecycle->{canonical_case} = {};
648 foreach my $category ( qw(initial active inactive) ) {
649 for my $status (@{ $lifecycle->{ $category } || [] }) {
650 if (exists $lifecycle->{canonical_case}{lc $status}) {
651 warn "Duplicate status @{[lc $status]} in lifecycle $name";
653 $lifecycle->{canonical_case}{lc $status} = $status;
655 push @{ $LIFECYCLES_TYPES{$type}{$category} }, $status;
656 push @statuses, $status;
660 # Lower-case for consistency
661 # ->{actions} are handled below
662 for my $state (keys %{ $lifecycle->{defaults} || {} }) {
663 my $status = $lifecycle->{defaults}{$state};
664 warn "Nonexistant status @{[lc $status]} in default states in $name lifecycle"
665 unless $lifecycle->{canonical_case}{lc $status};
666 $lifecycle->{defaults}{$state} =
667 $lifecycle->{canonical_case}{lc $status} || lc $status;
669 for my $from (keys %{ $lifecycle->{transitions} || {} }) {
670 warn "Nonexistant status @{[lc $from]} in transitions in $name lifecycle"
671 unless $from eq '' or $lifecycle->{canonical_case}{lc $from};
672 for my $status ( @{delete($lifecycle->{transitions}{$from}) || []} ) {
673 warn "Nonexistant status @{[lc $status]} in transitions in $name lifecycle"
674 unless $lifecycle->{canonical_case}{lc $status};
675 push @{ $lifecycle->{transitions}{lc $from} },
676 $lifecycle->{canonical_case}{lc $status} || lc $status;
679 for my $schema (keys %{ $lifecycle->{rights} || {} }) {
680 my ($from, $to) = split /\s*->\s*/, $schema, 2;
681 unless ($from and $to) {
682 warn "Invalid right transition $schema in $name lifecycle";
685 warn "Nonexistant status @{[lc $from]} in right transition in $name lifecycle"
686 unless $from eq '*' or $lifecycle->{canonical_case}{lc $from};
687 warn "Nonexistant status @{[lc $to]} in right transition in $name lifecycle"
688 unless $to eq '*' or $lifecycle->{canonical_case}{lc $to};
690 warn "Invalid right name ($lifecycle->{rights}{$schema}) in $name lifecycle; right names must be ASCII"
691 if $lifecycle->{rights}{$schema} =~ /\P{ASCII}/;
693 $lifecycle->{rights}{lc($from) . " -> " .lc($to)}
694 = delete $lifecycle->{rights}{$schema};
698 @statuses = grep !$seen{ lc $_ }++, @statuses;
699 $lifecycle->{''} = \@statuses;
701 unless ( $lifecycle->{'transitions'}{''} ) {
702 $lifecycle->{'transitions'}{''} = [ grep lc $_ ne 'deleted', @statuses ];
706 if ( ref $lifecycle->{'actions'} eq 'HASH' ) {
707 foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) {
708 push @actions, $k, $lifecycle->{'actions'}{ $k };
710 } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) {
711 @actions = @{ $lifecycle->{'actions'} };
714 $lifecycle->{'actions'} = [];
715 while ( my ($transition, $info) = splice @actions, 0, 2 ) {
716 my ($from, $to) = split /\s*->\s*/, $transition, 2;
717 unless ($from and $to) {
718 warn "Invalid action status change $transition in $name lifecycle";
721 warn "Nonexistant status @{[lc $from]} in action in $name lifecycle"
722 unless $from eq '*' or $lifecycle->{canonical_case}{lc $from};
723 warn "Nonexistant status @{[lc $to]} in action in $name lifecycle"
724 unless $to eq '*' or $lifecycle->{canonical_case}{lc $to};
725 push @{ $lifecycle->{'actions'} },
727 from => ($lifecycle->{canonical_case}{lc $from} || lc $from),
728 to => ($lifecycle->{canonical_case}{lc $to} || lc $to), };
732 # Lower-case the transition maps
733 for my $mapname (keys %{ $LIFECYCLES_CACHE{'__maps__'} || {} }) {
734 my ($from, $to) = split /\s*->\s*/, $mapname, 2;
735 unless ($from and $to) {
736 warn "Invalid lifecycle mapping $mapname";
739 warn "Nonexistant lifecycle $from in $mapname lifecycle map"
740 unless $LIFECYCLES_CACHE{$from};
741 warn "Nonexistant lifecycle $to in $mapname lifecycle map"
742 unless $LIFECYCLES_CACHE{$to};
743 my $map = delete $LIFECYCLES_CACHE{'__maps__'}{$mapname};
744 $LIFECYCLES_CACHE{'__maps__'}{"$from -> $to"} = $map;
745 for my $status (keys %{ $map }) {
746 warn "Nonexistant status @{[lc $status]} in $from in $mapname lifecycle map"
747 if $LIFECYCLES_CACHE{$from}
748 and not $LIFECYCLES_CACHE{$from}{canonical_case}{lc $status};
749 warn "Nonexistant status @{[lc $map->{$status}]} in $to in $mapname lifecycle map"
750 if $LIFECYCLES_CACHE{$to}
751 and not $LIFECYCLES_CACHE{$to}{canonical_case}{lc $map->{$status}};
752 $map->{lc $status} = lc delete $map->{$status};
756 for my $type (keys %LIFECYCLES_TYPES) {
757 for my $category ( qw(initial active inactive), '' ) {
759 @{ $LIFECYCLES_TYPES{$type}{$category} } =
760 grep !$seen{ lc $_ }++, @{ $LIFECYCLES_TYPES{$type}{$category} };
761 push @{ $LIFECYCLES_TYPES{$type}{''} },
762 @{ $LIFECYCLES_TYPES{$type}{$category} } if $category;
765 my $class = "RT::Lifecycle::".ucfirst($type);
766 $class->RegisterRights if $class->require
767 and $class->can("RegisterRights");