X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;ds=sidebyside;f=rt%2Flib%2FRT%2FLifecycle.pm;h=c2865bcaf316dd6ff180278931895ada290a78fb;hb=f2731f7f3883905cd17633f486d2aeb9593173da;hp=056599edb04b9d9df7ec8ba534417299252c826c;hpb=a6fe07e49e3fc12169e801b1ed6874c3a5bd8500;p=freeside.git diff --git a/rt/lib/RT/Lifecycle.pm b/rt/lib/RT/Lifecycle.pm index 056599edb..c2865bcaf 100644 --- a/rt/lib/RT/Lifecycle.pm +++ b/rt/lib/RT/Lifecycle.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -54,16 +54,10 @@ package RT::Lifecycle; our %LIFECYCLES; our %LIFECYCLES_CACHE; -__PACKAGE__->RegisterRights; +our %LIFECYCLES_TYPES; # cache structure: # { -# '' => { # all valid statuses -# '' => [...], -# initial => [...], -# active => [...], -# inactive => [...], -# }, # lifecycle_x => { # '' => [...], # all valid in lifecycle # initial => [...], @@ -119,66 +113,111 @@ sub new { return $self; } -=head2 Load +=head2 Load Name => I, Type => I -Takes a name of the lifecycle and loads it. If name is empty or undefined then -loads the global lifecycle with statuses from all named lifecycles. +Takes a name of the lifecycle and loads it. If only a Type is provided, +loads the global lifecycle with statuses from all named lifecycles of +that type. Can be called as class method, returns a new object, for example: - my $lifecycle = RT::Lifecycle->Load('default'); + my $lifecycle = RT::Lifecycle->Load( Name => 'default'); + +Returns an object which may be a subclass of L +(L, for example) depending on the type of the +lifecycle in question. =cut sub Load { my $self = shift; - my $name = shift || ''; - return $self->new->Load( $name, @_ ) + return $self->new->Load( @_ ) unless ref $self; - return unless exists $LIFECYCLES_CACHE{ $name }; + unshift @_, Type => "ticket", "Name" + if @_ % 2; + + my %args = ( + Type => "ticket", + Name => '', + @_, + ); + + if (defined $args{Name} and exists $LIFECYCLES_CACHE{ $args{Name} }) { + $self->{'name'} = $args{Name}; + $self->{'data'} = $LIFECYCLES_CACHE{ $args{Name} }; + $self->{'type'} = $args{Type}; + + my $found_type = $self->{'data'}{'type'}; + warn "Found type of $found_type ne $args{Type}" if $found_type ne $args{Type}; + } elsif (not $args{Name} and exists $LIFECYCLES_TYPES{ $args{Type} }) { + $self->{'data'} = $LIFECYCLES_TYPES{ $args{Type} }; + $self->{'type'} = $args{Type}; + } else { + return undef; + } - $self->{'name'} = $name; - $self->{'data'} = $LIFECYCLES_CACHE{ $name }; + my $class = "RT::Lifecycle::".ucfirst($args{Type}); + bless $self, $class if $class->require; return $self; } =head2 List -Returns sorted list of the lifecycles' names. +List available lifecycles. This list omits RT's default approvals +lifecycle. + +Takes: An optional parameter for lifecycle types other than tickets. + Defaults to 'ticket'. + +Returns: A sorted list of available lifecycles. =cut sub List { my $self = shift; + my $for = shift || 'ticket'; + + return grep { $_ ne 'approvals' } $self->ListAll( $for ); +} + +=head2 ListAll + +Returns a list of all lifecycles, including approvals. + +Takes: An optional parameter for lifecycle types other than tickets. + Defaults to 'ticket'. + +Returns: A sorted list of all available lifecycles. + +=cut + +sub ListAll { + my $self = shift; + my $for = shift || 'ticket'; $self->FillCache unless keys %LIFECYCLES_CACHE; - return sort grep length && $_ ne '__maps__', keys %LIFECYCLES_CACHE; + return sort grep {$LIFECYCLES_CACHE{$_}{type} eq $for} + grep $_ ne '__maps__', keys %LIFECYCLES_CACHE; } =head2 Name -Returns name of the laoded lifecycle. +Returns name of the loaded lifecycle. =cut sub Name { return $_[0]->{'name'} } -=head2 Queues +=head2 Type -Returns L collection with queues that use this lifecycle. +Returns the type of the loaded lifecycle. =cut -sub Queues { - my $self = shift; - require RT::Queues; - my $queues = RT::Queues->new( RT->SystemUser ); - $queues->Limit( FIELD => 'Lifecycle', VALUE => $self->Name ); - return $queues; -} +sub Type { return $_[0]->{'type'} } =head2 Getting statuses and validating. @@ -298,7 +337,7 @@ sub IsActive { return 0; } -=head3 inactive +=head3 Inactive Returns an array of all inactive statuses for this lifecycle. @@ -309,7 +348,7 @@ sub Inactive { return $self->Valid('inactive'); } -=head3 is_inactive +=head3 IsInactive Takes a value and returns true if value is a valid inactive status. Otherwise, returns false. @@ -354,41 +393,6 @@ sub DefaultOnCreate { return $self->DefaultStatus('on_create'); } - -=head3 DefaultOnMerge - -Returns the status that should be used when tickets -are merged. - -=cut - -sub DefaultOnMerge { - my $self = shift; - return $self->DefaultStatus('on_merge'); -} - -=head3 ReminderStatusOnOpen - -Returns the status that should be used when reminders are opened. - -=cut - -sub ReminderStatusOnOpen { - my $self = shift; - return $self->DefaultStatus('reminder_on_open') || 'open'; -} - -=head3 ReminderStatusOnResolve - -Returns the status that should be used when reminders are resolved. - -=cut - -sub ReminderStatusOnResolve { - my $self = shift; - return $self->DefaultStatus('reminder_on_resolve') || 'resolved'; -} - =head2 Transitions, rights, labels and actions. =head3 Transitions @@ -411,8 +415,8 @@ sub Transitions { return %{ $self->{'data'}{'transitions'} || {} } unless @_; - my $status = shift; - return @{ $self->{'data'}{'transitions'}{ $status || '' } || [] }; + my $status = shift || ''; + return @{ $self->{'data'}{'transitions'}{ lc $status } || [] }; } =head1 IsTransition @@ -439,8 +443,8 @@ be checked on the ticket. sub CheckRight { my $self = shift; - my $from = shift; - my $to = shift; + my $from = lc shift; + my $to = lc shift; if ( my $rights = $self->{'data'}{'rights'} ) { my $check = $rights->{ $from .' -> '. $to } @@ -452,33 +456,7 @@ sub CheckRight { return $to eq 'deleted' ? 'DeleteTicket' : 'ModifyTicket'; } -=head3 RegisterRights - -Registers all defined rights in the system, so they can be addigned -to users. No need to call it, as it's called when module is loaded. - -=cut - -sub RegisterRights { - my $self = shift; - - my %rights = $self->RightsDescription; - - require RT::ACE; - - require RT::Queue; - my $RIGHTS = $RT::Queue::RIGHTS; - - while ( my ($right, $description) = each %rights ) { - next if exists $RIGHTS->{ $right }; - - $RIGHTS->{ $right } = $description; - RT::Queue->AddRightCategories( $right => 'Status' ); - $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right; - } -} - -=head3 RightsDescription +=head3 RightsDescription [TYPE] Returns hash with description of rights that are defined for particular transitions. @@ -487,12 +465,14 @@ particular transitions. sub RightsDescription { my $self = shift; + my $type = shift; $self->FillCache unless keys %LIFECYCLES_CACHE; my %tmp; foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { next unless exists $lifecycle->{'rights'}; + next if $type and $lifecycle->{type} ne $type; while ( my ($transition, $right) = each %{ $lifecycle->{'rights'} } ) { push @{ $tmp{ $right } ||=[] }, $transition; } @@ -536,10 +516,11 @@ pairs: sub Actions { my $self = shift; my $from = shift || return (); + $from = lc $from; $self->FillCache unless keys %LIFECYCLES_CACHE; - my @res = grep $_->{'from'} eq $from || ( $_->{'from'} eq '*' && $_->{'to'} ne $from ), + my @res = grep lc $_->{'from'} eq $from || ( $_->{'from'} eq '*' && lc $_->{'to'} ne $from ), @{ $self->{'data'}{'actions'} }; # skip '* -> x' if there is '$from -> x' @@ -561,7 +542,7 @@ move map from this cycle to provided. sub MoveMap { my $from = shift; # self my $to = shift; - $to = RT::Lifecycle->Load( $to ) unless ref $to; + $to = RT::Lifecycle->Load( Name => $to, Type => $from->Type ) unless ref $to; return $LIFECYCLES{'__maps__'}{ $from->Name .' -> '. $to->Name } || {}; } @@ -589,13 +570,14 @@ move maps. sub NoMoveMaps { my $self = shift; - my @list = $self->List; + my $type = $self->Type; + my @list = $self->List( $type ); my @res; foreach my $from ( @list ) { foreach my $to ( @list ) { next if $from eq $to; push @res, $from, $to - unless RT::Lifecycle->Load( $from )->HasMoveMap( $to ); + unless RT::Lifecycle->Load( Name => $from, Type => $type )->HasMoveMap( $to ); } } return @res; @@ -616,7 +598,7 @@ sub ForLocalization { my @res = (); - push @res, @{ $LIFECYCLES_CACHE{''}{''} || [] }; + push @res, @{$_->{''}} for values %LIFECYCLES_TYPES; foreach my $lifecycle ( values %LIFECYCLES ) { push @res, grep defined && length, @@ -633,59 +615,179 @@ sub ForLocalization { sub loc { return RT->SystemUser->loc( @_ ) } +sub CanonicalCase { + my $self = shift; + my ($status) = @_; + return undef unless defined $status; + return($self->{data}{canonical_case}{lc $status} || lc $status); +} + sub FillCache { my $self = shift; my $map = RT->Config->Get('Lifecycles') or return; + { + my @lifecycles; + + # if users are upgrading from 3.* where we don't have lifecycle column yet, + # this could die. we also don't want to frighten them by the errors out + eval { + local $RT::Logger = Log::Dispatch->new; + @lifecycles = grep { defined } RT::Queues->new( RT->SystemUser )->DistinctFieldValues( 'Lifecycle' ); + }; + unless ( $@ ) { + for my $name ( @lifecycles ) { + unless ( $map->{$name} ) { + warn "Lifecycle $name is missing in %Lifecycles config"; + } + } + } + } + %LIFECYCLES_CACHE = %LIFECYCLES = %$map; $_ = { %$_ } foreach values %LIFECYCLES_CACHE; - my %all = ( - '' => [], - initial => [], - active => [], - inactive => [], - ); - foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { - my @res; - foreach my $type ( qw(initial active inactive) ) { - push @{ $all{ $type } }, @{ $lifecycle->{ $type } || [] }; - push @res, @{ $lifecycle->{ $type } || [] }; + foreach my $name ( keys %LIFECYCLES_CACHE ) { + next if $name eq "__maps__"; + my $lifecycle = $LIFECYCLES_CACHE{$name}; + + my $type = $lifecycle->{type} ||= 'ticket'; + $LIFECYCLES_TYPES{$type} ||= { + '' => [], + initial => [], + active => [], + inactive => [], + actions => [], + }; + + my @statuses; + $lifecycle->{canonical_case} = {}; + foreach my $category ( qw(initial active inactive) ) { + for my $status (@{ $lifecycle->{ $category } || [] }) { + if (exists $lifecycle->{canonical_case}{lc $status}) { + warn "Duplicate status @{[lc $status]} in lifecycle $name"; + } else { + $lifecycle->{canonical_case}{lc $status} = $status; + } + push @{ $LIFECYCLES_TYPES{$type}{$category} }, $status; + push @statuses, $status; + } + } + + # Lower-case for consistency + # ->{actions} are handled below + for my $state (keys %{ $lifecycle->{defaults} || {} }) { + my $status = $lifecycle->{defaults}{$state}; + warn "Nonexistant status @{[lc $status]} in default states in $name lifecycle" + unless $lifecycle->{canonical_case}{lc $status}; + $lifecycle->{defaults}{$state} = + $lifecycle->{canonical_case}{lc $status} || lc $status; + } + for my $from (keys %{ $lifecycle->{transitions} || {} }) { + warn "Nonexistant status @{[lc $from]} in transitions in $name lifecycle" + unless $from eq '' or $lifecycle->{canonical_case}{lc $from}; + for my $status ( @{delete($lifecycle->{transitions}{$from}) || []} ) { + warn "Nonexistant status @{[lc $status]} in transitions in $name lifecycle" + unless $lifecycle->{canonical_case}{lc $status}; + push @{ $lifecycle->{transitions}{lc $from} }, + $lifecycle->{canonical_case}{lc $status} || lc $status; + } + } + for my $schema (keys %{ $lifecycle->{rights} || {} }) { + my ($from, $to) = split /\s*->\s*/, $schema, 2; + unless ($from and $to) { + warn "Invalid right transition $schema in $name lifecycle"; + next; + } + warn "Nonexistant status @{[lc $from]} in right transition in $name lifecycle" + unless $from eq '*' or $lifecycle->{canonical_case}{lc $from}; + warn "Nonexistant status @{[lc $to]} in right transition in $name lifecycle" + unless $to eq '*' or $lifecycle->{canonical_case}{lc $to}; + + warn "Invalid right name ($lifecycle->{rights}{$schema}) in $name lifecycle; right names must be ASCII" + if $lifecycle->{rights}{$schema} =~ /\P{ASCII}/; + + warn "Invalid right name ($lifecycle->{rights}{$schema}) in $name lifecycle; right names must be <= 25 characters" + if length($lifecycle->{rights}{$schema}) > 25; + + $lifecycle->{rights}{lc($from) . " -> " .lc($to)} + = delete $lifecycle->{rights}{$schema}; } my %seen; - @res = grep !$seen{ lc $_ }++, @res; - $lifecycle->{''} = \@res; + @statuses = grep !$seen{ lc $_ }++, @statuses; + $lifecycle->{''} = \@statuses; unless ( $lifecycle->{'transitions'}{''} ) { - $lifecycle->{'transitions'}{''} = [ grep $_ ne 'deleted', @res ]; + $lifecycle->{'transitions'}{''} = [ grep lc $_ ne 'deleted', @statuses ]; } - } - foreach my $type ( qw(initial active inactive), '' ) { - my %seen; - @{ $all{ $type } } = grep !$seen{ lc $_ }++, @{ $all{ $type } }; - push @{ $all{''} }, @{ $all{ $type } } if $type; - } - $LIFECYCLES_CACHE{''} = \%all; - foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { - my @res; + my @actions; if ( ref $lifecycle->{'actions'} eq 'HASH' ) { foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) { - push @res, $k, $lifecycle->{'actions'}{ $k }; + push @actions, $k, $lifecycle->{'actions'}{ $k }; } } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) { - @res = @{ $lifecycle->{'actions'} }; + @actions = @{ $lifecycle->{'actions'} }; } - my @tmp = splice @res; - while ( my ($transition, $info) = splice @tmp, 0, 2 ) { + $lifecycle->{'actions'} = []; + while ( my ($transition, $info) = splice @actions, 0, 2 ) { my ($from, $to) = split /\s*->\s*/, $transition, 2; - push @res, { %$info, from => $from, to => $to }; + unless ($from and $to) { + warn "Invalid action status change $transition in $name lifecycle"; + next; + } + warn "Nonexistant status @{[lc $from]} in action in $name lifecycle" + unless $from eq '*' or $lifecycle->{canonical_case}{lc $from}; + warn "Nonexistant status @{[lc $to]} in action in $name lifecycle" + unless $to eq '*' or $lifecycle->{canonical_case}{lc $to}; + push @{ $lifecycle->{'actions'} }, + { %$info, + from => ($lifecycle->{canonical_case}{lc $from} || lc $from), + to => ($lifecycle->{canonical_case}{lc $to} || lc $to), }; + } + } + + # Lower-case the transition maps + for my $mapname (keys %{ $LIFECYCLES_CACHE{'__maps__'} || {} }) { + my ($from, $to) = split /\s*->\s*/, $mapname, 2; + unless ($from and $to) { + warn "Invalid lifecycle mapping $mapname"; + next; + } + warn "Nonexistant lifecycle $from in $mapname lifecycle map" + unless $LIFECYCLES_CACHE{$from}; + warn "Nonexistant lifecycle $to in $mapname lifecycle map" + unless $LIFECYCLES_CACHE{$to}; + my $map = delete $LIFECYCLES_CACHE{'__maps__'}{$mapname}; + $LIFECYCLES_CACHE{'__maps__'}{"$from -> $to"} = $map; + for my $status (keys %{ $map }) { + warn "Nonexistant status @{[lc $status]} in $from in $mapname lifecycle map" + if $LIFECYCLES_CACHE{$from} + and not $LIFECYCLES_CACHE{$from}{canonical_case}{lc $status}; + warn "Nonexistant status @{[lc $map->{$status}]} in $to in $mapname lifecycle map" + if $LIFECYCLES_CACHE{$to} + and not $LIFECYCLES_CACHE{$to}{canonical_case}{lc $map->{$status}}; + $map->{lc $status} = lc delete $map->{$status}; } - $lifecycle->{'actions'} = \@res; } + + for my $type (keys %LIFECYCLES_TYPES) { + for my $category ( qw(initial active inactive), '' ) { + my %seen; + @{ $LIFECYCLES_TYPES{$type}{$category} } = + grep !$seen{ lc $_ }++, @{ $LIFECYCLES_TYPES{$type}{$category} }; + push @{ $LIFECYCLES_TYPES{$type}{''} }, + @{ $LIFECYCLES_TYPES{$type}{$category} } if $category; + } + + my $class = "RT::Lifecycle::".ucfirst($type); + $class->RegisterRights if $class->require + and $class->can("RegisterRights"); + } + return; }