X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FAction%2FCreateTickets.pm;h=e3c7b53e01ad9b8de4c7972e9e74b7601d2006c3;hp=40d18d357a7e1ebedbc0524d2414ceff5df3a582;hb=e9e0cf0989259b94d9758eceff448666a2e5a5cc;hpb=40a7b3dc653e099f7bd0bd762b649b04c4432db2 diff --git a/rt/lib/RT/Action/CreateTickets.pm b/rt/lib/RT/Action/CreateTickets.pm index 40d18d357..e3c7b53e0 100644 --- a/rt/lib/RT/Action/CreateTickets.pm +++ b/rt/lib/RT/Action/CreateTickets.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC +# +# # (Except where explicitly superseded by other copyright notices) -# -# +# +# # LICENSE: -# +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# +# +# # CONTRIBUTION SUBMISSION POLICY: -# +# # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) -# +# # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that @@ -43,28 +43,24 @@ # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. -# +# # END BPS TAGGED BLOCK }}} + package RT::Action::CreateTickets; -require RT::Action::Generic; +use base 'RT::Action'; use strict; use warnings; -use vars qw/@ISA/; -@ISA = qw(RT::Action::Generic); use MIME::Entity; =head1 NAME - RT::Action::CreateTickets - -Create one or more tickets according to an externally supplied template. - +RT::Action::CreateTickets - Create one or more tickets according to an externally supplied template =head1 SYNOPSIS - ===Create-Ticket codereview + ===Create-Ticket: codereview Subject: Code review for {$Tickets{'TOP'}->Subject} Depended-On-By: TOP Content: Someone has created a ticket. you should review and approve it, @@ -73,18 +69,14 @@ Create one or more tickets according to an externally supplied template. =head1 DESCRIPTION +The CreateTickets ScripAction allows you to create automated workflows in RT, +creating new tickets in response to actions and conditions from other +tickets. -Using the "CreateTickets" ScripAction and mandatory dependencies, RT now has -the ability to model complex workflow. When a ticket is created in a queue -that has a "CreateTickets" scripaction, that ScripAction parses its "Template" - - - -=head2 FORMAT - -CreateTickets uses the template as a template for an ordered set of tickets -to create. The basic format is as follows: +=head2 Format +CreateTickets uses the RT template configured in the scrip as a template +for an ordered set of tickets to create. The basic format is as follows: ===Create-Ticket: identifier Param: Value @@ -99,17 +91,24 @@ to create. The basic format is as follows: Content: Blah ENDOFCONTENT - -Each ===Create-Ticket: section is evaluated as its own -Text::Template object, which means that you can embed snippets -of perl inside the Text::Template using {} delimiters, but that -such sections absolutely can not span a ===Create-Ticket boundary. - -After each ticket is created, it's stuffed into a hash called %Tickets -so as to be available during the creation of other tickets during the same -ScripAction. The hash is prepopulated with the ticket which triggered the -ScripAction as $Tickets{'TOP'}; you can also access that ticket using the -shorthand TOP. +As shown, you can put one or more C<===Create-Ticket:> sections in +a template. Each C<===Create-Ticket:> section is evaluated as its own +L object, which means that you can embed snippets +of Perl inside the L using C<{}> delimiters, but that +such sections absolutely can not span a C<===Create-Ticket:> boundary. + +Note that each C must come right after the C on the same +line. The C param can extend over multiple lines, but the text +of the first line must start right after C. Don't try to start +your C section with a newline. + +After each ticket is created, it's stuffed into a hash called C<%Tickets> +making it available during the creation of other tickets during the +same ScripAction. The hash key for each ticket is C, +where C<[identifier]> is the value you put after C<===Create-Ticket:>. The hash +is prepopulated with the ticket which triggered the ScripAction as +C<$Tickets{'TOP'}>. You can also access that ticket using the shorthand +C. A simple example: @@ -120,23 +119,21 @@ A simple example: so they can finish their work ENDOFCONTENT - - -A convoluted example +A convoluted example: ===Create-Ticket: approval { # Find out who the administrators of the group called "HR" # of which the creator of this ticket is a member my $name = "HR"; - - my $groups = RT::Groups->new($RT::SystemUser); + + my $groups = RT::Groups->new(RT->SystemUser); $groups->LimitToUserDefinedGroups(); $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name"); $groups->WithMember($TransactionObj->CreatorObj->Id); - + my $groupid = $groups->First->Id; - - my $adminccs = RT::Users->new($RT::SystemUser); + + my $adminccs = RT::Users->new(RT->SystemUser); $adminccs->WhoHaveRight( Right => "AdminGroup", Object =>$groups->First, @@ -144,10 +141,10 @@ A convoluted example IncludeSuperusers => 0, IncludeSubgroupMembers => 0, ); - - my @admins; + + our @admins; while (my $admin = $adminccs->Next) { - push (@admins, $admin->EmailAddress); + push (@admins, $admin->EmailAddress); } } Queue: ___Approvals @@ -164,51 +161,56 @@ A convoluted example ENDOFCONTENT ===Create-Ticket: two Subject: Manager approval + Type: approval Depended-On-By: TOP - Refers-On: {$Tickets{"approval"}->Id} + Refers-To: {$Tickets{"create-approval"}->Id} Queue: ___Approvals Content-Type: text/plain - Content: - Your approval is requred for this ticket, too. + Content: Your approval is requred for this ticket, too. ENDOFCONTENT - -=head2 Acceptable fields -A complete list of acceptable fields for this beastie: +As shown above, you can include a block with Perl code to set up some +values for the new tickets. If you want to access a variable in the +template section after the block, you must scope it with C rather +than C. Just as with other RT templates, you can also include +Perl code in the template sections using C<{}>. + +=head2 Acceptable Fields +A complete list of acceptable fields: * Queue => Name or id# of a queue Subject => A text string - ! Status => A valid status. defaults to 'new' + ! Status => A valid status. Defaults to 'new' Due => Dates can be specified in seconds since the epoch to be handled literally or in a semi-free textual format which RT will attempt to parse. - - - - Starts => - Started => - Resolved => - Owner => Username or id of an RT user who can and should own + Starts => + Started => + Resolved => + Owner => Username or id of an RT user who can and should own this ticket; forces the owner if necessary + Requestor => Email address - + Cc => Email address - + AdminCc => Email address - TimeWorked => - TimeEstimated => - TimeLeft => - InitialPriority => - FinalPriority => - Type => - +! DependsOn => + + Cc => Email address + + AdminCc => Email address + + RequestorGroup => Group name + + CcGroup => Group name + + AdminCcGroup => Group name + TimeWorked => + TimeEstimated => + TimeLeft => + InitialPriority => + FinalPriority => + Type => + +! DependsOn => +! DependedOnBy => +! RefersTo => - +! ReferredToBy => + +! ReferredToBy => +! Members => - +! MemberOf => - Content => content. Can extend to multiple lines. Everything + +! MemberOf => + Content => Content. Can extend to multiple lines. Everything within a template after a Content: header is treated - as content until we hit a line containing only + as content until we hit a line containing only ENDOFCONTENT ContentType => the content-type of the Content field. Defaults to 'text/plain' @@ -220,261 +222,22 @@ A complete list of acceptable fields for this beastie: CF-name => custom field value CustomField-name => custom field value -Fields marked with an * are required. +Fields marked with an C<*> are required. -Fields marked with a + may have multiple values, simply +Fields marked with a C<+> may have multiple values, simply by repeating the fieldname on a new line with an additional value. -Fields marked with a ! are postponed to be processed after all -tickets in the same actions are created. Except for 'Status', those -field can also take a ticket name within the same action (i.e. -the identifiers after ==Create-Ticket), instead of raw Ticket ID +Fields marked with a C have processing postponed until after all +tickets in the same actions are created. Except for C, those +fields can also take a ticket name within the same action (i.e. +the identifiers after C<===Create-Ticket:>), instead of raw ticket ID numbers. -When parsed, field names are converted to lowercase and have -s stripped. -Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all -be treated as the same thing. - - -=begin testing - -ok (require RT::Action::CreateTickets); -use_ok(RT::Scrip); -use_ok(RT::Template); -use_ok(RT::ScripAction); -use_ok(RT::ScripCondition); -use_ok(RT::Ticket); - -my $approvalsq = RT::Queue->new($RT::SystemUser); -$approvalsq->Create(Name => 'Approvals'); -ok ($approvalsq->Id, "Created Approvals test queue"); - - -my $approvals = -'===Create-Ticket: approval -Queue: ___Approvals -Type: approval -AdminCc: {join ("\nAdminCc: ",@admins) } -Depended-On-By: {$Tickets{"TOP"}->Id} -Refers-To: TOP -Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject} -Due: {time + 86400} -Content-Type: text/plain -Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject} -Blah -Blah -ENDOFCONTENT -===Create-Ticket: two -Subject: Manager approval. -Depended-On-By: approval -Queue: ___Approvals -Content-Type: text/plain -Content: -Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that? -ENDOFCONTENT -'; - -ok ($approvals =~ /Content/, "Read in the approvals template"); - -my $apptemp = RT::Template->new($RT::SystemUser); -$apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0"); - -ok ($apptemp->Id); - -my $q = RT::Queue->new($RT::SystemUser); -$q->Create(Name => 'WorkflowTest'); -ok ($q->Id, "Created workflow test queue"); - -my $scrip = RT::Scrip->new($RT::SystemUser); -my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction', - ScripAction => 'Create Tickets', - Template => 'Approvals', - Queue => $q->Id); -ok ($sval, $smsg); -ok ($scrip->Id, "Created the scrip"); -ok ($scrip->TemplateObj->Id, "Created the scrip template"); -ok ($scrip->ConditionObj->Id, "Created the scrip condition"); -ok ($scrip->ActionObj->Id, "Created the scrip action"); - -my $t = RT::Ticket->new($RT::SystemUser); -my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test", - Owner => "root", - Queue => $q->Id); - -ok ($tid,$tmsg); - -my $deps = $t->DependsOn; -is ($deps->Count, 1, "The ticket we created depends on one other ticket"); -my $dependson= $deps->First->TargetObj; -ok ($dependson->Id, "It depends on a real ticket"); -unlike ($dependson->Subject, qr/{/, "The subject doesn't have braces in it. that means we're interpreting expressions"); -is ($t->ReferredToBy->Count,1, "It's only referred to by one other ticket"); -is ($t->ReferredToBy->First->BaseObj->Id,$t->DependsOn->First->TargetObj->Id, "The same ticket that depends on it refers to it."); -use RT::Action::CreateTickets; -my $action = RT::Action::CreateTickets->new( CurrentUser => $RT::SystemUser);; - -# comma-delimited templates -my $commas = <<"EOF"; -id,Queue,Subject,Owner,Content -ticket1,General,"foo, bar",root,blah -ticket2,General,foo bar,root,blah -ticket3,General,foo' bar,root,blah'boo -ticket4,General,foo' bar,,blah'boo -EOF - - -# Comma delimited templates with missing data -my $sparse_commas = <<"EOF"; -id,Queue,Subject,Owner,Requestor -ticket14,General,,,bobby -ticket15,General,,,tommy -ticket16,General,,suzie,tommy -ticket17,General,Foo "bar" baz,suzie,tommy -ticket18,General,'Foo "bar" baz',suzie,tommy -ticket19,General,'Foo bar' baz,suzie,tommy -EOF - - -# tab-delimited templates -my $tabs = <<"EOF"; -id\tQueue\tSubject\tOwner\tContent -ticket10\tGeneral\t"foo' bar"\troot\tblah' -ticket11\tGeneral\tfoo, bar\troot\tblah -ticket12\tGeneral\tfoo' bar\troot\tblah'boo -ticket13\tGeneral\tfoo' bar\t\tblah'boo -EOF - -my %expected; - -$expected{ticket1} = <Parse(Content =>$commas); -$action->Parse(Content =>$sparse_commas); -$action->Parse(Content => $tabs); - -my %got; -foreach (@{ $action->{'create_tickets'} }) { - $got{$_} = $action->{'templates'}->{$_}; -} - -foreach my $id ( sort keys %expected ) { - ok(exists($got{"create-$id"}), "template exists for $id"); - is($got{"create-$id"}, $expected{$id}, "template is correct for $id"); -} +When parsed, field names are converted to lowercase and have hyphens stripped. +C, C, C, C and C will +all be treated as the same thing. -=end testing - - -=head1 AUTHOR - -Jesse Vincent - -=head1 SEE ALSO - -perl(1). +=head1 METHODS =cut @@ -518,9 +281,7 @@ my %LINKTYPEMAP = ( ); -# {{{ Scrip methods (Commit, Prepare) -# {{{ sub Commit #Do what we need to do and send it out. sub Commit { my $self = shift; @@ -533,38 +294,44 @@ sub Commit { return (1); } -# }}} -# {{{ sub Prepare sub Prepare { my $self = shift; unless ( $self->TemplateObj ) { - $RT::Logger->warning("No template object handed to $self\n"); + $RT::Logger->warning("No template object handed to $self"); } unless ( $self->TransactionObj ) { - $RT::Logger->warning("No transaction object handed to $self\n"); + $RT::Logger->warning("No transaction object handed to $self"); } unless ( $self->TicketObj ) { - $RT::Logger->warning("No ticket object handed to $self\n"); + $RT::Logger->warning("No ticket object handed to $self"); } + my $active = 0; + if ( $self->TemplateObj->Type eq 'Perl' ) { + $active = 1; + } else { + RT->Logger->info(sprintf( + "Template #%d is type %s. You most likely want to use a Perl template instead.", + $self->TemplateObj->id, $self->TemplateObj->Type + )); + } + $self->Parse( Content => $self->TemplateObj->Content, - _ActiveContent => 1 + _ActiveContent => $active, ); return 1; } -# }}} -# }}} sub CreateByTemplate { my $self = shift; @@ -575,12 +342,12 @@ sub CreateByTemplate { my @results; # XXX: cargo cult programming that works. i'll be back. - use bytes; local %T::Tickets = %T::Tickets; local $T::TOP = $T::TOP; local $T::ID = $T::ID; $T::Tickets{'TOP'} = $T::TOP = $top if $top; + local $T::TransactionObj = $self->TransactionObj; my $ticketargs; my ( @links, @postponed ); @@ -637,7 +404,6 @@ sub UpdateByTemplate { my $top = shift; # XXX: cargo cult programming that works. i'll be back. - use bytes; my @results; local %T::Tickets = %T::Tickets; @@ -759,12 +525,16 @@ sub UpdateByTemplate { return @results; } -=head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE +=head2 Parse -Parse a template from TEMPLATE_CONTENT +Takes (in order) template content, a default queue, a default requestor, and +active (a boolean flag). -If $active is set to true, then we'll use Text::Template to parse the templates, -allowing you to embed active perl in your templates. +Parses a template in the template content, defaulting queue and requestor if +unspecified in the template to the values provided as arguments. + +If the active flag is true, then we'll use L to parse the +templates, allowing you to embed active Perl in your templates. =cut @@ -789,7 +559,8 @@ sub Parse { $self->_ParseMultilineTemplate(%args); } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) { $self->_ParseXSVTemplate(%args); - + } else { + RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}"); } } @@ -797,9 +568,9 @@ sub Parse { Parses mulitline templates. Things like: - ===Create-Ticket ... + ===Create-Ticket: ... -Takes the same arguments as Parse +Takes the same arguments as L. =cut @@ -808,11 +579,15 @@ sub _ParseMultilineTemplate { my %args = (@_); my $template_id; + require Encode; + require utf8; my ( $queue, $requestor ); $RT::Logger->debug("Line: ==="); foreach my $line ( split( /\n/, $args{'Content'} ) ) { $line =~ s/\r$//; - $RT::Logger->debug("Line: $line"); + $RT::Logger->debug( "Line: " . utf8::is_utf8($line) + ? Encode::encode_utf8($line) + : $line ); if ( $line =~ /^===/ ) { if ( $template_id && !$queue && $args{'Queue'} ) { $self->{'templates'}->{$template_id} @@ -894,7 +669,7 @@ sub ParseLines { } ); - $RT::Logger->debug("Workflow: yielding\n$content"); + $RT::Logger->debug("Workflow: yielding $content"); if ($err) { $RT::Logger->error( "Ticket creation failed: " . $err ); @@ -945,14 +720,18 @@ sub ParseLines { $args{$tag} =~ s/^\s+//g; $args{$tag} =~ s/\s+$//g; } - if (($tag =~ /^(requestor|cc|admincc)$/i or grep {lc $_ eq $tag} keys %LINKTYPEMAP) and $args{$tag} =~ /,/) { + if ( + ($tag =~ /^(requestor|cc|admincc)(group)?$/i + or grep {lc $_ eq $tag} keys %LINKTYPEMAP) + and $args{$tag} =~ /,/ + ) { $args{$tag} = [ split /,\s*/, $args{$tag} ]; } } } } - foreach my $date qw(due starts started resolved) { + foreach my $date (qw(due starts started resolved)) { my $dateobj = RT::Date->new( $self->CurrentUser ); next unless $args{$date}; if ( $args{$date} =~ /^\d+$/ ) { @@ -968,6 +747,21 @@ sub ParseLines { $args{$date} = $dateobj->ISO; } + foreach my $role (qw(requestor cc admincc)) { + next unless my $value = $args{ $role . 'group' }; + + my $group = RT::Group->new( $self->CurrentUser ); + $group->LoadUserDefinedGroup( $value ); + unless ( $group->id ) { + $RT::Logger->error("Couldn't load group '$value'"); + next; + } + + $args{ $role } = $args{ $role } ? [$args{ $role }] : [] + unless ref $args{ $role }; + push @{ $args{ $role } }, $group->PrincipalObj->id; + } + $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses if $self->TicketObj; @@ -990,7 +784,9 @@ sub ParseLines { TimeLeft => $args{'timeleft'}, InitialPriority => $args{'initialpriority'} || 0, FinalPriority => $args{'finalpriority'} || 0, + SquelchMailTo => $args{'squelchmailto'}, Type => $args{'type'}, + $self->Rules ); if ( $args{content} ) { @@ -1008,14 +804,17 @@ sub ParseLines { my $orig_tag = $original_tags{$tag} or next; if ( $orig_tag =~ /^customfield-?(\d+)$/i ) { $ticketargs{ "CustomField-" . $1 } = $args{$tag}; - } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.*)$/i ) { + } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) { my $cf = RT::CustomField->new( $self->CurrentUser ); $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} ); + $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id; + next unless $cf->id; $ticketargs{ "CustomField-" . $cf->id } = $args{$tag}; } elsif ($orig_tag) { my $cf = RT::CustomField->new( $self->CurrentUser ); $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} ); - next unless ($cf->id) ; + $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id; + next unless $cf->id; $ticketargs{ "CustomField-" . $cf->id } = $args{$tag}; } @@ -1027,9 +826,10 @@ sub ParseLines { } -=head2 _ParseXSVTemplate +=head2 _ParseXSVTemplate -Parses a tab or comma delimited template. Should only ever be called by Parse +Parses a tab or comma delimited template. Should only ever be called by +L. =cut @@ -1223,7 +1023,7 @@ sub GetUpdateTemplate { my $mode = $LINKTYPEMAP{$type}->{Mode}; my $method = $LINKTYPEMAP{$type}->{Type}; - my $links; + my $links = ''; while ( my $link = $t->$method->Next ) { $links .= ", " if $links; @@ -1308,7 +1108,7 @@ sub UpdateWatchers { my @results; - foreach my $type qw(Requestor Cc AdminCc) { + foreach my $type (qw(Requestor Cc AdminCc)) { my $method = $type . 'Addresses'; my $oldaddr = $ticket->$method; @@ -1375,6 +1175,7 @@ sub UpdateCustomFields { my $cf = $1; my $CustomFieldObj = RT::CustomField->new($self->CurrentUser); + $CustomFieldObj->SetContextObject( $ticket ); $CustomFieldObj->LoadById($cf); my @values; @@ -1467,10 +1268,25 @@ sub PostProcess { } -eval "require RT::Action::CreateTickets_Vendor"; -die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Vendor.pm} ); -eval "require RT::Action::CreateTickets_Local"; -die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Local.pm} ); +sub Options { + my $self = shift; + my $queues = RT::Queues->new($self->CurrentUser); + $queues->UnLimit; + my @names; + while (my $queue = $queues->Next) { + push @names, $queue->Id, $queue->Name; + } + return ( + { + 'name' => 'Queue', + 'label' => 'In queue', + 'type' => 'select', + 'options' => \@names + } + ) +} + +RT::Base->_ImportOverlays(); 1;