1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC
6 # <jesse@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/copyleft/gpl.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 }}}
48 package RT::Action::CreateTickets;
49 require RT::Action::Generic;
54 @ISA = qw(RT::Action::Generic);
60 RT::Action::CreateTickets
62 Create one or more tickets according to an externally supplied template.
67 ===Create-Ticket codereview
68 Subject: Code review for {$Tickets{'TOP'}->Subject}
70 Content: Someone has created a ticket. you should review and approve it,
71 so they can finish their work
77 Using the "CreateTickets" ScripAction and mandatory dependencies, RT now has
78 the ability to model complex workflow. When a ticket is created in a queue
79 that has a "CreateTickets" scripaction, that ScripAction parses its "Template"
85 CreateTickets uses the template as a template for an ordered set of tickets
86 to create. The basic format is as follows:
89 ===Create-Ticket: identifier
103 Each ===Create-Ticket: section is evaluated as its own
104 Text::Template object, which means that you can embed snippets
105 of perl inside the Text::Template using {} delimiters, but that
106 such sections absolutely can not span a ===Create-Ticket boundary.
108 After each ticket is created, it's stuffed into a hash called %Tickets
109 so as to be available during the creation of other tickets during the same
110 ScripAction. The hash is prepopulated with the ticket which triggered the
111 ScripAction as $Tickets{'TOP'}; you can also access that ticket using the
116 ===Create-Ticket: codereview
117 Subject: Code review for {$Tickets{'TOP'}->Subject}
119 Content: Someone has created a ticket. you should review and approve it,
120 so they can finish their work
127 ===Create-Ticket: approval
128 { # Find out who the administrators of the group called "HR"
129 # of which the creator of this ticket is a member
132 my $groups = RT::Groups->new($RT::SystemUser);
133 $groups->LimitToUserDefinedGroups();
134 $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name");
135 $groups->WithMember($TransactionObj->CreatorObj->Id);
137 my $groupid = $groups->First->Id;
139 my $adminccs = RT::Users->new($RT::SystemUser);
140 $adminccs->WhoHaveRight(
141 Right => "AdminGroup",
142 Object =>$groups->First,
143 IncludeSystemRights => undef,
144 IncludeSuperusers => 0,
145 IncludeSubgroupMembers => 0,
149 while (my $admin = $adminccs->Next) {
150 push (@admins, $admin->EmailAddress);
155 AdminCc: {join ("\nAdminCc: ",@admins) }
158 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
160 Content-Type: text/plain
161 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
165 ===Create-Ticket: two
166 Subject: Manager approval
168 Refers-On: {$Tickets{"approval"}->Id}
170 Content-Type: text/plain
172 Your approval is requred for this ticket, too.
175 =head2 Acceptable fields
177 A complete list of acceptable fields for this beastie:
180 * Queue => Name or id# of a queue
181 Subject => A text string
182 ! Status => A valid status. defaults to 'new'
183 Due => Dates can be specified in seconds since the epoch
184 to be handled literally or in a semi-free textual
185 format which RT will attempt to parse.
192 Owner => Username or id of an RT user who can and should own
193 this ticket; forces the owner if necessary
194 + Requestor => Email address
195 + Cc => Email address
196 + AdminCc => Email address
209 Content => content. Can extend to multiple lines. Everything
210 within a template after a Content: header is treated
211 as content until we hit a line containing only
213 ContentType => the content-type of the Content field. Defaults to
215 UpdateType => 'correspond' or 'comment'; used in conjunction with
216 'content' if this is an update. Defaults to
219 CustomField-<id#> => custom field value
220 CF-name => custom field value
221 CustomField-name => custom field value
223 Fields marked with an * are required.
225 Fields marked with a + may have multiple values, simply
226 by repeating the fieldname on a new line with an additional value.
228 Fields marked with a ! are postponed to be processed after all
229 tickets in the same actions are created. Except for 'Status', those
230 field can also take a ticket name within the same action (i.e.
231 the identifiers after ==Create-Ticket), instead of raw Ticket ID
234 When parsed, field names are converted to lowercase and have -s stripped.
235 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
236 be treated as the same thing.
241 ok (require RT::Action::CreateTickets);
243 use_ok(RT::Template);
244 use_ok(RT::ScripAction);
245 use_ok(RT::ScripCondition);
248 my $approvalsq = RT::Queue->new($RT::SystemUser);
249 $approvalsq->Create(Name => 'Approvals');
250 ok ($approvalsq->Id, "Created Approvals test queue");
254 '===Create-Ticket: approval
257 AdminCc: {join ("\nAdminCc: ",@admins) }
258 Depended-On-By: {$Tickets{"TOP"}->Id}
260 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
262 Content-Type: text/plain
263 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
267 ===Create-Ticket: two
268 Subject: Manager approval.
269 Depended-On-By: approval
271 Content-Type: text/plain
273 Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that?
277 ok ($approvals =~ /Content/, "Read in the approvals template");
279 my $apptemp = RT::Template->new($RT::SystemUser);
280 $apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0");
284 my $q = RT::Queue->new($RT::SystemUser);
285 $q->Create(Name => 'WorkflowTest');
286 ok ($q->Id, "Created workflow test queue");
288 my $scrip = RT::Scrip->new($RT::SystemUser);
289 my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction',
290 ScripAction => 'Create Tickets',
291 Template => 'Approvals',
294 ok ($scrip->Id, "Created the scrip");
295 ok ($scrip->TemplateObj->Id, "Created the scrip template");
296 ok ($scrip->ConditionObj->Id, "Created the scrip condition");
297 ok ($scrip->ActionObj->Id, "Created the scrip action");
299 my $t = RT::Ticket->new($RT::SystemUser);
300 my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test",
306 my $deps = $t->DependsOn;
307 is ($deps->Count, 1, "The ticket we created depends on one other ticket");
308 my $dependson= $deps->First->TargetObj;
309 ok ($dependson->Id, "It depends on a real ticket");
310 unlike ($dependson->Subject, qr/{/, "The subject doesn't have braces in it. that means we're interpreting expressions");
311 is ($t->ReferredToBy->Count,1, "It's only referred to by one other ticket");
312 is ($t->ReferredToBy->First->BaseObj->Id,$t->DependsOn->First->TargetObj->Id, "The same ticket that depends on it refers to it.");
313 use RT::Action::CreateTickets;
314 my $action = RT::Action::CreateTickets->new( CurrentUser => $RT::SystemUser);;
316 # comma-delimited templates
317 my $commas = <<"EOF";
318 id,Queue,Subject,Owner,Content
319 ticket1,General,"foo, bar",root,blah
320 ticket2,General,foo bar,root,blah
321 ticket3,General,foo' bar,root,blah'boo
322 ticket4,General,foo' bar,,blah'boo
326 # Comma delimited templates with missing data
327 my $sparse_commas = <<"EOF";
328 id,Queue,Subject,Owner,Requestor
329 ticket14,General,,,bobby
330 ticket15,General,,,tommy
331 ticket16,General,,suzie,tommy
332 ticket17,General,Foo "bar" baz,suzie,tommy
333 ticket18,General,'Foo "bar" baz',suzie,tommy
334 ticket19,General,'Foo bar' baz,suzie,tommy
338 # tab-delimited templates
340 id\tQueue\tSubject\tOwner\tContent
341 ticket10\tGeneral\t"foo' bar"\troot\tblah'
342 ticket11\tGeneral\tfoo, bar\troot\tblah
343 ticket12\tGeneral\tfoo' bar\troot\tblah'boo
344 ticket13\tGeneral\tfoo' bar\t\tblah'boo
349 $expected{ticket1} = <<EOF;
357 $expected{ticket2} = <<EOF;
365 $expected{ticket3} = <<EOF;
373 $expected{ticket4} = <<EOF;
381 $expected{ticket10} = <<EOF;
389 $expected{ticket11} = <<EOF;
397 $expected{ticket12} = <<EOF;
405 $expected{ticket13} = <<EOF;
414 $expected{'ticket14'} = <<EOF;
420 $expected{'ticket15'} = <<EOF;
426 $expected{'ticket16'} = <<EOF;
432 $expected{'ticket17'} = <<EOF;
434 Subject: Foo "bar" baz
438 $expected{'ticket18'} = <<EOF;
440 Subject: Foo "bar" baz
444 $expected{'ticket19'} = <<EOF;
446 Subject: 'Foo bar' baz
454 $action->Parse(Content =>$commas);
455 $action->Parse(Content =>$sparse_commas);
456 $action->Parse(Content => $tabs);
459 foreach (@{ $action->{'create_tickets'} }) {
460 $got{$_} = $action->{'templates'}->{$_};
463 foreach my $id ( sort keys %expected ) {
464 ok(exists($got{"create-$id"}), "template exists for $id");
465 is($got{"create-$id"}, $expected{$id}, "template is correct for $id");
473 Jesse Vincent <jesse@bestpractical.com>
521 # {{{ Scrip methods (Commit, Prepare)
524 #Do what we need to do and send it out.
528 # Create all the tickets we care about
529 return (1) unless $self->TicketObj->Type eq 'ticket';
531 $self->CreateByTemplate( $self->TicketObj );
532 $self->UpdateByTemplate( $self->TicketObj );
543 unless ( $self->TemplateObj ) {
544 $RT::Logger->warning("No template object handed to $self\n");
547 unless ( $self->TransactionObj ) {
548 $RT::Logger->warning("No transaction object handed to $self\n");
552 unless ( $self->TicketObj ) {
553 $RT::Logger->warning("No ticket object handed to $self\n");
558 Content => $self->TemplateObj->Content,
569 sub CreateByTemplate {
573 $RT::Logger->debug("In CreateByTemplate");
577 # XXX: cargo cult programming that works. i'll be back.
580 local %T::Tickets = %T::Tickets;
581 local $T::TOP = $T::TOP;
582 local $T::ID = $T::ID;
583 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
586 my ( @links, @postponed );
587 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
588 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
591 $T::ID = $template_id;
592 @T::AllID = @{ $self->{'create_tickets'} };
594 ( $T::Tickets{$template_id}, $ticketargs )
595 = $self->ParseLines( $template_id, \@links, \@postponed );
597 # Now we have a %args to work with.
598 # Make sure we have at least the minimum set of
599 # reasonable data and do our thang
601 my ( $id, $transid, $msg )
602 = $T::Tickets{$template_id}->Create(%$ticketargs);
604 foreach my $res ( split( '\n', $msg ) ) {
606 $T::Tickets{$template_id}
607 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
611 if ( $self->TicketObj ) {
612 $msg = "Couldn't create related ticket $template_id for "
613 . $self->TicketObj->Id . " "
616 $msg = "Couldn't create ticket $template_id " . $msg;
619 $RT::Logger->error($msg);
623 $RT::Logger->debug("Assigned $template_id with $id");
624 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
626 && $T::Tickets{$template_id}->can('SetOriginObj');
630 $self->PostProcess( \@links, \@postponed );
635 sub UpdateByTemplate {
639 # XXX: cargo cult programming that works. i'll be back.
643 local %T::Tickets = %T::Tickets;
644 local $T::ID = $T::ID;
647 my ( @links, @postponed );
648 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
649 $RT::Logger->debug("Update Workflow: processing $template_id");
651 $T::ID = $template_id;
652 @T::AllID = @{ $self->{'update_tickets'} };
654 ( $T::Tickets{$template_id}, $ticketargs )
655 = $self->ParseLines( $template_id, \@links, \@postponed );
657 # Now we have a %args to work with.
658 # Make sure we have at least the minimum set of
659 # reasonable data and do our thang
676 my $id = $template_id;
677 $id =~ s/update-(\d+).*/$1/;
678 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
681 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
682 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
686 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
688 $template_id =~ m/^update-(.*)/;
689 my $base_id = "base-$1";
690 my $base = $self->{'templates'}->{$base_id};
694 $current =~ s/\n+$//;
696 # If we have no base template, set what we can.
697 if ( $base ne $current ) {
699 "Could not update ticket "
700 . $T::Tickets{$template_id}->Id
701 . ": Ticket has changed";
705 push @results, $T::Tickets{$template_id}->Update(
706 AttributesRef => \@attribs,
707 ARGSRef => $ticketargs
710 if ( $ticketargs->{'Owner'} ) {
711 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
712 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
716 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
719 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
721 next unless $ticketargs->{'MIMEObj'};
722 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
723 my ( $Transaction, $Description, $Object )
724 = $T::Tickets{$template_id}->Comment(
725 BccMessageTo => $ticketargs->{'Bcc'},
726 MIMEObj => $ticketargs->{'MIMEObj'},
727 TimeTaken => $ticketargs->{'TimeWorked'}
730 $T::Tickets{$template_id}
731 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
734 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
735 my ( $Transaction, $Description, $Object )
736 = $T::Tickets{$template_id}->Correspond(
737 BccMessageTo => $ticketargs->{'Bcc'},
738 MIMEObj => $ticketargs->{'MIMEObj'},
739 TimeTaken => $ticketargs->{'TimeWorked'}
742 $T::Tickets{$template_id}
743 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
749 $T::Tickets{$template_id}->loc(
750 "Update type was neither correspondence nor comment.")
752 . $T::Tickets{$template_id}->loc("Update not recorded.")
757 $self->PostProcess( \@links, \@postponed );
762 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
764 Parse a template from TEMPLATE_CONTENT
766 If $active is set to true, then we'll use Text::Template to parse the templates,
767 allowing you to embed active perl in your templates.
777 _ActiveContent => undef,
781 if ( $args{'_ActiveContent'} ) {
782 $self->{'UsePerlTextTemplate'} = 1;
785 $self->{'UsePerlTextTemplate'} = 0;
788 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
789 $self->_ParseMultilineTemplate(%args);
790 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
791 $self->_ParseXSVTemplate(%args);
796 =head2 _ParseMultilineTemplate
798 Parses mulitline templates. Things like:
802 Takes the same arguments as Parse
806 sub _ParseMultilineTemplate {
811 my ( $queue, $requestor );
812 $RT::Logger->debug("Line: ===");
813 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
815 $RT::Logger->debug("Line: $line");
816 if ( $line =~ /^===/ ) {
817 if ( $template_id && !$queue && $args{'Queue'} ) {
818 $self->{'templates'}->{$template_id}
819 .= "Queue: $args{'Queue'}\n";
821 if ( $template_id && !$requestor && $args{'Requestor'} ) {
822 $self->{'templates'}->{$template_id}
823 .= "Requestor: $args{'Requestor'}\n";
828 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
829 $template_id = "create-$1";
830 $RT::Logger->debug("**** Create ticket: $template_id");
831 push @{ $self->{'create_tickets'} }, $template_id;
832 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
833 $template_id = "update-$1";
834 $RT::Logger->debug("**** Update ticket: $template_id");
835 push @{ $self->{'update_tickets'} }, $template_id;
836 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
837 $template_id = "base-$1";
838 $RT::Logger->debug("**** Base ticket: $template_id");
839 push @{ $self->{'base_tickets'} }, $template_id;
840 } elsif ( $line =~ /^===#.*$/ ) { # a comment
843 if ( $line =~ /^Queue:(.*)/i ) {
848 if ( !$value && $args{'Queue'} ) {
849 $value = $args{'Queue'};
850 $line = "Queue: $value";
853 if ( $line =~ /^Requestors?:(.*)/i ) {
858 if ( !$value && $args{'Requestor'} ) {
859 $value = $args{'Requestor'};
860 $line = "Requestor: $value";
863 $self->{'templates'}->{$template_id} .= $line . "\n";
866 if ( $template_id && !$queue && $args{'Queue'} ) {
867 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
873 my $template_id = shift;
875 my $postponed = shift;
877 my $content = $self->{'templates'}->{$template_id};
879 if ( $self->{'UsePerlTextTemplate'} ) {
882 "Workflow: evaluating\n$self->{templates}{$template_id}");
884 my $template = Text::Template->new(
890 $content = $template->fill_in(
893 $err = {@_}->{error};
897 $RT::Logger->debug("Workflow: yielding\n$content");
900 $RT::Logger->error( "Ticket creation failed: " . $err );
901 while ( my ( $k, $v ) = each %T::X ) {
903 "Eliminating $template_id from ${k}'s parents.");
904 delete $v->{$template_id};
910 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
914 my @lines = ( split( /\n/, $content ) );
915 while ( defined( my $line = shift @lines ) ) {
916 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
918 my $original_tag = $1;
919 my $tag = lc($original_tag);
921 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
923 $original_tags{$tag} = $original_tag;
925 if ( ref( $args{$tag} ) )
926 { #If it's an array, we want to push the value
927 push @{ $args{$tag} }, $value;
928 } elsif ( defined( $args{$tag} ) )
929 { #if we're about to get a second value, make it an array
930 $args{$tag} = [ $args{$tag}, $value ];
931 } else { #if there's nothing there, just set the value
932 $args{$tag} = $value;
935 if ( $tag =~ /^content$/i ) { #just build up the content
936 # convert it to an array
937 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
938 while ( defined( my $l = shift @lines ) ) {
939 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
940 push @{ $args{'content'} }, $l . "\n";
943 # if it's not content, strip leading and trailing spaces
945 $args{$tag} =~ s/^\s+//g;
946 $args{$tag} =~ s/\s+$//g;
948 if (($tag =~ /^(requestor|cc|admincc)$/i or grep {lc $_ eq $tag} keys %LINKTYPEMAP) and $args{$tag} =~ /,/) {
949 $args{$tag} = [ split /,\s*/, $args{$tag} ];
955 foreach my $date qw(due starts started resolved) {
956 my $dateobj = RT::Date->new( $self->CurrentUser );
957 next unless $args{$date};
958 if ( $args{$date} =~ /^\d+$/ ) {
959 $dateobj->Set( Format => 'unix', Value => $args{$date} );
962 $dateobj->Set( Format => 'iso', Value => $args{$date} );
964 if ($@ or $dateobj->Unix <= 0) {
965 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
968 $args{$date} = $dateobj->ISO;
971 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
974 $args{'type'} ||= 'ticket';
977 Queue => $args{'queue'},
978 Subject => $args{'subject'},
979 Status => $args{'status'} || 'new',
981 Starts => $args{'starts'},
982 Started => $args{'started'},
983 Resolved => $args{'resolved'},
984 Owner => $args{'owner'},
985 Requestor => $args{'requestor'},
987 AdminCc => $args{'admincc'},
988 TimeWorked => $args{'timeworked'},
989 TimeEstimated => $args{'timeestimated'},
990 TimeLeft => $args{'timeleft'},
991 InitialPriority => $args{'initialpriority'} || 0,
992 FinalPriority => $args{'finalpriority'} || 0,
993 Type => $args{'type'},
996 if ( $args{content} ) {
997 my $mimeobj = MIME::Entity->new();
999 Type => $args{'contenttype'} || 'text/plain',
1000 Data => $args{'content'}
1002 $ticketargs{MIMEObj} = $mimeobj;
1003 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
1006 foreach my $tag ( keys(%args) ) {
1007 # if the tag was added later, skip it
1008 my $orig_tag = $original_tags{$tag} or next;
1009 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
1010 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
1011 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.*)$/i ) {
1012 my $cf = RT::CustomField->new( $self->CurrentUser );
1013 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
1014 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
1015 } elsif ($orig_tag) {
1016 my $cf = RT::CustomField->new( $self->CurrentUser );
1017 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
1018 next unless ($cf->id) ;
1019 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
1024 $self->GetDeferred( \%args, $template_id, $links, $postponed );
1026 return $TicketObj, \%ticketargs;
1030 =head2 _ParseXSVTemplate
1032 Parses a tab or comma delimited template. Should only ever be called by Parse
1036 sub _ParseXSVTemplate {
1040 use Regexp::Common qw(delimited);
1041 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
1044 if ( $first =~ /\t/ ) {
1049 my @fields = split( /$delimiter/, $first );
1051 my $delimiter_re = qr[$delimiter];
1052 my $justquoted = qr[$RE{quoted}];
1054 # Used to generate automatic template ids
1059 $content =~ s/^(\s*\r?\n)+//;
1061 # Keep track of Queue and Requestor, so we can provide defaults
1065 # The template for this line
1068 # What column we're on
1071 # If the last iteration was the end of the line
1078 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
1081 # Strip off quotes, if they exist
1083 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
1084 substr( $value, 0, 1 ) = "";
1085 substr( $value, -1, 1 ) = "";
1088 # What column is this?
1089 my $field = $fields[$i++];
1090 next COLUMN unless $field =~ /\S/;
1094 if ( $field =~ /^id$/i ) {
1095 # Special case if this is the ID column
1096 if ( $value =~ /^\d+$/ ) {
1097 $template_id = 'update-' . $value;
1098 push @{ $self->{'update_tickets'} }, $template_id;
1099 } elsif ( $value =~ /^#base-(\d+)$/ ) {
1100 $template_id = 'base-' . $1;
1101 push @{ $self->{'base_tickets'} }, $template_id;
1102 } elsif ( $value =~ /\S/ ) {
1103 $template_id = 'create-' . $value;
1104 push @{ $self->{'create_tickets'} }, $template_id;
1108 if ( $field =~ /^Body$/i
1109 || $field =~ /^Data$/i
1110 || $field =~ /^Message$/i )
1113 } elsif ( $field =~ /^Summary$/i ) {
1115 } elsif ( $field =~ /^Queue$/i ) {
1116 # Note that we found a queue
1118 $value ||= $args{'Queue'};
1119 } elsif ( $field =~ /^Requestors?$/i ) {
1120 $field = 'Requestor'; # Remove plural
1121 # Note that we found a requestor
1123 $value ||= $args{'Requestor'};
1126 # Tack onto the end of the template
1127 $template .= $field . ": ";
1128 $template .= (defined $value ? $value : "");
1130 $template .= "ENDOFCONTENT\n"
1131 if $field =~ /^Content$/i;
1135 # Ignore blank lines
1136 next unless $template;
1138 # If we didn't find a queue of requestor, tack on the defaults
1139 if ( !$queue && $args{'Queue'} ) {
1140 $template .= "Queue: $args{'Queue'}\n";
1142 if ( !$requestor && $args{'Requestor'} ) {
1143 $template .= "Requestor: $args{'Requestor'}\n";
1146 # If we never found an ID, come up with one
1147 unless ($template_id) {
1148 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
1149 $template_id = "create-auto-$autoid";
1150 # Also, it's a ticket to create
1151 push @{ $self->{'create_tickets'} }, $template_id;
1154 # Save the template we generated
1155 $self->{'templates'}->{$template_id} = $template;
1165 my $postponed = shift;
1167 # Deferred processing
1171 { DependsOn => $args->{'dependson'},
1172 DependedOnBy => $args->{'dependedonby'},
1173 RefersTo => $args->{'refersto'},
1174 ReferredToBy => $args->{'referredtoby'},
1175 Children => $args->{'children'},
1176 Parents => $args->{'parents'},
1182 # Status is postponed so we don't violate dependencies
1183 $id, { Status => $args->{'status'}, }
1187 sub GetUpdateTemplate {
1192 $string .= "Queue: " . $t->QueueObj->Name . "\n";
1193 $string .= "Subject: " . $t->Subject . "\n";
1194 $string .= "Status: " . $t->Status . "\n";
1195 $string .= "UpdateType: correspond\n";
1196 $string .= "Content: \n";
1197 $string .= "ENDOFCONTENT\n";
1198 $string .= "Due: " . $t->DueObj->AsString . "\n";
1199 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1200 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1201 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1202 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
1203 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1204 $string .= "Cc: " . $t->CcAddresses . "\n";
1205 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1206 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1207 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1208 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1209 $string .= "InitialPriority: " . $t->Priority . "\n";
1210 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1212 foreach my $type ( sort keys %LINKTYPEMAP ) {
1214 # don't display duplicates
1215 if ( $type eq "HasMember"
1216 || $type eq "Members"
1217 || $type eq "MemberOf" )
1221 $string .= "$type: ";
1223 my $mode = $LINKTYPEMAP{$type}->{Mode};
1224 my $method = $LINKTYPEMAP{$type}->{Type};
1227 while ( my $link = $t->$method->Next ) {
1228 $links .= ", " if $links;
1230 my $object = $mode . "Obj";
1231 my $member = $link->$object;
1232 $links .= $member->Id if $member;
1241 sub GetBaseTemplate {
1246 $string .= "Queue: " . $t->Queue . "\n";
1247 $string .= "Subject: " . $t->Subject . "\n";
1248 $string .= "Status: " . $t->Status . "\n";
1249 $string .= "Due: " . $t->DueObj->Unix . "\n";
1250 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1251 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1252 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1253 $string .= "Owner: " . $t->Owner . "\n";
1254 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1255 $string .= "Cc: " . $t->CcAddresses . "\n";
1256 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1257 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1258 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1259 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1260 $string .= "InitialPriority: " . $t->Priority . "\n";
1261 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1266 sub GetCreateTemplate {
1271 $string .= "Queue: General\n";
1272 $string .= "Subject: \n";
1273 $string .= "Status: new\n";
1274 $string .= "Content: \n";
1275 $string .= "ENDOFCONTENT\n";
1276 $string .= "Due: \n";
1277 $string .= "Starts: \n";
1278 $string .= "Started: \n";
1279 $string .= "Resolved: \n";
1280 $string .= "Owner: \n";
1281 $string .= "Requestor: \n";
1282 $string .= "Cc: \n";
1283 $string .= "AdminCc:\n";
1284 $string .= "TimeWorked: \n";
1285 $string .= "TimeEstimated: \n";
1286 $string .= "TimeLeft: \n";
1287 $string .= "InitialPriority: \n";
1288 $string .= "FinalPriority: \n";
1290 foreach my $type ( keys %LINKTYPEMAP ) {
1292 # don't display duplicates
1293 if ( $type eq "HasMember"
1294 || $type eq 'Members'
1295 || $type eq 'MemberOf' )
1299 $string .= "$type: \n";
1304 sub UpdateWatchers {
1311 foreach my $type qw(Requestor Cc AdminCc) {
1312 my $method = $type . 'Addresses';
1313 my $oldaddr = $ticket->$method;
1315 # Skip unless we have a defined field
1316 next unless defined $args->{$type};
1317 my $newaddr = $args->{$type};
1319 my @old = split( /,\s*/, $oldaddr );
1321 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1322 # Sometimes these are email addresses, sometimes they're
1323 # users. Try to guess which is which, as we want to deal
1324 # with email addresses if at all possible.
1328 # It doesn't look like an email address. Try to load it.
1329 my $user = RT::User->new($self->CurrentUser);
1332 push @new, $user->EmailAddress;
1339 my %oldhash = map { $_ => 1 } @old;
1340 my %newhash = map { $_ => 1 } @new;
1342 my @add = grep( !defined $oldhash{$_}, @new );
1343 my @delete = grep( !defined $newhash{$_}, @old );
1346 my ( $val, $msg ) = $ticket->AddWatcher(
1352 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1356 my ( $val, $msg ) = $ticket->DeleteWatcher(
1361 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1367 sub UpdateCustomFields {
1373 foreach my $arg (keys %{$args}) {
1374 next unless $arg =~ /^CustomField-(\d+)$/;
1377 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1378 $CustomFieldObj->LoadById($cf);
1381 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1382 @values = ($args->{$arg});
1384 @values = split /\n/, $args->{$arg};
1387 if ( ($CustomFieldObj->Type eq 'Freeform'
1388 && ! $CustomFieldObj->SingleValue) ||
1389 $CustomFieldObj->Type =~ /text/i) {
1390 foreach my $val (@values) {
1395 foreach my $value (@values) {
1396 next unless length($value);
1397 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1401 push ( @results, $msg );
1410 my $postponed = shift;
1412 # postprocessing: add links
1414 while ( my $template_id = shift(@$links) ) {
1415 my $ticket = $T::Tickets{$template_id};
1416 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1417 my %args = %{ shift(@$links) };
1419 foreach my $type ( keys %LINKTYPEMAP ) {
1420 next unless ( defined $args{$type} );
1422 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1426 if ( $link =~ /^TOP$/i ) {
1427 $RT::Logger->debug( "Building $type link for $link: "
1428 . $T::Tickets{TOP}->Id );
1429 $link = $T::Tickets{TOP}->Id;
1431 } elsif ( $link !~ m/^\d+$/ ) {
1432 my $key = "create-$link";
1433 if ( !exists $T::Tickets{$key} ) {
1435 "Skipping $type link for $key (non-existent)");
1438 $RT::Logger->debug( "Building $type link for $link: "
1439 . $T::Tickets{$key}->Id );
1440 $link = $T::Tickets{$key}->Id;
1442 $RT::Logger->debug("Building $type link for $link");
1445 my ( $wval, $wmsg ) = $ticket->AddLink(
1446 Type => $LINKTYPEMAP{$type}->{'Type'},
1447 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1451 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1454 # push @non_fatal_errors, $wmsg unless ($wval);
1460 # postponed actions -- Status only, currently
1461 while ( my $template_id = shift(@$postponed) ) {
1462 my $ticket = $T::Tickets{$template_id};
1463 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1464 my %args = %{ shift(@$postponed) };
1465 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1470 eval "require RT::Action::CreateTickets_Vendor";
1471 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Vendor.pm} );
1472 eval "require RT::Action::CreateTickets_Local";
1473 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Local.pm} );