1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 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 + man 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.
571 my ( @links, @postponed );
572 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
573 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
574 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
577 $T::ID = $template_id;
578 @T::AllID = @{ $self->{'create_tickets'} };
580 ( $T::Tickets{$template_id}, $ticketargs ) =
581 $self->ParseLines( $template_id, \@links, \@postponed );
583 # Now we have a %args to work with.
584 # Make sure we have at least the minimum set of
585 # reasonable data and do our thang
587 my ( $id, $transid, $msg ) =
588 $T::Tickets{$template_id}->Create(%$ticketargs);
590 foreach my $res ( split( '\n', $msg ) ) {
592 $T::Tickets{$template_id}
593 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
597 if ( $self->TicketObj ) {
599 "Couldn't create related ticket $template_id for "
600 . $self->TicketObj->Id . " "
604 $msg = "Couldn't create ticket $template_id " . $msg;
607 $RT::Logger->error($msg);
611 $RT::Logger->debug("Assigned $template_id with $id");
612 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
614 && $T::Tickets{$template_id}->can('SetOriginObj');
618 $self->PostProcess( \@links, \@postponed );
623 sub UpdateByTemplate {
627 # XXX: cargo cult programming that works. i'll be back.
634 my ( @links, @postponed );
635 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
636 $RT::Logger->debug("Update Workflow: processing $template_id");
638 $T::ID = $template_id;
639 @T::AllID = @{ $self->{'update_tickets'} };
641 ( $T::Tickets{$template_id}, $ticketargs ) =
642 $self->ParseLines( $template_id, \@links, \@postponed );
644 # Now we have a %args to work with.
645 # Make sure we have at least the minimum set of
646 # reasonable data and do our thang
663 my $id = $template_id;
664 $id =~ s/update-(\d+).*/$1/;
665 $T::Tickets{$template_id}->Load($id);
668 if ( !$T::Tickets{$template_id}->Id ) {
669 $msg = "Couldn't update ticket $template_id " . $msg;
671 $RT::Logger->error($msg);
675 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
677 $template_id =~ m/^update-(.*)/;
678 my $base_id = "base-$1";
679 my $base = $self->{'templates'}->{$base_id};
683 $current =~ s/\n+$//;
685 # If we have no base template, set what we can.
686 if ($base ne $current) {
688 "Could not update ticket "
689 . $T::Tickets{$template_id}->Id
690 . ": Ticket has changed";
694 push @results, $T::Tickets{$template_id}->Update(
695 AttributesRef => \@attribs,
696 ARGSRef => $ticketargs
700 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
702 next unless exists $ticketargs->{'UpdateType'};
703 if ( $ticketargs->{'UpdateType'} =~ /^(private|public)$/ ) {
704 my ( $Transaction, $Description, $Object ) =
705 $T::Tickets{$template_id}->Comment(
706 CcMessageTo => $ticketargs->{'Cc'},
707 BccMessageTo => $ticketargs->{'Bcc'},
708 MIMEObj => $ticketargs->{'MIMEObj'},
709 TimeTaken => $ticketargs->{'TimeWorked'}
712 $T::Tickets{$template_id}
713 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id ) . ': '
716 elsif ( $ticketargs->{'UpdateType'} eq 'response' ) {
717 my ( $Transaction, $Description, $Object ) =
718 $T::Tickets{$template_id}->Correspond(
719 CcMessageTo => $ticketargs->{'Cc'},
720 BccMessageTo => $ticketargs->{'Bcc'},
721 MIMEObj => $ticketargs->{'MIMEObj'},
722 TimeTaken => $ticketargs->{'TimeWorked'}
725 $T::Tickets{$template_id}
726 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id ) . ': '
731 $T::Tickets{$template_id}
732 ->loc("Update type was neither correspondence nor comment.")
734 . $T::Tickets{$template_id}->loc("Update not recorded.") );
738 $self->PostProcess( \@links, \@postponed );
743 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
745 Parse a template from TEMPLATE_CONTENT
747 If $active is set to true, then we'll use Text::Template to parse the templates,
748 allowing you to embed active perl in your templates.
754 my %args = ( Content => undef,
757 _ActiveContent => undef,
760 if ($args{'_ActiveContent'}) {
761 $self->{'UsePerlTextTemplate'} =1;
764 $self->{'UsePerlTextTemplate'} = 0;
769 my ( $queue, $requestor );
770 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
771 $RT::Logger->debug("Line: ===");
772 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
774 $RT::Logger->debug("Line: $line");
775 if ( $line =~ /^===/ ) {
776 if ( $template_id && !$queue && $args{'Queue'} ) {
777 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
779 if ( $template_id && !$requestor && $args{'Requestor'} ) {
780 $self->{'templates'}->{$template_id} .=
781 "Requestor: $args{'Requestor'}\n";
786 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
787 $template_id = "create-$1";
788 $RT::Logger->debug("**** Create ticket: $template_id");
789 push @{ $self->{'create_tickets'} }, $template_id;
791 elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
792 $template_id = "update-$1";
793 $RT::Logger->debug("**** Update ticket: $template_id");
794 push @{ $self->{'update_tickets'} }, $template_id;
796 elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
797 $template_id = "base-$1";
798 $RT::Logger->debug("**** Base ticket: $template_id");
799 push @{ $self->{'base_tickets'} }, $template_id;
801 elsif ( $line =~ /^===#.*$/ ) { # a comment
805 if ( $line =~ /^Queue:(.*)/i ) {
810 if ( !$value && $args{'Queue'}) {
811 $value = $args{'Queue'};
812 $line = "Queue: $value";
815 if ( $line =~ /^Requestor:(.*)/i ) {
820 if ( !$value && $args{'Requestor'}) {
821 $value = $args{'Requestor'};
822 $line = "Requestor: $value";
825 $self->{'templates'}->{$template_id} .= $line . "\n";
828 if ( $template_id && !$queue && $args{'Queue'} ) {
829 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
832 elsif ( substr( $args{'Content'}, 0, 2 ) =~ /^id$/i ) {
833 $RT::Logger->debug("Line: id");
834 use Regexp::Common qw(delimited);
835 my $first = substr( $args{'Content'}, 0, index( $args{'Content'}, "\n" ) );
839 if ( $first =~ /\t/ ) {
845 my @fields = split( /$delimiter/, $first );
848 my $delimiter_re = qr[$delimiter];
850 my $delimited = qr[[^$delimiter]+];
851 my $empty = qr[^[$delimiter](?=[$delimiter])];
852 my $justquoted = qr[$RE{quoted}];
854 $args{'Content'} = substr( $args{'Content'}, index( $args{'Content'}, "\n" ) + 1 );
855 $RT::Logger->debug("First: $first");
858 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
860 $RT::Logger->debug("Line: $line");
862 # first item is $template_id
865 while ($line && $line =~ s/^($justquoted|.*?)(?:$delimiter_re|$)//ix) {
875 if ($tid =~ /^\d+$/) {
876 $template_id = 'update-' . $tid;
877 push @{ $self->{'update_tickets'} }, $template_id;
879 } elsif ($tid =~ /^#base-(\d+)$/) {
881 $template_id = 'base-' . $1;
882 push @{ $self->{'base_tickets'} }, $template_id;
885 $template_id = 'create-' . $tid;
886 push @{ $self->{'create_tickets'} }, $template_id;
888 $RT::Logger->debug("template_id: $tid");
892 $value = '' if ( $value =~ /^$delimiter$/ );
893 if ($value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/) {
894 substr($value,0,1) = "";
895 substr($value,-1,1) = "";
897 my $field = $fields[$i];
901 if ( $field =~ /Body/i
903 || $field =~ /Message/i )
907 if ( $field =~ /Summary/i ) {
910 if ( $field =~ /Queue/i ) {
912 if ( !$value && $args{'Queue'} ) {
913 $value = $args{'Queue'};
916 if ( $field =~ /Requestor/i ) {
918 if ( !$value && $args{'Requestor'} ) {
919 $value = $args{'Requestor'};
922 $self->{'templates'}->{$template_id} .= $field . ": ";
923 $self->{'templates'}->{$template_id} .= $value || "";
924 $self->{'templates'}->{$template_id} .= "\n";
925 $self->{'templates'}->{$template_id} .= "ENDOFCONTENT\n"
926 if $field =~ /content/i;
930 if ( !$queue && $args{'Queue'} ) {
931 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
933 if ( !$requestor && $args{'Requestor'} ) {
934 $self->{'templates'}->{$template_id} .=
935 "Requestor: $args{'Requestor'}\n";
943 my $template_id = shift;
945 my $postponed = shift;
948 my $content = $self->{'templates'}->{$template_id};
950 if ( $self->{'UsePerlTextTemplate'} ) {
953 "Workflow: evaluating\n$self->{templates}{$template_id}");
955 my $template = Text::Template->new(
961 $content = $template->fill_in(
964 $err = {@_}->{error};
968 $RT::Logger->debug("Workflow: yielding\n$content");
971 $RT::Logger->error( "Ticket creation failed: " . $err );
972 while ( my ( $k, $v ) = each %T::X ) {
974 "Eliminating $template_id from ${k}'s parents.");
975 delete $v->{$template_id};
981 my $TicketObj ||= RT::Ticket->new($self->CurrentUser);
984 my @lines = ( split( /\n/, $content ) );
985 while ( defined( my $line = shift @lines ) ) {
986 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
991 if ( ref( $args{$tag} ) )
992 { #If it's an array, we want to push the value
993 push @{ $args{$tag} }, $value;
995 elsif ( defined( $args{$tag} ) )
996 { #if we're about to get a second value, make it an array
997 $args{$tag} = [ $args{$tag}, $value ];
999 else { #if there's nothing there, just set the value
1000 $args{$tag} = $value;
1003 if ( $tag eq 'content' ) { #just build up the content
1004 # convert it to an array
1005 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
1006 while ( defined( my $l = shift @lines ) ) {
1007 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
1008 push @{ $args{'content'} }, $l . "\n";
1013 # if it's not content, strip leading and trailing spaces
1014 if ( $args{$tag} ) {
1015 $args{$tag} =~ s/^\s+//g;
1016 $args{$tag} =~ s/\s+$//g;
1022 foreach my $date qw(due starts started resolved) {
1023 my $dateobj = RT::Date->new($self->CurrentUser);
1024 next unless $args{$date};
1025 if ( $args{$date} =~ /^\d+$/ ) {
1026 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1029 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1031 $args{$date} = $dateobj->ISO;
1034 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
1035 if $self->TicketObj;
1037 $args{'type'} ||= 'ticket';
1040 Queue => $args{'queue'},
1041 Subject => $args{'subject'},
1043 Due => $args{'due'},
1044 Starts => $args{'starts'},
1045 Started => $args{'started'},
1046 Resolved => $args{'resolved'},
1047 Owner => $args{'owner'},
1048 Requestor => $args{'requestor'},
1050 AdminCc => $args{'admincc'},
1051 TimeWorked => $args{'timeworked'},
1052 TimeEstimated => $args{'timeestimated'},
1053 TimeLeft => $args{'timeleft'},
1054 InitialPriority => $args{'initialpriority'} || 0,
1055 FinalPriority => $args{'finalpriority'} || 0,
1056 Type => $args{'type'},
1059 if ($args{content}) {
1060 my $mimeobj = MIME::Entity->new();
1062 Type => $args{'contenttype'},
1063 Data => $args{'content'}
1065 $ticketargs{MIMEObj} = $mimeobj;
1066 $ticketargs{UpdateType} = $args{'updatetype'} if $args{'updatetype'};
1069 foreach my $key ( keys(%args) ) {
1070 $key =~ /^customfield(\d+)$/ or next;
1071 $ticketargs{ "CustomField-" . $1 } = $args{$key};
1074 $self->GetDeferred( \%args, $template_id, $links, $postponed );
1076 return $TicketObj, \%ticketargs;
1084 my $postponed = shift;
1086 # Deferred processing
1091 DependsOn => $args->{'dependson'},
1092 DependedOnBy => $args->{'dependedonby'},
1093 RefersTo => $args->{'refersto'},
1094 ReferredToBy => $args->{'referredtoby'},
1095 Children => $args->{'children'},
1096 Parents => $args->{'parents'},
1102 # Status is postponed so we don't violate dependencies
1103 $id, { Status => $args->{'status'}, }
1107 sub GetUpdateTemplate {
1112 $string .= "Queue: " . $t->QueueObj->Name . "\n";
1113 $string .= "Subject: " . $t->Subject . "\n";
1114 $string .= "Status: " . $t->Status . "\n";
1115 $string .= "UpdateType: response\n";
1116 $string .= "Content: \n";
1117 $string .= "ENDOFCONTENT\n";
1118 $string .= "Due: " . $t->DueObj->AsString . "\n";
1119 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1120 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1121 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1122 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
1123 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1124 $string .= "Cc: " . $t->CcAddresses . "\n";
1125 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1126 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1127 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1128 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1129 $string .= "InitialPriority: " . $t->Priority . "\n";
1130 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1132 foreach my $type ( sort keys %LINKTYPEMAP ) {
1134 # don't display duplicates
1135 if ( $type eq "HasMember"
1136 || $type eq "Members"
1137 || $type eq "MemberOf" )
1141 $string .= "$type: ";
1143 my $mode = $LINKTYPEMAP{$type}->{Mode};
1144 my $method = $LINKTYPEMAP{$type}->{Type};
1147 while ( my $link = $t->$method->Next ) {
1148 $links .= ", " if $links;
1150 my $object = $mode . "Obj";
1151 my $member = $link->$object;
1152 $links .= $member->Id if $member;
1161 sub GetBaseTemplate {
1166 $string .= "Queue: " . $t->Queue . "\n";
1167 $string .= "Subject: " . $t->Subject . "\n";
1168 $string .= "Status: " . $t->Status . "\n";
1169 $string .= "Due: " . $t->DueObj->Unix . "\n";
1170 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1171 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1172 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1173 $string .= "Owner: " . $t->Owner . "\n";
1174 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1175 $string .= "Cc: " . $t->CcAddresses . "\n";
1176 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1177 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1178 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1179 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1180 $string .= "InitialPriority: " . $t->Priority . "\n";
1181 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1186 sub GetCreateTemplate {
1191 $string .= "Queue: General\n";
1192 $string .= "Subject: \n";
1193 $string .= "Status: new\n";
1194 $string .= "Content: \n";
1195 $string .= "ENDOFCONTENT\n";
1196 $string .= "Due: \n";
1197 $string .= "Starts: \n";
1198 $string .= "Started: \n";
1199 $string .= "Resolved: \n";
1200 $string .= "Owner: \n";
1201 $string .= "Requestor: \n";
1202 $string .= "Cc: \n";
1203 $string .= "AdminCc:\n";
1204 $string .= "TimeWorked: \n";
1205 $string .= "TimeEstimated: \n";
1206 $string .= "TimeLeft: \n";
1207 $string .= "InitialPriority: \n";
1208 $string .= "FinalPriority: \n";
1210 foreach my $type ( keys %LINKTYPEMAP ) {
1212 # don't display duplicates
1213 if ( $type eq "HasMember"
1214 || $type eq 'Members'
1215 || $type eq 'MemberOf' )
1219 $string .= "$type: \n";
1224 sub UpdateWatchers {
1231 foreach my $type qw(Requestor Cc AdminCc) {
1232 my $method = $type . 'Addresses';
1233 my $oldaddr = $ticket->$method;
1236 # Skip unless we have a defined field
1237 next unless defined $args->{$type};
1238 my $newaddr = $args->{$type};
1240 my @old = split( ', ', $oldaddr );
1241 my @new = split( ', ', $newaddr );
1242 my %oldhash = map { $_ => 1 } @old;
1243 my %newhash = map { $_ => 1 } @new;
1245 my @add = grep( !defined $oldhash{$_}, @new );
1246 my @delete = grep( !defined $newhash{$_}, @old );
1249 my ( $val, $msg ) = $ticket->AddWatcher(
1255 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1259 my ( $val, $msg ) = $ticket->DeleteWatcher(
1264 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1273 my $postponed = shift;
1275 # postprocessing: add links
1277 while ( my $template_id = shift(@$links) ) {
1278 my $ticket = $T::Tickets{$template_id};
1279 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1280 my %args = %{ shift(@$links) };
1282 foreach my $type ( keys %LINKTYPEMAP ) {
1283 next unless ( defined $args{$type} );
1285 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1289 if ($link =~ /^TOP$/i) {
1290 $RT::Logger->debug( "Building $type link for $link: " . $T::Tickets{TOP}->Id );
1291 $link = $T::Tickets{TOP}->Id;
1294 elsif ( $link !~ m/^\d+$/ ) {
1295 my $key = "create-$link";
1296 if ( !exists $T::Tickets{$key} ) {
1297 $RT::Logger->debug( "Skipping $type link for $key (non-existent)");
1300 $RT::Logger->debug( "Building $type link for $link: " . $T::Tickets{$key}->Id );
1301 $link = $T::Tickets{$key}->Id;
1304 $RT::Logger->debug("Building $type link for $link");
1307 my ( $wval, $wmsg ) = $ticket->AddLink(
1308 Type => $LINKTYPEMAP{$type}->{'Type'},
1309 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1313 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1316 # push @non_fatal_errors, $wmsg unless ($wval);
1322 # postponed actions -- Status only, currently
1323 while ( my $template_id = shift(@$postponed) ) {
1324 my $ticket = $T::Tickets{$template_id};
1325 $RT::Logger->debug("Handling postponed actions for ".$ticket->id);
1326 my %args = %{ shift(@$postponed) };
1327 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1332 eval "require RT::Action::CreateTickets_Vendor";
1333 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Vendor.pm} );
1334 eval "require RT::Action::CreateTickets_Local";
1335 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Local.pm} );