import rt 3.6.6
[freeside.git] / rt / lib / RT / Action / CreateTickets.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
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
16 # from www.gnu.org.
17
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.
22
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.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
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.)
37
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.
46
47 # END BPS TAGGED BLOCK }}}
48 package RT::Action::CreateTickets;
49 require RT::Action::Generic;
50
51 use strict;
52 use warnings;
53 use vars qw/@ISA/;
54 @ISA = qw(RT::Action::Generic);
55
56 use MIME::Entity;
57
58 =head1 NAME
59
60  RT::Action::CreateTickets
61
62 Create one or more tickets according to an externally supplied template.
63
64
65 =head1 SYNOPSIS
66
67  ===Create-Ticket codereview
68  Subject: Code review for {$Tickets{'TOP'}->Subject}
69  Depended-On-By: TOP
70  Content: Someone has created a ticket. you should review and approve it,
71  so they can finish their work
72  ENDOFCONTENT
73
74 =head1 DESCRIPTION
75
76
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"
80
81
82
83 =head2 FORMAT
84
85 CreateTickets uses the template as a template for an ordered set of tickets 
86 to create. The basic format is as follows:
87
88
89  ===Create-Ticket: identifier
90  Param: Value
91  Param2: Value
92  Param3: Value
93  Content: Blah
94  blah
95  blah
96  ENDOFCONTENT
97  ===Create-Ticket: id2
98  Param: Value
99  Content: Blah
100  ENDOFCONTENT
101
102
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.
107
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
112 shorthand TOP.
113
114 A simple example:
115
116  ===Create-Ticket: codereview
117  Subject: Code review for {$Tickets{'TOP'}->Subject}
118  Depended-On-By: TOP
119  Content: Someone has created a ticket. you should review and approve it,
120  so they can finish their work
121  ENDOFCONTENT
122
123
124
125 A convoluted example
126
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
130     my $name = "HR";
131    
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);
136  
137     my $groupid = $groups->First->Id;
138  
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,
146     );
147  
148      my @admins;
149      while (my $admin = $adminccs->Next) {
150          push (@admins, $admin->EmailAddress); 
151      }
152  }
153  Queue: ___Approvals
154  Type: approval
155  AdminCc: {join ("\nAdminCc: ",@admins) }
156  Depended-On-By: TOP
157  Refers-To: TOP
158  Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
159  Due: {time + 86400}
160  Content-Type: text/plain
161  Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
162  Blah
163  Blah
164  ENDOFCONTENT
165  ===Create-Ticket: two
166  Subject: Manager approval
167  Depended-On-By: TOP
168  Refers-On: {$Tickets{"approval"}->Id}
169  Queue: ___Approvals
170  Content-Type: text/plain
171  Content: 
172  Your approval is requred for this ticket, too.
173  ENDOFCONTENT
174  
175 =head2 Acceptable fields
176
177 A complete list of acceptable fields for this beastie:
178
179
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.
186                         
187                           
188                           
189        Starts          => 
190        Started         => 
191        Resolved        => 
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 
197        TimeWorked      => 
198        TimeEstimated   => 
199        TimeLeft        => 
200        InitialPriority => 
201        FinalPriority   => 
202        Type            => 
203     +! DependsOn       => 
204     +! DependedOnBy    =>
205     +! RefersTo        =>
206     +! ReferredToBy    => 
207     +! Members         =>
208     +! MemberOf        => 
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 
212                           ENDOFCONTENT
213        ContentType     => the content-type of the Content field.  Defaults to
214                           'text/plain'
215        UpdateType      => 'correspond' or 'comment'; used in conjunction with
216                           'content' if this is an update.  Defaults to
217                           'correspond'
218
219        CustomField-<id#> => custom field value
220        CF-name           => custom field value
221        CustomField-name  => custom field value
222
223 Fields marked with an * are required.
224
225 Fields marked with a + may have multiple values, simply
226 by repeating the fieldname on a new line with an additional value.
227
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
232 numbers.
233
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.
237
238
239 =begin testing
240
241 ok (require RT::Action::CreateTickets);
242 use_ok(RT::Scrip);
243 use_ok(RT::Template);
244 use_ok(RT::ScripAction);
245 use_ok(RT::ScripCondition);
246 use_ok(RT::Ticket);
247
248 my $approvalsq = RT::Queue->new($RT::SystemUser);
249 $approvalsq->Create(Name => 'Approvals');
250 ok ($approvalsq->Id, "Created Approvals test queue");
251
252
253 my $approvals = 
254 '===Create-Ticket: approval
255 Queue: ___Approvals
256 Type: approval
257 AdminCc: {join ("\nAdminCc: ",@admins) }
258 Depended-On-By: {$Tickets{"TOP"}->Id}
259 Refers-To: TOP 
260 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
261 Due: {time + 86400}
262 Content-Type: text/plain
263 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
264 Blah
265 Blah
266 ENDOFCONTENT
267 ===Create-Ticket: two
268 Subject: Manager approval.
269 Depended-On-By: approval
270 Queue: ___Approvals
271 Content-Type: text/plain
272 Content: 
273 Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that?
274 ENDOFCONTENT
275 ';
276
277 ok ($approvals =~ /Content/, "Read in the approvals template");
278
279 my $apptemp = RT::Template->new($RT::SystemUser);
280 $apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0");
281
282 ok ($apptemp->Id);
283
284 my $q = RT::Queue->new($RT::SystemUser);
285 $q->Create(Name => 'WorkflowTest');
286 ok ($q->Id, "Created workflow test queue");
287
288 my $scrip = RT::Scrip->new($RT::SystemUser);
289 my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction',
290                 ScripAction => 'Create Tickets',
291                 Template => 'Approvals',
292                 Queue => $q->Id);
293 ok ($sval, $smsg);
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");
298
299 my $t = RT::Ticket->new($RT::SystemUser);
300 my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test",
301            Owner => "root",
302            Queue => $q->Id);
303
304 ok ($tid,$tmsg);
305
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);;
315
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
323 EOF
324
325
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
335 EOF
336
337
338 # tab-delimited templates
339 my $tabs = <<"EOF";
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
345 EOF
346
347 my %expected;
348
349 $expected{ticket1} = <<EOF;
350 Queue: General
351 Subject: foo, bar
352 Owner: root
353 Content: blah
354 ENDOFCONTENT
355 EOF
356
357 $expected{ticket2} = <<EOF;
358 Queue: General
359 Subject: foo bar
360 Owner: root
361 Content: blah
362 ENDOFCONTENT
363 EOF
364
365 $expected{ticket3} = <<EOF;
366 Queue: General
367 Subject: foo' bar
368 Owner: root
369 Content: blah'boo
370 ENDOFCONTENT
371 EOF
372
373 $expected{ticket4} = <<EOF;
374 Queue: General
375 Subject: foo' bar
376 Owner: 
377 Content: blah'boo
378 ENDOFCONTENT
379 EOF
380
381 $expected{ticket10} = <<EOF;
382 Queue: General
383 Subject: foo' bar
384 Owner: root
385 Content: blah'
386 ENDOFCONTENT
387 EOF
388
389 $expected{ticket11} = <<EOF;
390 Queue: General
391 Subject: foo, bar
392 Owner: root
393 Content: blah
394 ENDOFCONTENT
395 EOF
396
397 $expected{ticket12} = <<EOF;
398 Queue: General
399 Subject: foo' bar
400 Owner: root
401 Content: blah'boo
402 ENDOFCONTENT
403 EOF
404
405 $expected{ticket13} = <<EOF;
406 Queue: General
407 Subject: foo' bar
408 Owner: 
409 Content: blah'boo
410 ENDOFCONTENT
411 EOF
412
413
414 $expected{'ticket14'} = <<EOF;
415 Queue: General
416 Subject: 
417 Owner: 
418 Requestor: bobby
419 EOF
420 $expected{'ticket15'} = <<EOF;
421 Queue: General
422 Subject: 
423 Owner: 
424 Requestor: tommy
425 EOF
426 $expected{'ticket16'} = <<EOF;
427 Queue: General
428 Subject: 
429 Owner: suzie
430 Requestor: tommy
431 EOF
432 $expected{'ticket17'} = <<EOF;
433 Queue: General
434 Subject: Foo "bar" baz
435 Owner: suzie
436 Requestor: tommy
437 EOF
438 $expected{'ticket18'} = <<EOF;
439 Queue: General
440 Subject: Foo "bar" baz
441 Owner: suzie
442 Requestor: tommy
443 EOF
444 $expected{'ticket19'} = <<EOF;
445 Queue: General
446 Subject: 'Foo bar' baz
447 Owner: suzie
448 Requestor: tommy
449 EOF
450
451
452
453
454 $action->Parse(Content =>$commas);
455 $action->Parse(Content =>$sparse_commas);
456 $action->Parse(Content => $tabs);
457
458 my %got;
459 foreach (@{ $action->{'create_tickets'} }) {
460   $got{$_} = $action->{'templates'}->{$_};
461 }
462
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");
466 }
467
468 =end testing
469
470
471 =head1 AUTHOR
472
473 Jesse Vincent <jesse@bestpractical.com> 
474
475 =head1 SEE ALSO
476
477 perl(1).
478
479 =cut
480
481 my %LINKTYPEMAP = (
482     MemberOf => {
483         Type => 'MemberOf',
484         Mode => 'Target',
485     },
486     Parents => {
487         Type => 'MemberOf',
488         Mode => 'Target',
489     },
490     Members => {
491         Type => 'MemberOf',
492         Mode => 'Base',
493     },
494     Children => {
495         Type => 'MemberOf',
496         Mode => 'Base',
497     },
498     HasMember => {
499         Type => 'MemberOf',
500         Mode => 'Base',
501     },
502     RefersTo => {
503         Type => 'RefersTo',
504         Mode => 'Target',
505     },
506     ReferredToBy => {
507         Type => 'RefersTo',
508         Mode => 'Base',
509     },
510     DependsOn => {
511         Type => 'DependsOn',
512         Mode => 'Target',
513     },
514     DependedOnBy => {
515         Type => 'DependsOn',
516         Mode => 'Base',
517     },
518
519 );
520
521 # {{{ Scrip methods (Commit, Prepare)
522
523 # {{{ sub Commit
524 #Do what we need to do and send it out.
525 sub Commit {
526     my $self = shift;
527
528     # Create all the tickets we care about
529     return (1) unless $self->TicketObj->Type eq 'ticket';
530
531     $self->CreateByTemplate( $self->TicketObj );
532     $self->UpdateByTemplate( $self->TicketObj );
533     return (1);
534 }
535
536 # }}}
537
538 # {{{ sub Prepare
539
540 sub Prepare {
541     my $self = shift;
542
543     unless ( $self->TemplateObj ) {
544         $RT::Logger->warning("No template object handed to $self\n");
545     }
546
547     unless ( $self->TransactionObj ) {
548         $RT::Logger->warning("No transaction object handed to $self\n");
549
550     }
551
552     unless ( $self->TicketObj ) {
553         $RT::Logger->warning("No ticket object handed to $self\n");
554
555     }
556
557     $self->Parse(
558         Content        => $self->TemplateObj->Content,
559         _ActiveContent => 1
560     );
561     return 1;
562
563 }
564
565 # }}}
566
567 # }}}
568
569 sub CreateByTemplate {
570     my $self = shift;
571     my $top  = shift;
572
573     $RT::Logger->debug("In CreateByTemplate");
574
575     my @results;
576
577     # XXX: cargo cult programming that works. i'll be back.
578     use bytes;
579
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;
584
585     my $ticketargs;
586     my ( @links, @postponed );
587     foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
588         $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
589             if $T::TOP;
590
591         $T::ID    = $template_id;
592         @T::AllID = @{ $self->{'create_tickets'} };
593
594         ( $T::Tickets{$template_id}, $ticketargs )
595             = $self->ParseLines( $template_id, \@links, \@postponed );
596
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
600
601         my ( $id, $transid, $msg )
602             = $T::Tickets{$template_id}->Create(%$ticketargs);
603
604         foreach my $res ( split( '\n', $msg ) ) {
605             push @results,
606                 $T::Tickets{$template_id}
607                 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
608                 . $res;
609         }
610         if ( !$id ) {
611             if ( $self->TicketObj ) {
612                 $msg = "Couldn't create related ticket $template_id for "
613                     . $self->TicketObj->Id . " "
614                     . $msg;
615             } else {
616                 $msg = "Couldn't create ticket $template_id " . $msg;
617             }
618
619             $RT::Logger->error($msg);
620             next;
621         }
622
623         $RT::Logger->debug("Assigned $template_id with $id");
624         $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
625             if $self->TicketObj
626             && $T::Tickets{$template_id}->can('SetOriginObj');
627
628     }
629
630     $self->PostProcess( \@links, \@postponed );
631
632     return @results;
633 }
634
635 sub UpdateByTemplate {
636     my $self = shift;
637     my $top  = shift;
638
639     # XXX: cargo cult programming that works. i'll be back.
640     use bytes;
641
642     my @results;
643     local %T::Tickets = %T::Tickets;
644     local $T::ID      = $T::ID;
645
646     my $ticketargs;
647     my ( @links, @postponed );
648     foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
649         $RT::Logger->debug("Update Workflow: processing $template_id");
650
651         $T::ID    = $template_id;
652         @T::AllID = @{ $self->{'update_tickets'} };
653
654         ( $T::Tickets{$template_id}, $ticketargs )
655             = $self->ParseLines( $template_id, \@links, \@postponed );
656
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
660
661         my @attribs = qw(
662             Subject
663             FinalPriority
664             Priority
665             TimeEstimated
666             TimeWorked
667             TimeLeft
668             Status
669             Queue
670             Due
671             Starts
672             Started
673             Resolved
674         );
675
676         my $id = $template_id;
677         $id =~ s/update-(\d+).*/$1/;
678         my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
679
680         unless ( $loaded ) {
681             $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
682             push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
683             next;
684         }
685
686         my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
687
688         $template_id =~ m/^update-(.*)/;
689         my $base_id = "base-$1";
690         my $base    = $self->{'templates'}->{$base_id};
691         if ($base) {
692             $base    =~ s/\r//g;
693             $base    =~ s/\n+$//;
694             $current =~ s/\n+$//;
695
696             # If we have no base template, set what we can.
697             if ( $base ne $current ) {
698                 push @results,
699                     "Could not update ticket "
700                     . $T::Tickets{$template_id}->Id
701                     . ": Ticket has changed";
702                 next;
703             }
704         }
705         push @results, $T::Tickets{$template_id}->Update(
706             AttributesRef => \@attribs,
707             ARGSRef       => $ticketargs
708         );
709
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");
713         }
714
715         push @results,
716             $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
717
718         push @results,
719             $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
720
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'}
728                 );
729             push( @results,
730                 $T::Tickets{$template_id}
731                     ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
732                     . ': '
733                     . $Description );
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'}
740                 );
741             push( @results,
742                 $T::Tickets{$template_id}
743                     ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
744                     . ': '
745                     . $Description );
746         } else {
747             push(
748                 @results,
749                 $T::Tickets{$template_id}->loc(
750                     "Update type was neither correspondence nor comment.")
751                     . " "
752                     . $T::Tickets{$template_id}->loc("Update not recorded.")
753             );
754         }
755     }
756
757     $self->PostProcess( \@links, \@postponed );
758
759     return @results;
760 }
761
762 =head2 Parse  TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
763
764 Parse a template from TEMPLATE_CONTENT
765
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.
768
769 =cut
770
771 sub Parse {
772     my $self = shift;
773     my %args = (
774         Content        => undef,
775         Queue          => undef,
776         Requestor      => undef,
777         _ActiveContent => undef,
778         @_
779     );
780
781     if ( $args{'_ActiveContent'} ) {
782         $self->{'UsePerlTextTemplate'} = 1;
783     } else {
784
785         $self->{'UsePerlTextTemplate'} = 0;
786     }
787
788     if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
789         $self->_ParseMultilineTemplate(%args);
790     } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
791         $self->_ParseXSVTemplate(%args);
792
793     }
794 }
795
796 =head2 _ParseMultilineTemplate
797
798 Parses mulitline templates. Things like:
799
800  ===Create-Ticket ... 
801
802 Takes the same arguments as Parse
803
804 =cut
805
806 sub _ParseMultilineTemplate {
807     my $self = shift;
808     my %args = (@_);
809
810     my $template_id;
811     my ( $queue, $requestor );
812         $RT::Logger->debug("Line: ===");
813         foreach my $line ( split( /\n/, $args{'Content'} ) ) {
814             $line =~ s/\r$//;
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";
820                 }
821                 if ( $template_id && !$requestor && $args{'Requestor'} ) {
822                     $self->{'templates'}->{$template_id}
823                         .= "Requestor: $args{'Requestor'}\n";
824                 }
825                 $queue     = 0;
826                 $requestor = 0;
827             }
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
841                 next;
842             } else {
843                 if ( $line =~ /^Queue:(.*)/i ) {
844                     $queue = 1;
845                     my $value = $1;
846                     $value =~ s/^\s//;
847                     $value =~ s/\s$//;
848                     if ( !$value && $args{'Queue'} ) {
849                         $value = $args{'Queue'};
850                         $line  = "Queue: $value";
851                     }
852                 }
853                 if ( $line =~ /^Requestors?:(.*)/i ) {
854                     $requestor = 1;
855                     my $value = $1;
856                     $value =~ s/^\s//;
857                     $value =~ s/\s$//;
858                     if ( !$value && $args{'Requestor'} ) {
859                         $value = $args{'Requestor'};
860                         $line  = "Requestor: $value";
861                     }
862                 }
863                 $self->{'templates'}->{$template_id} .= $line . "\n";
864             }
865         }
866         if ( $template_id && !$queue && $args{'Queue'} ) {
867             $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
868         }
869     }
870
871 sub ParseLines {
872     my $self        = shift;
873     my $template_id = shift;
874     my $links       = shift;
875     my $postponed   = shift;
876
877     my $content = $self->{'templates'}->{$template_id};
878
879     if ( $self->{'UsePerlTextTemplate'} ) {
880
881         $RT::Logger->debug(
882             "Workflow: evaluating\n$self->{templates}{$template_id}");
883
884         my $template = Text::Template->new(
885             TYPE   => 'STRING',
886             SOURCE => $content
887         );
888
889         my $err;
890         $content = $template->fill_in(
891             PACKAGE => 'T',
892             BROKEN  => sub {
893                 $err = {@_}->{error};
894             }
895         );
896
897         $RT::Logger->debug("Workflow: yielding\n$content");
898
899         if ($err) {
900             $RT::Logger->error( "Ticket creation failed: " . $err );
901             while ( my ( $k, $v ) = each %T::X ) {
902                 $RT::Logger->debug(
903                     "Eliminating $template_id from ${k}'s parents.");
904                 delete $v->{$template_id};
905             }
906             next;
907         }
908     }
909
910     my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
911
912     my %args;
913     my %original_tags;
914     my @lines = ( split( /\n/, $content ) );
915     while ( defined( my $line = shift @lines ) ) {
916         if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
917             my $value = $2;
918             my $original_tag = $1;
919             my $tag   = lc($original_tag);
920             $tag =~ s/-//g;
921             $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
922
923             $original_tags{$tag} = $original_tag;
924
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;
933             }
934
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";
941                 }
942             } else {
943                 # if it's not content, strip leading and trailing spaces
944                 if ( $args{$tag} ) {
945                     $args{$tag} =~ s/^\s+//g;
946                     $args{$tag} =~ s/\s+$//g;
947                 }
948                 if (($tag =~ /^(requestor|cc|admincc)$/i or grep {lc $_ eq $tag} keys %LINKTYPEMAP) and $args{$tag} =~ /,/) {
949                     $args{$tag} = [ split /,\s*/, $args{$tag} ];
950                 }
951             }
952         }
953     }
954
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} );
960         } else {
961             eval {
962                 $dateobj->Set( Format => 'iso', Value => $args{$date} );
963             };
964             if ($@ or $dateobj->Unix <= 0) {
965                 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
966             }
967         }
968         $args{$date} = $dateobj->ISO;
969     }
970
971     $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
972         if $self->TicketObj;
973
974     $args{'type'} ||= 'ticket';
975
976     my %ticketargs = (
977         Queue           => $args{'queue'},
978         Subject         => $args{'subject'},
979         Status          => $args{'status'} || 'new',
980         Due             => $args{'due'},
981         Starts          => $args{'starts'},
982         Started         => $args{'started'},
983         Resolved        => $args{'resolved'},
984         Owner           => $args{'owner'},
985         Requestor       => $args{'requestor'},
986         Cc              => $args{'cc'},
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'},
994     );
995
996     if ( $args{content} ) {
997         my $mimeobj = MIME::Entity->new();
998         $mimeobj->build(
999             Type => $args{'contenttype'} || 'text/plain',
1000             Data => $args{'content'}
1001         );
1002         $ticketargs{MIMEObj} = $mimeobj;
1003         $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
1004     }
1005
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};
1020
1021         }
1022     }
1023
1024     $self->GetDeferred( \%args, $template_id, $links, $postponed );
1025
1026     return $TicketObj, \%ticketargs;
1027 }
1028
1029
1030 =head2 _ParseXSVTemplate 
1031
1032 Parses a tab or comma delimited template. Should only ever be called by Parse
1033
1034 =cut
1035
1036 sub _ParseXSVTemplate {
1037     my $self = shift;
1038     my %args = (@_);
1039
1040     use Regexp::Common qw(delimited);
1041     my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
1042
1043     my $delimiter;
1044     if ( $first =~ /\t/ ) {
1045         $delimiter = "\t";
1046     } else {
1047         $delimiter = ',';
1048     }
1049     my @fields = split( /$delimiter/, $first );
1050
1051     my $delimiter_re = qr[$delimiter];
1052     my $justquoted = qr[$RE{quoted}];
1053
1054     # Used to generate automatic template ids
1055     my $autoid = 1;
1056
1057   LINE:
1058     while ($content) {
1059         $content =~ s/^(\s*\r?\n)+//;
1060
1061         # Keep track of Queue and Requestor, so we can provide defaults
1062         my $queue;
1063         my $requestor;
1064
1065         # The template for this line
1066         my $template;
1067
1068         # What column we're on
1069         my $i = 0;
1070
1071         # If the last iteration was the end of the line
1072         my $EOL = 0;
1073
1074         # The template id
1075         my $template_id;
1076
1077       COLUMN:
1078         while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
1079             $EOL = not $2;
1080
1081             # Strip off quotes, if they exist
1082             my $value = $1;
1083             if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
1084                 substr( $value, 0,  1 ) = "";
1085                 substr( $value, -1, 1 ) = "";
1086             }
1087
1088             # What column is this?
1089             my $field = $fields[$i++];
1090             next COLUMN unless $field =~ /\S/;
1091             $field =~ s/^\s//;
1092             $field =~ s/\s$//;
1093
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;
1105                 }
1106             } else {
1107                 # Some translations
1108                 if (   $field =~ /^Body$/i
1109                     || $field =~ /^Data$/i
1110                     || $field =~ /^Message$/i )
1111                   {
1112                   $field = 'Content';
1113                 } elsif ( $field =~ /^Summary$/i ) {
1114                     $field = 'Subject';
1115                 } elsif ( $field =~ /^Queue$/i ) {
1116                     # Note that we found a queue
1117                     $queue = 1;
1118                     $value ||= $args{'Queue'};
1119                 } elsif ( $field =~ /^Requestors?$/i ) {
1120                     $field = 'Requestor'; # Remove plural
1121                     # Note that we found a requestor
1122                     $requestor = 1;
1123                     $value ||= $args{'Requestor'};
1124                 }
1125
1126                 # Tack onto the end of the template
1127                 $template .= $field . ": ";
1128                 $template .= (defined $value ? $value : "");
1129                 $template .= "\n";
1130                 $template .= "ENDOFCONTENT\n"
1131                   if $field =~ /^Content$/i;
1132             }
1133         }
1134
1135         # Ignore blank lines
1136         next unless $template;
1137         
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";
1141         }
1142         if ( !$requestor && $args{'Requestor'} ) {
1143             $template .= "Requestor: $args{'Requestor'}\n";
1144         }
1145
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;
1152         }
1153         
1154         # Save the template we generated
1155         $self->{'templates'}->{$template_id} = $template;
1156
1157     }
1158 }
1159
1160 sub GetDeferred {
1161     my $self      = shift;
1162     my $args      = shift;
1163     my $id        = shift;
1164     my $links     = shift;
1165     my $postponed = shift;
1166
1167     # Deferred processing
1168     push @$links,
1169         (
1170         $id,
1171         {   DependsOn    => $args->{'dependson'},
1172             DependedOnBy => $args->{'dependedonby'},
1173             RefersTo     => $args->{'refersto'},
1174             ReferredToBy => $args->{'referredtoby'},
1175             Children     => $args->{'children'},
1176             Parents      => $args->{'parents'},
1177         }
1178         );
1179
1180     push @$postponed, (
1181
1182         # Status is postponed so we don't violate dependencies
1183         $id, { Status => $args->{'status'}, }
1184     );
1185 }
1186
1187 sub GetUpdateTemplate {
1188     my $self = shift;
1189     my $t    = shift;
1190
1191     my $string;
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";
1211
1212     foreach my $type ( sort keys %LINKTYPEMAP ) {
1213
1214         # don't display duplicates
1215         if (   $type eq "HasMember"
1216             || $type eq "Members"
1217             || $type eq "MemberOf" )
1218         {
1219             next;
1220         }
1221         $string .= "$type: ";
1222
1223         my $mode   = $LINKTYPEMAP{$type}->{Mode};
1224         my $method = $LINKTYPEMAP{$type}->{Type};
1225
1226         my $links;
1227         while ( my $link = $t->$method->Next ) {
1228             $links .= ", " if $links;
1229
1230             my $object = $mode . "Obj";
1231             my $member = $link->$object;
1232             $links .= $member->Id if $member;
1233         }
1234         $string .= $links;
1235         $string .= "\n";
1236     }
1237
1238     return $string;
1239 }
1240
1241 sub GetBaseTemplate {
1242     my $self = shift;
1243     my $t    = shift;
1244
1245     my $string;
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";
1262
1263     return $string;
1264 }
1265
1266 sub GetCreateTemplate {
1267     my $self = shift;
1268
1269     my $string;
1270
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";
1289
1290     foreach my $type ( keys %LINKTYPEMAP ) {
1291
1292         # don't display duplicates
1293         if (   $type eq "HasMember"
1294             || $type eq 'Members'
1295             || $type eq 'MemberOf' )
1296         {
1297             next;
1298         }
1299         $string .= "$type: \n";
1300     }
1301     return $string;
1302 }
1303
1304 sub UpdateWatchers {
1305     my $self   = shift;
1306     my $ticket = shift;
1307     my $args   = shift;
1308
1309     my @results;
1310
1311     foreach my $type qw(Requestor Cc AdminCc) {
1312         my $method  = $type . 'Addresses';
1313         my $oldaddr = $ticket->$method;
1314
1315         # Skip unless we have a defined field
1316         next unless defined $args->{$type};
1317         my $newaddr = $args->{$type};
1318
1319         my @old = split( /,\s*/, $oldaddr );
1320         my @new;
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.
1325             if (/^\S+@\S+$/) {
1326                 push @new, $_;
1327             } else {
1328                 # It doesn't look like an email address.  Try to load it.
1329                 my $user = RT::User->new($self->CurrentUser);
1330                 $user->Load($_);
1331                 if ($user->Id) {
1332                     push @new, $user->EmailAddress;
1333                 } else {
1334                     push @new, $_;
1335                 }
1336             }
1337         }
1338
1339         my %oldhash = map { $_ => 1 } @old;
1340         my %newhash = map { $_ => 1 } @new;
1341
1342         my @add    = grep( !defined $oldhash{$_}, @new );
1343         my @delete = grep( !defined $newhash{$_}, @old );
1344
1345         foreach (@add) {
1346             my ( $val, $msg ) = $ticket->AddWatcher(
1347                 Type  => $type,
1348                 Email => $_
1349             );
1350
1351             push @results,
1352                 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1353         }
1354
1355         foreach (@delete) {
1356             my ( $val, $msg ) = $ticket->DeleteWatcher(
1357                 Type  => $type,
1358                 Email => $_
1359             );
1360             push @results,
1361                 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1362         }
1363     }
1364     return @results;
1365 }
1366
1367 sub UpdateCustomFields {
1368     my $self   = shift;
1369     my $ticket = shift;
1370     my $args   = shift;
1371
1372     my @results;
1373     foreach my $arg (keys %{$args}) {
1374         next unless $arg =~ /^CustomField-(\d+)$/;
1375         my $cf = $1;
1376
1377         my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1378         $CustomFieldObj->LoadById($cf);
1379
1380         my @values;
1381         if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1382             @values = ($args->{$arg});
1383         } else {
1384             @values = split /\n/, $args->{$arg};
1385         }
1386         
1387         if ( ($CustomFieldObj->Type eq 'Freeform' 
1388               && ! $CustomFieldObj->SingleValue) ||
1389               $CustomFieldObj->Type =~ /text/i) {
1390             foreach my $val (@values) {
1391                 $val =~ s/\r//g;
1392             }
1393         }
1394
1395         foreach my $value (@values) {
1396             next unless length($value);
1397             my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1398                 Field => $cf,
1399                 Value => $value
1400             );
1401             push ( @results, $msg );
1402         }
1403     }
1404     return @results;
1405 }
1406
1407 sub PostProcess {
1408     my $self      = shift;
1409     my $links     = shift;
1410     my $postponed = shift;
1411
1412     # postprocessing: add links
1413
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) };
1418
1419         foreach my $type ( keys %LINKTYPEMAP ) {
1420             next unless ( defined $args{$type} );
1421             foreach my $link (
1422                 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1423             {
1424                 next unless $link;
1425
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;
1430
1431                 } elsif ( $link !~ m/^\d+$/ ) {
1432                     my $key = "create-$link";
1433                     if ( !exists $T::Tickets{$key} ) {
1434                         $RT::Logger->debug(
1435                             "Skipping $type link for $key (non-existent)");
1436                         next;
1437                     }
1438                     $RT::Logger->debug( "Building $type link for $link: "
1439                             . $T::Tickets{$key}->Id );
1440                     $link = $T::Tickets{$key}->Id;
1441                 } else {
1442                     $RT::Logger->debug("Building $type link for $link");
1443                 }
1444
1445                 my ( $wval, $wmsg ) = $ticket->AddLink(
1446                     Type => $LINKTYPEMAP{$type}->{'Type'},
1447                     $LINKTYPEMAP{$type}->{'Mode'} => $link,
1448                     Silent                        => 1
1449                 );
1450
1451                 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1452                     unless $wval;
1453
1454                 # push @non_fatal_errors, $wmsg unless ($wval);
1455             }
1456
1457         }
1458     }
1459
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};
1466     }
1467
1468 }
1469
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} );
1474
1475 1;
1476