1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
46 package RT::Action::CreateTickets;
47 require RT::Action::Generic;
52 @ISA = qw(RT::Action::Generic);
58 RT::Action::CreateTickets
60 Create one or more tickets according to an externally supplied template.
65 ===Create-Ticket codereview
66 Subject: Code review for {$Tickets{'TOP'}->Subject}
68 Content: Someone has created a ticket. you should review and approve it,
69 so they can finish their work
75 Using the "CreateTickets" ScripAction and mandatory dependencies, RT now has
76 the ability to model complex workflow. When a ticket is created in a queue
77 that has a "CreateTickets" scripaction, that ScripAction parses its "Template"
83 CreateTickets uses the template as a template for an ordered set of tickets
84 to create. The basic format is as follows:
87 ===Create-Ticket: identifier
101 Each ===Create-Ticket: section is evaluated as its own
102 Text::Template object, which means that you can embed snippets
103 of perl inside the Text::Template using {} delimiters, but that
104 such sections absolutely can not span a ===Create-Ticket boundary.
106 After each ticket is created, it's stuffed into a hash called %Tickets
107 so as to be available during the creation of other tickets during the same
108 ScripAction. The hash is prepopulated with the ticket which triggered the
109 ScripAction as $Tickets{'TOP'}; you can also access that ticket using the
114 ===Create-Ticket: codereview
115 Subject: Code review for {$Tickets{'TOP'}->Subject}
117 Content: Someone has created a ticket. you should review and approve it,
118 so they can finish their work
125 ===Create-Ticket: approval
126 { # Find out who the administrators of the group called "HR"
127 # of which the creator of this ticket is a member
130 my $groups = RT::Groups->new($RT::SystemUser);
131 $groups->LimitToUserDefinedGroups();
132 $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name");
133 $groups->WithMember($TransactionObj->CreatorObj->Id);
135 my $groupid = $groups->First->Id;
137 my $adminccs = RT::Users->new($RT::SystemUser);
138 $adminccs->WhoHaveRight(
139 Right => "AdminGroup",
140 Object =>$groups->First,
141 IncludeSystemRights => undef,
142 IncludeSuperusers => 0,
143 IncludeSubgroupMembers => 0,
147 while (my $admin = $adminccs->Next) {
148 push (@admins, $admin->EmailAddress);
153 AdminCc: {join ("\nAdminCc: ",@admins) }
156 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
158 Content-Type: text/plain
159 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
163 ===Create-Ticket: two
164 Subject: Manager approval
166 Refers-On: {$Tickets{"approval"}->Id}
168 Content-Type: text/plain
170 Your approval is requred for this ticket, too.
173 =head2 Acceptable fields
175 A complete list of acceptable fields for this beastie:
178 * Queue => Name or id# of a queue
179 Subject => A text string
180 ! Status => A valid status. defaults to 'new'
181 Due => Dates can be specified in seconds since the epoch
182 to be handled literally or in a semi-free textual
183 format which RT will attempt to parse.
190 Owner => Username or id of an RT user who can and should own
192 + Requestor => Email address
193 + Cc => Email address
194 + AdminCc => Email address
207 Content => content. Can extend to multiple lines. Everything
208 within a template after a Content: header is treated
209 as content until we hit a line containing only
211 ContentType => the content-type of the Content field
212 CustomField-<id#> => custom field value
214 Fields marked with an * are required.
216 Fields marked with a + may have multiple values, simply
217 by repeating the fieldname on a new line with an additional value.
219 Fields marked with a ! are postponed to be processed after all
220 tickets in the same actions are created. Except for 'Status', those
221 field can also take a ticket name within the same action (i.e.
222 the identifiers after ==Create-Ticket), instead of raw Ticket ID
225 When parsed, field names are converted to lowercase and have -s stripped.
226 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
227 be treated as the same thing.
232 ok (require RT::Action::CreateTickets);
234 use_ok(RT::Template);
235 use_ok(RT::ScripAction);
236 use_ok(RT::ScripCondition);
239 my $approvalsq = RT::Queue->new($RT::SystemUser);
240 $approvalsq->Create(Name => 'Approvals');
241 ok ($approvalsq->Id, "Created Approvals test queue");
245 '===Create-Ticket: approval
248 AdminCc: {join ("\nAdminCc: ",@admins) }
249 Depended-On-By: {$Tickets{"TOP"}->Id}
251 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
253 Content-Type: text/plain
254 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
258 ===Create-Ticket: two
259 Subject: Manager approval.
260 Depended-On-By: approval
262 Content-Type: text/plain
264 Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that?
268 ok ($approvals =~ /Content/, "Read in the approvals template");
270 my $apptemp = RT::Template->new($RT::SystemUser);
271 $apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0");
275 my $q = RT::Queue->new($RT::SystemUser);
276 $q->Create(Name => 'WorkflowTest');
277 ok ($q->Id, "Created workflow test queue");
279 my $scrip = RT::Scrip->new($RT::SystemUser);
280 my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction',
281 ScripAction => 'Create Tickets',
282 Template => 'Approvals',
285 ok ($scrip->Id, "Created the scrip");
286 ok ($scrip->TemplateObj->Id, "Created the scrip template");
287 ok ($scrip->ConditionObj->Id, "Created the scrip condition");
288 ok ($scrip->ActionObj->Id, "Created the scrip action");
290 my $t = RT::Ticket->new($RT::SystemUser);
291 my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test",
297 my $deps = $t->DependsOn;
298 is ($deps->Count, 1, "The ticket we created depends on one other ticket");
299 my $dependson= $deps->First->TargetObj;
300 ok ($dependson->Id, "It depends on a real ticket");
301 unlike ($dependson->Subject, qr/{/, "The subject doesn't have braces in it. that means we're interpreting expressions");
302 is ($t->ReferredToBy->Count,1, "It's only referred to by one other ticket");
303 is ($t->ReferredToBy->First->BaseObj->Id,$t->DependsOn->First->TargetObj->Id, "The same ticket that depends on it refers to it.");
304 use RT::Action::CreateTickets;
305 my $action = RT::Action::CreateTickets->new( CurrentUser => $RT::SystemUser);;
307 # comma-delimited templates
308 my $commas = <<"EOF";
309 id,Queue,Subject,Owner,Content
310 ticket1,General,"foo, bar",root,blah
311 ticket2,General,foo bar,root,blah
312 ticket3,General,foo' bar,root,blah'boo
313 ticket4,General,foo' bar,,blah'boo
317 # Comma delimited templates with missing data
318 my $sparse_commas = <<"EOF";
319 id,Queue,Subject,Owner,Requestor
320 ticket14,General,,,bobby
321 ticket15,General,,,tommy
322 ticket16,General,,suzie,tommy
323 ticket17,General,Foo "bar" baz,suzie,tommy
324 ticket18,General,'Foo "bar" baz',suzie,tommy
325 ticket19,General,'Foo bar' baz,suzie,tommy
329 # tab-delimited templates
331 id\tQueue\tSubject\tOwner\tContent
332 ticket10\tGeneral\t"foo' bar"\troot\tblah'
333 ticket11\tGeneral\tfoo, bar\troot\tblah
334 ticket12\tGeneral\tfoo' bar\troot\tblah'boo
335 ticket13\tGeneral\tfoo' bar\t\tblah'boo
340 $expected{ticket1} = <<EOF;
348 $expected{ticket2} = <<EOF;
356 $expected{ticket3} = <<EOF;
364 $expected{ticket4} = <<EOF;
372 $expected{ticket10} = <<EOF;
380 $expected{ticket11} = <<EOF;
388 $expected{ticket12} = <<EOF;
396 $expected{ticket13} = <<EOF;
405 $expected{'ticket14'} = <<EOF;
411 $expected{'ticket15'} = <<EOF;
417 $expected{'ticket16'} = <<EOF;
423 $expected{'ticket17'} = <<EOF;
425 Subject: Foo "bar" baz
429 $expected{'ticket18'} = <<EOF;
431 Subject: Foo "bar" baz
435 $expected{'ticket19'} = <<EOF;
437 Subject: 'Foo bar' baz
445 $action->Parse(Content =>$commas);
446 $action->Parse(Content =>$sparse_commas);
447 $action->Parse(Content => $tabs);
450 foreach (@{ $action->{'create_tickets'} }) {
451 $got{$_} = $action->{'templates'}->{$_};
454 foreach my $id ( sort keys %expected ) {
455 ok(exists($got{"create-$id"}), "template exists for $id");
456 is($got{"create-$id"}, $expected{$id}, "template is correct for $id");
464 Jesse Vincent <jesse@bestpractical.com>
512 # {{{ Scrip methods (Commit, Prepare)
515 #Do what we need to do and send it out.
519 # Create all the tickets we care about
520 return (1) unless $self->TicketObj->Type eq 'ticket';
522 $self->CreateByTemplate( $self->TicketObj );
523 $self->UpdateByTemplate( $self->TicketObj );
534 unless ( $self->TemplateObj ) {
535 $RT::Logger->warning("No template object handed to $self\n");
538 unless ( $self->TransactionObj ) {
539 $RT::Logger->warning("No transaction object handed to $self\n");
543 unless ( $self->TicketObj ) {
544 $RT::Logger->warning("No ticket object handed to $self\n");
548 $self->Parse( Content => $self->TemplateObj->Content, _ActiveContent => 1);
557 sub CreateByTemplate {
561 $RT::Logger->debug("In CreateByTemplate");
565 # XXX: cargo cult programming that works. i'll be back.
568 local %T::Tickets = %T::Tickets;
569 local $T::TOP = $T::TOP;
570 local $T::ID = $T::ID;
571 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
574 my ( @links, @postponed );
575 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
576 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
579 $T::ID = $template_id;
580 @T::AllID = @{ $self->{'create_tickets'} };
582 ( $T::Tickets{$template_id}, $ticketargs ) =
583 $self->ParseLines( $template_id, \@links, \@postponed );
585 # Now we have a %args to work with.
586 # Make sure we have at least the minimum set of
587 # reasonable data and do our thang
589 my ( $id, $transid, $msg ) =
590 $T::Tickets{$template_id}->Create(%$ticketargs);
592 foreach my $res ( split( '\n', $msg ) ) {
594 $T::Tickets{$template_id}
595 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
599 if ( $self->TicketObj ) {
601 "Couldn't create related ticket $template_id for "
602 . $self->TicketObj->Id . " "
606 $msg = "Couldn't create ticket $template_id " . $msg;
609 $RT::Logger->error($msg);
613 $RT::Logger->debug("Assigned $template_id with $id");
614 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
616 && $T::Tickets{$template_id}->can('SetOriginObj');
620 $self->PostProcess( \@links, \@postponed );
625 sub UpdateByTemplate {
629 # XXX: cargo cult programming that works. i'll be back.
633 local %T::Tickets = %T::Tickets;
634 local $T::ID = $T::ID;
637 my ( @links, @postponed );
638 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
639 $RT::Logger->debug("Update Workflow: processing $template_id");
641 $T::ID = $template_id;
642 @T::AllID = @{ $self->{'update_tickets'} };
644 ( $T::Tickets{$template_id}, $ticketargs ) =
645 $self->ParseLines( $template_id, \@links, \@postponed );
647 # Now we have a %args to work with.
648 # Make sure we have at least the minimum set of
649 # reasonable data and do our thang
666 my $id = $template_id;
667 $id =~ s/update-(\d+).*/$1/;
668 $T::Tickets{$template_id}->Load($id);
671 if ( !$T::Tickets{$template_id}->Id ) {
672 $msg = "Couldn't update ticket $template_id " . $msg;
674 $RT::Logger->error($msg);
678 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
680 $template_id =~ m/^update-(.*)/;
681 my $base_id = "base-$1";
682 my $base = $self->{'templates'}->{$base_id};
686 $current =~ s/\n+$//;
688 # If we have no base template, set what we can.
689 if ($base ne $current) {
691 "Could not update ticket "
692 . $T::Tickets{$template_id}->Id
693 . ": Ticket has changed";
697 push @results, $T::Tickets{$template_id}->Update(
698 AttributesRef => \@attribs,
699 ARGSRef => $ticketargs
703 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
705 next unless exists $ticketargs->{'UpdateType'};
706 if ( $ticketargs->{'UpdateType'} =~ /^(private|public)$/ ) {
707 my ( $Transaction, $Description, $Object ) =
708 $T::Tickets{$template_id}->Comment(
709 CcMessageTo => $ticketargs->{'Cc'},
710 BccMessageTo => $ticketargs->{'Bcc'},
711 MIMEObj => $ticketargs->{'MIMEObj'},
712 TimeTaken => $ticketargs->{'TimeWorked'}
715 $T::Tickets{$template_id}
716 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id ) . ': '
719 elsif ( $ticketargs->{'UpdateType'} eq 'response' ) {
720 my ( $Transaction, $Description, $Object ) =
721 $T::Tickets{$template_id}->Correspond(
722 CcMessageTo => $ticketargs->{'Cc'},
723 BccMessageTo => $ticketargs->{'Bcc'},
724 MIMEObj => $ticketargs->{'MIMEObj'},
725 TimeTaken => $ticketargs->{'TimeWorked'}
728 $T::Tickets{$template_id}
729 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id ) . ': '
734 $T::Tickets{$template_id}
735 ->loc("Update type was neither correspondence nor comment.")
737 . $T::Tickets{$template_id}->loc("Update not recorded.") );
741 $self->PostProcess( \@links, \@postponed );
746 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
748 Parse a template from TEMPLATE_CONTENT
750 If $active is set to true, then we'll use Text::Template to parse the templates,
751 allowing you to embed active perl in your templates.
757 my %args = ( Content => undef,
760 _ActiveContent => undef,
763 if ($args{'_ActiveContent'}) {
764 $self->{'UsePerlTextTemplate'} =1;
767 $self->{'UsePerlTextTemplate'} = 0;
772 my ( $queue, $requestor );
773 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
774 $RT::Logger->debug("Line: ===");
775 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
777 $RT::Logger->debug("Line: $line");
778 if ( $line =~ /^===/ ) {
779 if ( $template_id && !$queue && $args{'Queue'} ) {
780 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
782 if ( $template_id && !$requestor && $args{'Requestor'} ) {
783 $self->{'templates'}->{$template_id} .=
784 "Requestor: $args{'Requestor'}\n";
789 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
790 $template_id = "create-$1";
791 $RT::Logger->debug("**** Create ticket: $template_id");
792 push @{ $self->{'create_tickets'} }, $template_id;
794 elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
795 $template_id = "update-$1";
796 $RT::Logger->debug("**** Update ticket: $template_id");
797 push @{ $self->{'update_tickets'} }, $template_id;
799 elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
800 $template_id = "base-$1";
801 $RT::Logger->debug("**** Base ticket: $template_id");
802 push @{ $self->{'base_tickets'} }, $template_id;
804 elsif ( $line =~ /^===#.*$/ ) { # a comment
808 if ( $line =~ /^Queue:(.*)/i ) {
813 if ( !$value && $args{'Queue'}) {
814 $value = $args{'Queue'};
815 $line = "Queue: $value";
818 if ( $line =~ /^Requestor:(.*)/i ) {
823 if ( !$value && $args{'Requestor'}) {
824 $value = $args{'Requestor'};
825 $line = "Requestor: $value";
828 $self->{'templates'}->{$template_id} .= $line . "\n";
831 if ( $template_id && !$queue && $args{'Queue'} ) {
832 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
835 elsif ( substr( $args{'Content'}, 0, 2 ) =~ /^id$/i ) {
836 $RT::Logger->debug("Line: id");
837 use Regexp::Common qw(delimited);
838 my $first = substr( $args{'Content'}, 0, index( $args{'Content'}, "\n" ) );
842 if ( $first =~ /\t/ ) {
848 my @fields = split( /$delimiter/, $first );
851 my $delimiter_re = qr[$delimiter];
853 my $delimited = qr[[^$delimiter]+];
854 my $empty = qr[^[$delimiter](?=[$delimiter])];
855 my $justquoted = qr[$RE{quoted}];
857 $args{'Content'} = substr( $args{'Content'}, index( $args{'Content'}, "\n" ) + 1 );
858 $RT::Logger->debug("First: $first");
861 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
863 $RT::Logger->debug("Line: $line");
865 # first item is $template_id
868 while ($line && $line =~ s/^($justquoted|.*?)(?:$delimiter_re|$)//ix) {
878 if ($tid =~ /^\d+$/) {
879 $template_id = 'update-' . $tid;
880 push @{ $self->{'update_tickets'} }, $template_id;
882 } elsif ($tid =~ /^#base-(\d+)$/) {
884 $template_id = 'base-' . $1;
885 push @{ $self->{'base_tickets'} }, $template_id;
888 $template_id = 'create-' . $tid;
889 push @{ $self->{'create_tickets'} }, $template_id;
891 $RT::Logger->debug("template_id: $tid");
895 $value = '' if ( $value =~ /^$delimiter$/ );
896 if ($value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/) {
897 substr($value,0,1) = "";
898 substr($value,-1,1) = "";
900 my $field = $fields[$i];
904 if ( $field =~ /Body/i
906 || $field =~ /Message/i )
910 if ( $field =~ /Summary/i ) {
913 if ( $field =~ /Queue/i ) {
915 if ( !$value && $args{'Queue'} ) {
916 $value = $args{'Queue'};
919 if ( $field =~ /Requestor/i ) {
921 if ( !$value && $args{'Requestor'} ) {
922 $value = $args{'Requestor'};
925 $self->{'templates'}->{$template_id} .= $field . ": ";
926 $self->{'templates'}->{$template_id} .= $value || "";
927 $self->{'templates'}->{$template_id} .= "\n";
928 $self->{'templates'}->{$template_id} .= "ENDOFCONTENT\n"
929 if $field =~ /content/i;
933 if ( !$queue && $args{'Queue'} ) {
934 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
936 if ( !$requestor && $args{'Requestor'} ) {
937 $self->{'templates'}->{$template_id} .=
938 "Requestor: $args{'Requestor'}\n";
946 my $template_id = shift;
948 my $postponed = shift;
951 my $content = $self->{'templates'}->{$template_id};
953 if ( $self->{'UsePerlTextTemplate'} ) {
956 "Workflow: evaluating\n$self->{templates}{$template_id}");
958 my $template = Text::Template->new(
964 $content = $template->fill_in(
967 $err = {@_}->{error};
971 $RT::Logger->debug("Workflow: yielding\n$content");
974 $RT::Logger->error( "Ticket creation failed: " . $err );
975 while ( my ( $k, $v ) = each %T::X ) {
977 "Eliminating $template_id from ${k}'s parents.");
978 delete $v->{$template_id};
984 my $TicketObj ||= RT::Ticket->new($self->CurrentUser);
987 my @lines = ( split( /\n/, $content ) );
988 while ( defined( my $line = shift @lines ) ) {
989 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
994 if ( ref( $args{$tag} ) )
995 { #If it's an array, we want to push the value
996 push @{ $args{$tag} }, $value;
998 elsif ( defined( $args{$tag} ) )
999 { #if we're about to get a second value, make it an array
1000 $args{$tag} = [ $args{$tag}, $value ];
1002 else { #if there's nothing there, just set the value
1003 $args{$tag} = $value;
1006 if ( $tag eq 'content' ) { #just build up the content
1007 # convert it to an array
1008 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
1009 while ( defined( my $l = shift @lines ) ) {
1010 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
1011 push @{ $args{'content'} }, $l . "\n";
1016 # if it's not content, strip leading and trailing spaces
1017 if ( $args{$tag} ) {
1018 $args{$tag} =~ s/^\s+//g;
1019 $args{$tag} =~ s/\s+$//g;
1025 foreach my $date qw(due starts started resolved) {
1026 my $dateobj = RT::Date->new($self->CurrentUser);
1027 next unless $args{$date};
1028 if ( $args{$date} =~ /^\d+$/ ) {
1029 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1032 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1034 $args{$date} = $dateobj->ISO;
1037 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
1038 if $self->TicketObj;
1040 $args{'type'} ||= 'ticket';
1043 Queue => $args{'queue'},
1044 Subject => $args{'subject'},
1046 Due => $args{'due'},
1047 Starts => $args{'starts'},
1048 Started => $args{'started'},
1049 Resolved => $args{'resolved'},
1050 Owner => $args{'owner'},
1051 Requestor => $args{'requestor'},
1053 AdminCc => $args{'admincc'},
1054 TimeWorked => $args{'timeworked'},
1055 TimeEstimated => $args{'timeestimated'},
1056 TimeLeft => $args{'timeleft'},
1057 InitialPriority => $args{'initialpriority'} || 0,
1058 FinalPriority => $args{'finalpriority'} || 0,
1059 Type => $args{'type'},
1062 if ($args{content}) {
1063 my $mimeobj = MIME::Entity->new();
1065 Type => $args{'contenttype'},
1066 Data => $args{'content'}
1068 $ticketargs{MIMEObj} = $mimeobj;
1069 $ticketargs{UpdateType} = $args{'updatetype'} if $args{'updatetype'};
1072 foreach my $key ( keys(%args) ) {
1073 $key =~ /^customfield(\d+)$/ or next;
1074 $ticketargs{ "CustomField-" . $1 } = $args{$key};
1077 $self->GetDeferred( \%args, $template_id, $links, $postponed );
1079 return $TicketObj, \%ticketargs;
1087 my $postponed = shift;
1089 # Deferred processing
1094 DependsOn => $args->{'dependson'},
1095 DependedOnBy => $args->{'dependedonby'},
1096 RefersTo => $args->{'refersto'},
1097 ReferredToBy => $args->{'referredtoby'},
1098 Children => $args->{'children'},
1099 Parents => $args->{'parents'},
1105 # Status is postponed so we don't violate dependencies
1106 $id, { Status => $args->{'status'}, }
1110 sub GetUpdateTemplate {
1115 $string .= "Queue: " . $t->QueueObj->Name . "\n";
1116 $string .= "Subject: " . $t->Subject . "\n";
1117 $string .= "Status: " . $t->Status . "\n";
1118 $string .= "UpdateType: response\n";
1119 $string .= "Content: \n";
1120 $string .= "ENDOFCONTENT\n";
1121 $string .= "Due: " . $t->DueObj->AsString . "\n";
1122 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1123 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1124 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1125 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
1126 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1127 $string .= "Cc: " . $t->CcAddresses . "\n";
1128 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1129 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1130 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1131 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1132 $string .= "InitialPriority: " . $t->Priority . "\n";
1133 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1135 foreach my $type ( sort keys %LINKTYPEMAP ) {
1137 # don't display duplicates
1138 if ( $type eq "HasMember"
1139 || $type eq "Members"
1140 || $type eq "MemberOf" )
1144 $string .= "$type: ";
1146 my $mode = $LINKTYPEMAP{$type}->{Mode};
1147 my $method = $LINKTYPEMAP{$type}->{Type};
1150 while ( my $link = $t->$method->Next ) {
1151 $links .= ", " if $links;
1153 my $object = $mode . "Obj";
1154 my $member = $link->$object;
1155 $links .= $member->Id if $member;
1164 sub GetBaseTemplate {
1169 $string .= "Queue: " . $t->Queue . "\n";
1170 $string .= "Subject: " . $t->Subject . "\n";
1171 $string .= "Status: " . $t->Status . "\n";
1172 $string .= "Due: " . $t->DueObj->Unix . "\n";
1173 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1174 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1175 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1176 $string .= "Owner: " . $t->Owner . "\n";
1177 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1178 $string .= "Cc: " . $t->CcAddresses . "\n";
1179 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1180 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1181 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1182 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1183 $string .= "InitialPriority: " . $t->Priority . "\n";
1184 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1189 sub GetCreateTemplate {
1194 $string .= "Queue: General\n";
1195 $string .= "Subject: \n";
1196 $string .= "Status: new\n";
1197 $string .= "Content: \n";
1198 $string .= "ENDOFCONTENT\n";
1199 $string .= "Due: \n";
1200 $string .= "Starts: \n";
1201 $string .= "Started: \n";
1202 $string .= "Resolved: \n";
1203 $string .= "Owner: \n";
1204 $string .= "Requestor: \n";
1205 $string .= "Cc: \n";
1206 $string .= "AdminCc:\n";
1207 $string .= "TimeWorked: \n";
1208 $string .= "TimeEstimated: \n";
1209 $string .= "TimeLeft: \n";
1210 $string .= "InitialPriority: \n";
1211 $string .= "FinalPriority: \n";
1213 foreach my $type ( keys %LINKTYPEMAP ) {
1215 # don't display duplicates
1216 if ( $type eq "HasMember"
1217 || $type eq 'Members'
1218 || $type eq 'MemberOf' )
1222 $string .= "$type: \n";
1227 sub UpdateWatchers {
1234 foreach my $type qw(Requestor Cc AdminCc) {
1235 my $method = $type . 'Addresses';
1236 my $oldaddr = $ticket->$method;
1239 # Skip unless we have a defined field
1240 next unless defined $args->{$type};
1241 my $newaddr = $args->{$type};
1243 my @old = split( ', ', $oldaddr );
1244 my @new = split( ', ', $newaddr );
1245 my %oldhash = map { $_ => 1 } @old;
1246 my %newhash = map { $_ => 1 } @new;
1248 my @add = grep( !defined $oldhash{$_}, @new );
1249 my @delete = grep( !defined $newhash{$_}, @old );
1252 my ( $val, $msg ) = $ticket->AddWatcher(
1258 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1262 my ( $val, $msg ) = $ticket->DeleteWatcher(
1267 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1276 my $postponed = shift;
1278 # postprocessing: add links
1280 while ( my $template_id = shift(@$links) ) {
1281 my $ticket = $T::Tickets{$template_id};
1282 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1283 my %args = %{ shift(@$links) };
1285 foreach my $type ( keys %LINKTYPEMAP ) {
1286 next unless ( defined $args{$type} );
1288 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1292 if ($link =~ /^TOP$/i) {
1293 $RT::Logger->debug( "Building $type link for $link: " . $T::Tickets{TOP}->Id );
1294 $link = $T::Tickets{TOP}->Id;
1297 elsif ( $link !~ m/^\d+$/ ) {
1298 my $key = "create-$link";
1299 if ( !exists $T::Tickets{$key} ) {
1300 $RT::Logger->debug( "Skipping $type link for $key (non-existent)");
1303 $RT::Logger->debug( "Building $type link for $link: " . $T::Tickets{$key}->Id );
1304 $link = $T::Tickets{$key}->Id;
1307 $RT::Logger->debug("Building $type link for $link");
1310 my ( $wval, $wmsg ) = $ticket->AddLink(
1311 Type => $LINKTYPEMAP{$type}->{'Type'},
1312 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1316 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1319 # push @non_fatal_errors, $wmsg unless ($wval);
1325 # postponed actions -- Status only, currently
1326 while ( my $template_id = shift(@$postponed) ) {
1327 my $ticket = $T::Tickets{$template_id};
1328 $RT::Logger->debug("Handling postponed actions for ".$ticket->id);
1329 my %args = %{ shift(@$postponed) };
1330 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1335 eval "require RT::Action::CreateTickets_Vendor";
1336 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Vendor.pm} );
1337 eval "require RT::Action::CreateTickets_Local";
1338 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Local.pm} );