1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
49 package RT::Action::CreateTickets;
50 use base 'RT::Action';
59 RT::Action::CreateTickets - Create one or more tickets according to an externally supplied template
63 ===Create-Ticket: codereview
64 Subject: Code review for {$Tickets{'TOP'}->Subject}
66 Content: Someone has created a ticket. you should review and approve it,
67 so they can finish their work
72 The CreateTickets ScripAction allows you to create automated workflows in RT,
73 creating new tickets in response to actions and conditions from other
78 CreateTickets uses the RT template configured in the scrip as a template
79 for an ordered set of tickets to create. The basic format is as follows:
81 ===Create-Ticket: identifier
94 As shown, you can put one or more C<===Create-Ticket:> sections in
95 a template. Each C<===Create-Ticket:> section is evaluated as its own
96 L<Text::Template> object, which means that you can embed snippets
97 of Perl inside the L<Text::Template> using C<{}> delimiters, but that
98 such sections absolutely can not span a C<===Create-Ticket:> boundary.
100 Note that each C<Value> must come right after the C<Param> on the same
101 line. The C<Content:> param can extend over multiple lines, but the text
102 of the first line must start right after C<Content:>. Don't try to start
103 your C<Content:> section with a newline.
105 After each ticket is created, it's stuffed into a hash called C<%Tickets>
106 making it available during the creation of other tickets during the
107 same ScripAction. The hash key for each ticket is C<create-[identifier]>,
108 where C<[identifier]> is the value you put after C<===Create-Ticket:>. The hash
109 is prepopulated with the ticket which triggered the ScripAction as
110 C<$Tickets{'TOP'}>. You can also access that ticket using the shorthand
115 ===Create-Ticket: codereview
116 Subject: Code review for {$Tickets{'TOP'}->Subject}
118 Content: Someone has created a ticket. you should review and approve it,
119 so they can finish their work
122 A convoluted example:
124 ===Create-Ticket: approval
125 { # Find out who the administrators of the group called "HR"
126 # of which the creator of this ticket is a member
129 my $groups = RT::Groups->new(RT->SystemUser);
130 $groups->LimitToUserDefinedGroups();
131 $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name");
132 $groups->WithMember($TransactionObj->CreatorObj->Id);
134 my $groupid = $groups->First->Id;
136 my $adminccs = RT::Users->new(RT->SystemUser);
137 $adminccs->WhoHaveRight(
138 Right => "AdminGroup",
139 Object =>$groups->First,
140 IncludeSystemRights => undef,
141 IncludeSuperusers => 0,
142 IncludeSubgroupMembers => 0,
146 while (my $admin = $adminccs->Next) {
147 push (@admins, $admin->EmailAddress);
152 AdminCc: {join ("\nAdminCc: ",@admins) }
155 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
157 Content-Type: text/plain
158 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
162 ===Create-Ticket: two
163 Subject: Manager approval
166 Refers-To: {$Tickets{"create-approval"}->Id}
168 Content-Type: text/plain
169 Content: Your approval is requred for this ticket, too.
172 As shown above, you can include a block with Perl code to set up some
173 values for the new tickets. If you want to access a variable in the
174 template section after the block, you must scope it with C<our> rather
175 than C<my>. Just as with other RT templates, you can also include
176 Perl code in the template sections using C<{}>.
178 =head2 Acceptable Fields
180 A complete list of acceptable fields:
182 * Queue => Name or id# of a queue
183 Subject => A text string
184 ! Status => A valid status. Defaults to 'new'
185 Due => Dates can be specified in seconds since the epoch
186 to be handled literally or in a semi-free textual
187 format which RT will attempt to parse.
191 Owner => Username or id of an RT user who can and should own
192 this ticket; forces the owner if necessary
193 + Requestor => Email address
194 + Cc => Email address
195 + AdminCc => Email address
196 + RequestorGroup => Group name
197 + CcGroup => Group name
198 + AdminCcGroup => Group name
211 Content => Content. Can extend to multiple lines. Everything
212 within a template after a Content: header is treated
213 as content until we hit a line containing only
215 ContentType => the content-type of the Content field. Defaults to
217 UpdateType => 'correspond' or 'comment'; used in conjunction with
218 'content' if this is an update. Defaults to
221 CustomField-<id#> => custom field value
222 CF-name => custom field value
223 CustomField-name => custom field value
225 Fields marked with an C<*> are required.
227 Fields marked with a C<+> may have multiple values, simply
228 by repeating the fieldname on a new line with an additional value.
230 Fields marked with a C<!> have processing postponed until after all
231 tickets in the same actions are created. Except for C<Status>, those
232 fields can also take a ticket name within the same action (i.e.
233 the identifiers after C<===Create-Ticket:>), instead of raw ticket ID
236 When parsed, field names are converted to lowercase and have hyphens stripped.
237 C<Refers-To>, C<RefersTo>, C<refersto>, C<refers-to> and C<r-e-f-er-s-tO> will
238 all be treated as the same thing.
285 #Do what we need to do and send it out.
289 # Create all the tickets we care about
290 return (1) unless $self->TicketObj->Type eq 'ticket';
292 $self->CreateByTemplate( $self->TicketObj );
293 $self->UpdateByTemplate( $self->TicketObj );
302 unless ( $self->TemplateObj ) {
303 $RT::Logger->warning("No template object handed to $self");
306 unless ( $self->TransactionObj ) {
307 $RT::Logger->warning("No transaction object handed to $self");
311 unless ( $self->TicketObj ) {
312 $RT::Logger->warning("No ticket object handed to $self");
317 if ( $self->TemplateObj->Type eq 'Perl' ) {
320 RT->Logger->info(sprintf(
321 "Template #%d is type %s. You most likely want to use a Perl template instead.",
322 $self->TemplateObj->id, $self->TemplateObj->Type
327 Content => $self->TemplateObj->Content,
328 _ActiveContent => $active,
336 sub CreateByTemplate {
340 $RT::Logger->debug("In CreateByTemplate");
344 # XXX: cargo cult programming that works. i'll be back.
346 local %T::Tickets = %T::Tickets;
347 local $T::TOP = $T::TOP;
348 local $T::ID = $T::ID;
349 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
350 local $T::TransactionObj = $self->TransactionObj;
353 my ( @links, @postponed );
354 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
355 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
358 $T::ID = $template_id;
359 @T::AllID = @{ $self->{'create_tickets'} };
361 ( $T::Tickets{$template_id}, $ticketargs )
362 = $self->ParseLines( $template_id, \@links, \@postponed );
364 # Now we have a %args to work with.
365 # Make sure we have at least the minimum set of
366 # reasonable data and do our thang
368 my ( $id, $transid, $msg )
369 = $T::Tickets{$template_id}->Create(%$ticketargs);
371 foreach my $res ( split( '\n', $msg ) ) {
373 $T::Tickets{$template_id}
374 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
378 if ( $self->TicketObj ) {
379 $msg = "Couldn't create related ticket $template_id for "
380 . $self->TicketObj->Id . " "
383 $msg = "Couldn't create ticket $template_id " . $msg;
386 $RT::Logger->error($msg);
390 $RT::Logger->debug("Assigned $template_id with $id");
391 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
393 && $T::Tickets{$template_id}->can('SetOriginObj');
397 $self->PostProcess( \@links, \@postponed );
402 sub UpdateByTemplate {
406 # XXX: cargo cult programming that works. i'll be back.
409 local %T::Tickets = %T::Tickets;
410 local $T::ID = $T::ID;
413 my ( @links, @postponed );
414 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
415 $RT::Logger->debug("Update Workflow: processing $template_id");
417 $T::ID = $template_id;
418 @T::AllID = @{ $self->{'update_tickets'} };
420 ( $T::Tickets{$template_id}, $ticketargs )
421 = $self->ParseLines( $template_id, \@links, \@postponed );
423 # Now we have a %args to work with.
424 # Make sure we have at least the minimum set of
425 # reasonable data and do our thang
442 my $id = $template_id;
443 $id =~ s/update-(\d+).*/$1/;
444 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
447 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
448 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
452 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
454 $template_id =~ m/^update-(.*)/;
455 my $base_id = "base-$1";
456 my $base = $self->{'templates'}->{$base_id};
460 $current =~ s/\n+$//;
462 # If we have no base template, set what we can.
463 if ( $base ne $current ) {
465 "Could not update ticket "
466 . $T::Tickets{$template_id}->Id
467 . ": Ticket has changed";
471 push @results, $T::Tickets{$template_id}->Update(
472 AttributesRef => \@attribs,
473 ARGSRef => $ticketargs
476 if ( $ticketargs->{'Owner'} ) {
477 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
478 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
482 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
485 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
487 next unless $ticketargs->{'MIMEObj'};
488 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
489 my ( $Transaction, $Description, $Object )
490 = $T::Tickets{$template_id}->Comment(
491 BccMessageTo => $ticketargs->{'Bcc'},
492 MIMEObj => $ticketargs->{'MIMEObj'},
493 TimeTaken => $ticketargs->{'TimeWorked'}
496 $T::Tickets{$template_id}
497 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
500 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
501 my ( $Transaction, $Description, $Object )
502 = $T::Tickets{$template_id}->Correspond(
503 BccMessageTo => $ticketargs->{'Bcc'},
504 MIMEObj => $ticketargs->{'MIMEObj'},
505 TimeTaken => $ticketargs->{'TimeWorked'}
508 $T::Tickets{$template_id}
509 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
515 $T::Tickets{$template_id}->loc(
516 "Update type was neither correspondence nor comment.")
518 . $T::Tickets{$template_id}->loc("Update not recorded.")
523 $self->PostProcess( \@links, \@postponed );
530 Takes (in order) template content, a default queue, a default requestor, and
531 active (a boolean flag).
533 Parses a template in the template content, defaulting queue and requestor if
534 unspecified in the template to the values provided as arguments.
536 If the active flag is true, then we'll use L<Text::Template> to parse the
537 templates, allowing you to embed active Perl in your templates.
547 _ActiveContent => undef,
551 if ( $args{'_ActiveContent'} ) {
552 $self->{'UsePerlTextTemplate'} = 1;
555 $self->{'UsePerlTextTemplate'} = 0;
558 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
559 $self->_ParseMultilineTemplate(%args);
560 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
561 $self->_ParseXSVTemplate(%args);
563 RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}");
567 =head2 _ParseMultilineTemplate
569 Parses mulitline templates. Things like:
571 ===Create-Ticket: ...
573 Takes the same arguments as L</Parse>.
577 sub _ParseMultilineTemplate {
584 my ( $queue, $requestor );
585 $RT::Logger->debug("Line: ===");
586 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
588 $RT::Logger->debug( "Line: " . utf8::is_utf8($line)
589 ? Encode::encode_utf8($line)
591 if ( $line =~ /^===/ ) {
592 if ( $template_id && !$queue && $args{'Queue'} ) {
593 $self->{'templates'}->{$template_id}
594 .= "Queue: $args{'Queue'}\n";
596 if ( $template_id && !$requestor && $args{'Requestor'} ) {
597 $self->{'templates'}->{$template_id}
598 .= "Requestor: $args{'Requestor'}\n";
603 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
604 $template_id = "create-$1";
605 $RT::Logger->debug("**** Create ticket: $template_id");
606 push @{ $self->{'create_tickets'} }, $template_id;
607 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
608 $template_id = "update-$1";
609 $RT::Logger->debug("**** Update ticket: $template_id");
610 push @{ $self->{'update_tickets'} }, $template_id;
611 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
612 $template_id = "base-$1";
613 $RT::Logger->debug("**** Base ticket: $template_id");
614 push @{ $self->{'base_tickets'} }, $template_id;
615 } elsif ( $line =~ /^===#.*$/ ) { # a comment
618 if ( $line =~ /^Queue:(.*)/i ) {
623 if ( !$value && $args{'Queue'} ) {
624 $value = $args{'Queue'};
625 $line = "Queue: $value";
628 if ( $line =~ /^Requestors?:(.*)/i ) {
633 if ( !$value && $args{'Requestor'} ) {
634 $value = $args{'Requestor'};
635 $line = "Requestor: $value";
638 $self->{'templates'}->{$template_id} .= $line . "\n";
641 if ( $template_id && !$queue && $args{'Queue'} ) {
642 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
648 my $template_id = shift;
650 my $postponed = shift;
652 my $content = $self->{'templates'}->{$template_id};
654 if ( $self->{'UsePerlTextTemplate'} ) {
657 "Workflow: evaluating\n$self->{templates}{$template_id}");
659 my $template = Text::Template->new(
665 $content = $template->fill_in(
668 $err = {@_}->{error};
672 $RT::Logger->debug("Workflow: yielding $content");
675 $RT::Logger->error( "Ticket creation failed: " . $err );
676 while ( my ( $k, $v ) = each %T::X ) {
678 "Eliminating $template_id from ${k}'s parents.");
679 delete $v->{$template_id};
685 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
689 my @lines = ( split( /\n/, $content ) );
690 while ( defined( my $line = shift @lines ) ) {
691 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
693 my $original_tag = $1;
694 my $tag = lc($original_tag);
696 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
698 $original_tags{$tag} = $original_tag;
700 if ( ref( $args{$tag} ) )
701 { #If it's an array, we want to push the value
702 push @{ $args{$tag} }, $value;
703 } elsif ( defined( $args{$tag} ) )
704 { #if we're about to get a second value, make it an array
705 $args{$tag} = [ $args{$tag}, $value ];
706 } else { #if there's nothing there, just set the value
707 $args{$tag} = $value;
710 if ( $tag =~ /^content$/i ) { #just build up the content
711 # convert it to an array
712 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
713 while ( defined( my $l = shift @lines ) ) {
714 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
715 push @{ $args{'content'} }, $l . "\n";
718 # if it's not content, strip leading and trailing spaces
720 $args{$tag} =~ s/^\s+//g;
721 $args{$tag} =~ s/\s+$//g;
724 ($tag =~ /^(requestor|cc|admincc)(group)?$/i
725 or grep {lc $_ eq $tag} keys %LINKTYPEMAP)
726 and $args{$tag} =~ /,/
728 $args{$tag} = [ split /,\s*/, $args{$tag} ];
734 foreach my $date (qw(due starts started resolved)) {
735 my $dateobj = RT::Date->new( $self->CurrentUser );
736 next unless $args{$date};
737 if ( $args{$date} =~ /^\d+$/ ) {
738 $dateobj->Set( Format => 'unix', Value => $args{$date} );
741 $dateobj->Set( Format => 'iso', Value => $args{$date} );
743 if ($@ or $dateobj->Unix <= 0) {
744 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
747 $args{$date} = $dateobj->ISO;
750 foreach my $role (qw(requestor cc admincc)) {
751 next unless my $value = $args{ $role . 'group' };
753 my $group = RT::Group->new( $self->CurrentUser );
754 $group->LoadUserDefinedGroup( $value );
755 unless ( $group->id ) {
756 $RT::Logger->error("Couldn't load group '$value'");
760 $args{ $role } = $args{ $role } ? [$args{ $role }] : []
761 unless ref $args{ $role };
762 push @{ $args{ $role } }, $group->PrincipalObj->id;
765 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
768 $args{'type'} ||= 'ticket';
771 Queue => $args{'queue'},
772 Subject => $args{'subject'},
773 Status => $args{'status'} || 'new',
775 Starts => $args{'starts'},
776 Started => $args{'started'},
777 Resolved => $args{'resolved'},
778 Owner => $args{'owner'},
779 Requestor => $args{'requestor'},
781 AdminCc => $args{'admincc'},
782 TimeWorked => $args{'timeworked'},
783 TimeEstimated => $args{'timeestimated'},
784 TimeLeft => $args{'timeleft'},
785 InitialPriority => $args{'initialpriority'} || 0,
786 FinalPriority => $args{'finalpriority'} || 0,
787 SquelchMailTo => $args{'squelchmailto'},
788 Type => $args{'type'},
792 if ( $args{content} ) {
793 my $mimeobj = MIME::Entity->new();
795 Type => $args{'contenttype'} || 'text/plain',
796 Data => $args{'content'}
798 $ticketargs{MIMEObj} = $mimeobj;
799 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
802 foreach my $tag ( keys(%args) ) {
803 # if the tag was added later, skip it
804 my $orig_tag = $original_tags{$tag} or next;
805 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
806 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
807 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
808 my $cf = RT::CustomField->new( $self->CurrentUser );
809 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
810 $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
812 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
813 } elsif ($orig_tag) {
814 my $cf = RT::CustomField->new( $self->CurrentUser );
815 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
816 $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
818 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
823 $self->GetDeferred( \%args, $template_id, $links, $postponed );
825 return $TicketObj, \%ticketargs;
829 =head2 _ParseXSVTemplate
831 Parses a tab or comma delimited template. Should only ever be called by
836 sub _ParseXSVTemplate {
840 use Regexp::Common qw(delimited);
841 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
844 if ( $first =~ /\t/ ) {
849 my @fields = split( /$delimiter/, $first );
851 my $delimiter_re = qr[$delimiter];
852 my $justquoted = qr[$RE{quoted}];
854 # Used to generate automatic template ids
859 $content =~ s/^(\s*\r?\n)+//;
861 # Keep track of Queue and Requestor, so we can provide defaults
865 # The template for this line
868 # What column we're on
871 # If the last iteration was the end of the line
878 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
881 # Strip off quotes, if they exist
883 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
884 substr( $value, 0, 1 ) = "";
885 substr( $value, -1, 1 ) = "";
888 # What column is this?
889 my $field = $fields[$i++];
890 next COLUMN unless $field =~ /\S/;
894 if ( $field =~ /^id$/i ) {
895 # Special case if this is the ID column
896 if ( $value =~ /^\d+$/ ) {
897 $template_id = 'update-' . $value;
898 push @{ $self->{'update_tickets'} }, $template_id;
899 } elsif ( $value =~ /^#base-(\d+)$/ ) {
900 $template_id = 'base-' . $1;
901 push @{ $self->{'base_tickets'} }, $template_id;
902 } elsif ( $value =~ /\S/ ) {
903 $template_id = 'create-' . $value;
904 push @{ $self->{'create_tickets'} }, $template_id;
908 if ( $field =~ /^Body$/i
909 || $field =~ /^Data$/i
910 || $field =~ /^Message$/i )
913 } elsif ( $field =~ /^Summary$/i ) {
915 } elsif ( $field =~ /^Queue$/i ) {
916 # Note that we found a queue
918 $value ||= $args{'Queue'};
919 } elsif ( $field =~ /^Requestors?$/i ) {
920 $field = 'Requestor'; # Remove plural
921 # Note that we found a requestor
923 $value ||= $args{'Requestor'};
926 # Tack onto the end of the template
927 $template .= $field . ": ";
928 $template .= (defined $value ? $value : "");
930 $template .= "ENDOFCONTENT\n"
931 if $field =~ /^Content$/i;
936 next unless $template;
938 # If we didn't find a queue of requestor, tack on the defaults
939 if ( !$queue && $args{'Queue'} ) {
940 $template .= "Queue: $args{'Queue'}\n";
942 if ( !$requestor && $args{'Requestor'} ) {
943 $template .= "Requestor: $args{'Requestor'}\n";
946 # If we never found an ID, come up with one
947 unless ($template_id) {
948 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
949 $template_id = "create-auto-$autoid";
950 # Also, it's a ticket to create
951 push @{ $self->{'create_tickets'} }, $template_id;
954 # Save the template we generated
955 $self->{'templates'}->{$template_id} = $template;
965 my $postponed = shift;
967 # Deferred processing
971 { DependsOn => $args->{'dependson'},
972 DependedOnBy => $args->{'dependedonby'},
973 RefersTo => $args->{'refersto'},
974 ReferredToBy => $args->{'referredtoby'},
975 Children => $args->{'children'},
976 Parents => $args->{'parents'},
982 # Status is postponed so we don't violate dependencies
983 $id, { Status => $args->{'status'}, }
987 sub GetUpdateTemplate {
992 $string .= "Queue: " . $t->QueueObj->Name . "\n";
993 $string .= "Subject: " . $t->Subject . "\n";
994 $string .= "Status: " . $t->Status . "\n";
995 $string .= "UpdateType: correspond\n";
996 $string .= "Content: \n";
997 $string .= "ENDOFCONTENT\n";
998 $string .= "Due: " . $t->DueObj->AsString . "\n";
999 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1000 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1001 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1002 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
1003 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1004 $string .= "Cc: " . $t->CcAddresses . "\n";
1005 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1006 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1007 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1008 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1009 $string .= "InitialPriority: " . $t->Priority . "\n";
1010 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1012 foreach my $type ( sort keys %LINKTYPEMAP ) {
1014 # don't display duplicates
1015 if ( $type eq "HasMember"
1016 || $type eq "Members"
1017 || $type eq "MemberOf" )
1021 $string .= "$type: ";
1023 my $mode = $LINKTYPEMAP{$type}->{Mode};
1024 my $method = $LINKTYPEMAP{$type}->{Type};
1027 while ( my $link = $t->$method->Next ) {
1028 $links .= ", " if $links;
1030 my $object = $mode . "Obj";
1031 my $member = $link->$object;
1032 $links .= $member->Id if $member;
1041 sub GetBaseTemplate {
1046 $string .= "Queue: " . $t->Queue . "\n";
1047 $string .= "Subject: " . $t->Subject . "\n";
1048 $string .= "Status: " . $t->Status . "\n";
1049 $string .= "Due: " . $t->DueObj->Unix . "\n";
1050 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1051 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1052 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1053 $string .= "Owner: " . $t->Owner . "\n";
1054 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1055 $string .= "Cc: " . $t->CcAddresses . "\n";
1056 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1057 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1058 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1059 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1060 $string .= "InitialPriority: " . $t->Priority . "\n";
1061 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1066 sub GetCreateTemplate {
1071 $string .= "Queue: General\n";
1072 $string .= "Subject: \n";
1073 $string .= "Status: new\n";
1074 $string .= "Content: \n";
1075 $string .= "ENDOFCONTENT\n";
1076 $string .= "Due: \n";
1077 $string .= "Starts: \n";
1078 $string .= "Started: \n";
1079 $string .= "Resolved: \n";
1080 $string .= "Owner: \n";
1081 $string .= "Requestor: \n";
1082 $string .= "Cc: \n";
1083 $string .= "AdminCc:\n";
1084 $string .= "TimeWorked: \n";
1085 $string .= "TimeEstimated: \n";
1086 $string .= "TimeLeft: \n";
1087 $string .= "InitialPriority: \n";
1088 $string .= "FinalPriority: \n";
1090 foreach my $type ( keys %LINKTYPEMAP ) {
1092 # don't display duplicates
1093 if ( $type eq "HasMember"
1094 || $type eq 'Members'
1095 || $type eq 'MemberOf' )
1099 $string .= "$type: \n";
1104 sub UpdateWatchers {
1111 foreach my $type (qw(Requestor Cc AdminCc)) {
1112 my $method = $type . 'Addresses';
1113 my $oldaddr = $ticket->$method;
1115 # Skip unless we have a defined field
1116 next unless defined $args->{$type};
1117 my $newaddr = $args->{$type};
1119 my @old = split( /,\s*/, $oldaddr );
1121 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1122 # Sometimes these are email addresses, sometimes they're
1123 # users. Try to guess which is which, as we want to deal
1124 # with email addresses if at all possible.
1128 # It doesn't look like an email address. Try to load it.
1129 my $user = RT::User->new($self->CurrentUser);
1132 push @new, $user->EmailAddress;
1139 my %oldhash = map { $_ => 1 } @old;
1140 my %newhash = map { $_ => 1 } @new;
1142 my @add = grep( !defined $oldhash{$_}, @new );
1143 my @delete = grep( !defined $newhash{$_}, @old );
1146 my ( $val, $msg ) = $ticket->AddWatcher(
1152 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1156 my ( $val, $msg ) = $ticket->DeleteWatcher(
1161 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1167 sub UpdateCustomFields {
1173 foreach my $arg (keys %{$args}) {
1174 next unless $arg =~ /^CustomField-(\d+)$/;
1177 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1178 $CustomFieldObj->SetContextObject( $ticket );
1179 $CustomFieldObj->LoadById($cf);
1182 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1183 @values = ($args->{$arg});
1185 @values = split /\n/, $args->{$arg};
1188 if ( ($CustomFieldObj->Type eq 'Freeform'
1189 && ! $CustomFieldObj->SingleValue) ||
1190 $CustomFieldObj->Type =~ /text/i) {
1191 foreach my $val (@values) {
1196 foreach my $value (@values) {
1197 next unless length($value);
1198 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1202 push ( @results, $msg );
1211 my $postponed = shift;
1213 # postprocessing: add links
1215 while ( my $template_id = shift(@$links) ) {
1216 my $ticket = $T::Tickets{$template_id};
1217 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1218 my %args = %{ shift(@$links) };
1220 foreach my $type ( keys %LINKTYPEMAP ) {
1221 next unless ( defined $args{$type} );
1223 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1227 if ( $link =~ /^TOP$/i ) {
1228 $RT::Logger->debug( "Building $type link for $link: "
1229 . $T::Tickets{TOP}->Id );
1230 $link = $T::Tickets{TOP}->Id;
1232 } elsif ( $link !~ m/^\d+$/ ) {
1233 my $key = "create-$link";
1234 if ( !exists $T::Tickets{$key} ) {
1236 "Skipping $type link for $key (non-existent)");
1239 $RT::Logger->debug( "Building $type link for $link: "
1240 . $T::Tickets{$key}->Id );
1241 $link = $T::Tickets{$key}->Id;
1243 $RT::Logger->debug("Building $type link for $link");
1246 my ( $wval, $wmsg ) = $ticket->AddLink(
1247 Type => $LINKTYPEMAP{$type}->{'Type'},
1248 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1252 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1255 # push @non_fatal_errors, $wmsg unless ($wval);
1261 # postponed actions -- Status only, currently
1262 while ( my $template_id = shift(@$postponed) ) {
1263 my $ticket = $T::Tickets{$template_id};
1264 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1265 my %args = %{ shift(@$postponed) };
1266 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1273 my $queues = RT::Queues->new($self->CurrentUser);
1276 while (my $queue = $queues->Next) {
1277 push @names, $queue->Id, $queue->Name;
1282 'label' => 'In queue',
1284 'options' => \@names
1289 RT::Base->_ImportOverlays();