1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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 __PACKAGE__->RegisterRights;
61 # '' => { # all valid statuses
68 # '' => [...], # all valid in lifecycle
73 # status_x => [status_next1, status_next2,...],
76 # 'status_y -> status_y' => 'right',
80 # { from => 'a', to => 'b', label => '...', update => '...' },
88 RT::Lifecycle - class to access and manipulate lifecycles
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'.
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.
109 Simple constructor, takes no arguments.
115 my $self = bless {}, ref($proto) || $proto;
117 $self->FillCache unless keys %LIFECYCLES_CACHE;
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.
127 Can be called as class method, returns a new object, for example:
129 my $lifecycle = RT::Lifecycle->Load('default');
135 my $name = shift || '';
136 return $self->new->Load( $name, @_ )
139 return unless exists $LIFECYCLES_CACHE{ $name };
141 $self->{'name'} = $name;
142 $self->{'data'} = $LIFECYCLES_CACHE{ $name };
149 Returns sorted list of the lifecycles' names.
156 $self->FillCache unless keys %LIFECYCLES_CACHE;
158 return sort grep length && $_ ne '__maps__', keys %LIFECYCLES_CACHE;
163 Returns name of the laoded lifecycle.
167 sub Name { return $_[0]->{'name'} }
171 Returns L<RT::Queues> collection with queues that use this lifecycle.
178 my $queues = RT::Queues->new( RT->SystemUser );
179 $queues->Limit( FIELD => 'Lifecycle', VALUE => $self->Name );
183 =head2 Getting statuses and validating.
185 Methods to get statuses in different sets or validating them.
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.
193 Takes optional list of status types, from 'initial', 'active' or
194 'inactive'. For example:
196 $lifecycle->Valid('initial', 'active');
204 return @{ $self->{'data'}{''} || [] };
208 push @res, @{ $self->{'data'}{ $_ } || [] } foreach @types;
214 Takes a status and returns true if value is a valid status for the current
215 lifecycle. Otherwise, returns false.
217 Takes optional list of status types after the status, so it's possible check
218 validity in particular sets, for example:
220 # returns true if status is valid and from initial or active set
221 $lifecycle->IsValid('some_status', 'initial', 'active');
229 my $value = shift or return 0;
230 return 1 if grep lc($_) eq lc($value), $self->Valid( @_ );
236 Takes a status and returns its type, one of 'initial', 'active' or
244 foreach my $type ( qw(initial active inactive) ) {
245 return $type if $self->IsValid( $status, $type );
252 Returns an array of all initial statuses for the current lifecycle.
258 return $self->Valid('initial');
263 Takes a status and returns true if value is a valid initial status.
264 Otherwise, returns false.
270 my $value = shift or return 0;
271 return 1 if grep lc($_) eq lc($value), $self->Valid('initial');
278 Returns an array of all active statuses for this lifecycle.
284 return $self->Valid('active');
289 Takes a value and returns true if value is a valid active status.
290 Otherwise, returns false.
296 my $value = shift or return 0;
297 return 1 if grep lc($_) eq lc($value), $self->Valid('active');
303 Returns an array of all inactive statuses for this lifecycle.
309 return $self->Valid('inactive');
314 Takes a value and returns true if value is a valid inactive status.
315 Otherwise, returns false.
321 my $value = shift or return 0;
322 return 1 if grep lc($_) eq lc($value), $self->Valid('inactive');
327 =head2 Default statuses
329 In some cases when status is not provided a default values should
334 Takes a situation name and returns value. Name should be
335 spelled following spelling in the RT config file.
341 my $situation = shift;
342 return $self->{data}{defaults}{ $situation };
345 =head3 DefaultOnCreate
347 Returns the status that should be used by default
348 when ticket is created.
352 sub DefaultOnCreate {
354 return $self->DefaultStatus('on_create');
358 =head3 DefaultOnMerge
360 Returns the status that should be used when tickets
367 return $self->DefaultStatus('on_merge');
370 =head3 ReminderStatusOnOpen
372 Returns the status that should be used when reminders are opened.
376 sub ReminderStatusOnOpen {
378 return $self->DefaultStatus('reminder_on_open') || 'open';
381 =head3 ReminderStatusOnResolve
383 Returns the status that should be used when reminders are resolved.
387 sub ReminderStatusOnResolve {
389 return $self->DefaultStatus('reminder_on_resolve') || 'resolved';
392 =head2 Transitions, rights, labels and actions.
396 Takes status and returns list of statuses it can be changed to.
398 Is status is empty or undefined then returns list of statuses for
401 If argument is ommitted then returns a hash with all possible
402 transitions in the following format:
404 status_x => [ next_status, next_status, ... ],
405 status_y => [ next_status, next_status, ... ],
411 return %{ $self->{'data'}{'transitions'} || {} }
415 return @{ $self->{'data'}{'transitions'}{ $status || '' } || [] };
420 Takes two statuses (from -> to) and returns true if it's valid
421 transition and false otherwise.
428 my $to = shift or return 0;
429 return 1 if grep lc($_) eq lc($to), $self->Transitions($from);
435 Takes two statuses (from -> to) and returns the right that should
436 be checked on the ticket.
444 if ( my $rights = $self->{'data'}{'rights'} ) {
446 $rights->{ $from .' -> '. $to }
447 || $rights->{ '* -> '. $to }
448 || $rights->{ $from .' -> *' }
449 || $rights->{ '* -> *' };
450 return $check if $check;
452 return $to eq 'deleted' ? 'DeleteTicket' : 'ModifyTicket';
455 =head3 RegisterRights
457 Registers all defined rights in the system, so they can be addigned
458 to users. No need to call it, as it's called when module is loaded.
465 my %rights = $self->RightsDescription;
470 my $RIGHTS = $RT::Queue::RIGHTS;
472 while ( my ($right, $description) = each %rights ) {
473 next if exists $RIGHTS->{ $right };
475 $RIGHTS->{ $right } = $description;
476 RT::Queue->AddRightCategories( $right => 'Status' );
477 $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
481 =head3 RightsDescription
483 Returns hash with description of rights that are defined for
484 particular transitions.
488 sub RightsDescription {
491 $self->FillCache unless keys %LIFECYCLES_CACHE;
494 foreach my $lifecycle ( values %LIFECYCLES_CACHE ) {
495 next unless exists $lifecycle->{'rights'};
496 while ( my ($transition, $right) = each %{ $lifecycle->{'rights'} } ) {
497 push @{ $tmp{ $right } ||=[] }, $transition;
502 while ( my ($right, $transitions) = each %tmp ) {
504 foreach ( @$transitions ) {
505 ($from[@from], $to[@to]) = split / -> /, $_;
507 my $description = 'Change status'
508 . ( (grep $_ eq '*', @from)? '' : ' from '. join ', ', @from )
509 . ( (grep $_ eq '*', @to )? '' : ' to '. join ', ', @to );
511 $res{ $right } = $description;
518 Takes a status and returns list of defined actions for the status. Each
519 element in the list is a hash reference with the following key/value
524 =item from - either the status or *
526 =item to - next status
528 =item label - label of the action
530 =item update - 'Respond', 'Comment' or '' (empty string)
538 my $from = shift || return ();
540 $self->FillCache unless keys %LIFECYCLES_CACHE;
542 my @res = grep $_->{'from'} eq $from || ( $_->{'from'} eq '*' && $_->{'to'} ne $from ),
543 @{ $self->{'data'}{'actions'} };
545 # skip '* -> x' if there is '$from -> x'
546 foreach my $e ( grep $_->{'from'} eq '*', @res ) {
547 $e = undef if grep $_->{'from'} ne '*' && $_->{'to'} eq $e->{'to'}, @res;
549 return grep defined, @res;
552 =head2 Moving tickets between lifecycles
556 Takes lifecycle as a name string or an object and returns a hash reference with
557 move map from this cycle to provided.
562 my $from = shift; # self
564 $to = RT::Lifecycle->Load( $to ) unless ref $to;
565 return $LIFECYCLES{'__maps__'}{ $from->Name .' -> '. $to->Name } || {};
570 Takes a lifecycle as a name string or an object and returns true if move map
571 defined for move from this cycle to provided.
577 my $map = $self->MoveMap( @_ );
578 return 0 unless $map && keys %$map;
579 return 0 unless grep defined && length, values %$map;
585 Takes no arguments and returns hash with pairs that has no
592 my @list = $self->List;
594 foreach my $from ( @list ) {
595 foreach my $to ( @list ) {
596 next if $from eq $to;
597 push @res, $from, $to
598 unless RT::Lifecycle->Load( $from )->HasMoveMap( $to );
606 =head3 ForLocalization
608 A class method that takes no arguments and returns list of strings
609 that require translation.
613 sub ForLocalization {
615 $self->FillCache unless keys %LIFECYCLES_CACHE;
619 push @res, @{ $LIFECYCLES_CACHE{''}{''} || [] };
620 foreach my $lifecycle ( values %LIFECYCLES ) {
622 grep defined && length,
625 @{ $lifecycle->{'actions'} || [] };
628 push @res, $self->RightsDescription;
631 return grep !$seen{lc $_}++, @res;
634 sub loc { return RT->SystemUser->loc( @_ ) }
639 my $map = RT->Config->Get('Lifecycles') or return;
641 %LIFECYCLES_CACHE = %LIFECYCLES = %$map;
642 $_ = { %$_ } foreach values %LIFECYCLES_CACHE;
650 foreach my $lifecycle ( values %LIFECYCLES_CACHE ) {
652 foreach my $type ( qw(initial active inactive) ) {
653 push @{ $all{ $type } }, @{ $lifecycle->{ $type } || [] };
654 push @res, @{ $lifecycle->{ $type } || [] };
658 @res = grep !$seen{ lc $_ }++, @res;
659 $lifecycle->{''} = \@res;
661 unless ( $lifecycle->{'transitions'}{''} ) {
662 $lifecycle->{'transitions'}{''} = [ grep $_ ne 'deleted', @res ];
665 foreach my $type ( qw(initial active inactive), '' ) {
667 @{ $all{ $type } } = grep !$seen{ lc $_ }++, @{ $all{ $type } };
668 push @{ $all{''} }, @{ $all{ $type } } if $type;
670 $LIFECYCLES_CACHE{''} = \%all;
672 foreach my $lifecycle ( values %LIFECYCLES_CACHE ) {
674 if ( ref $lifecycle->{'actions'} eq 'HASH' ) {
675 foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) {
676 push @res, $k, $lifecycle->{'actions'}{ $k };
678 } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) {
679 @res = @{ $lifecycle->{'actions'} };
682 my @tmp = splice @res;
683 while ( my ($transition, $info) = splice @tmp, 0, 2 ) {
684 my ($from, $to) = split /\s*->\s*/, $transition, 2;
685 push @res, { %$info, from => $from, to => $to };
687 $lifecycle->{'actions'} = \@res;