1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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';
60 RT::Action::CreateTickets - Create one or more tickets according to an externally supplied template
64 ===Create-Ticket: codereview
65 Subject: Code review for {$Tickets{'TOP'}->Subject}
67 Content: Someone has created a ticket. you should review and approve it,
68 so they can finish their work
73 The CreateTickets ScripAction allows you to create automated workflows in RT,
74 creating new tickets in response to actions and conditions from other
79 CreateTickets uses the RT template configured in the scrip as a template
80 for an ordered set of tickets to create. The basic format is as follows:
82 ===Create-Ticket: identifier
95 As shown, you can put one or more C<===Create-Ticket:> sections in
96 a template. Each C<===Create-Ticket:> section is evaluated as its own
97 L<Text::Template> object, which means that you can embed snippets
98 of Perl inside the L<Text::Template> using C<{}> delimiters, but that
99 such sections absolutely can not span a C<===Create-Ticket:> boundary.
101 Note that each C<Value> must come right after the C<Param> on the same
102 line. The C<Content:> param can extend over multiple lines, but the text
103 of the first line must start right after C<Content:>. Don't try to start
104 your C<Content:> section with a newline.
106 After each ticket is created, it's stuffed into a hash called C<%Tickets>
107 making it available during the creation of other tickets during the
108 same ScripAction. The hash key for each ticket is C<create-[identifier]>,
109 where C<[identifier]> is the value you put after C<===Create-Ticket:>. The hash
110 is prepopulated with the ticket which triggered the ScripAction as
111 C<$Tickets{'TOP'}>. You can also access that ticket using the shorthand
116 ===Create-Ticket: codereview
117 Subject: Code review for {$Tickets{'TOP'}->Subject}
119 Content: Someone has created a ticket. you should review and approve it,
120 so they can finish their work
123 A convoluted example:
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, CASESENSITIVE => 0);
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
167 Refers-To: {$Tickets{"create-approval"}->Id}
169 Content-Type: text/plain
170 Content: Your approval is requred for this ticket, too.
173 As shown above, you can include a block with Perl code to set up some
174 values for the new tickets. If you want to access a variable in the
175 template section after the block, you must scope it with C<our> rather
176 than C<my>. Just as with other RT templates, you can also include
177 Perl code in the template sections using C<{}>.
179 =head2 Acceptable Fields
181 A complete list of acceptable fields:
183 * Queue => Name or id# of a queue
184 Subject => A text string
185 ! Status => A valid status. Defaults to 'new'
186 Due => Dates can be specified in seconds since the epoch
187 to be handled literally or in a semi-free textual
188 format which RT will attempt to parse.
192 Owner => Username or id of an RT user who can and should own
193 this ticket; forces the owner if necessary
194 + Requestor => Email address
195 + Cc => Email address
196 + AdminCc => Email address
197 + RequestorGroup => Group name
198 + CcGroup => Group name
199 + AdminCcGroup => Group name
212 Content => Content. Can extend to multiple lines. Everything
213 within a template after a Content: header is treated
214 as content until we hit a line containing only
216 ContentType => the content-type of the Content field. Defaults to
218 UpdateType => 'correspond' or 'comment'; used in conjunction with
219 'content' if this is an update. Defaults to
222 CustomField-<id#> => custom field value
223 CF-name => custom field value
224 CustomField-name => custom field value
226 Fields marked with an C<*> are required.
228 Fields marked with a C<+> may have multiple values, simply
229 by repeating the fieldname on a new line with an additional value.
231 Fields marked with a C<!> have processing postponed until after all
232 tickets in the same actions are created. Except for C<Status>, those
233 fields can also take a ticket name within the same action (i.e.
234 the identifiers after C<===Create-Ticket:>), instead of raw ticket ID
237 When parsed, field names are converted to lowercase and have hyphens stripped.
238 C<Refers-To>, C<RefersTo>, C<refersto>, C<refers-to> and C<r-e-f-er-s-tO> will
239 all be treated as the same thing.
245 #Do what we need to do and send it out.
249 # Create all the tickets we care about
250 return (1) unless $self->TicketObj->Type eq 'ticket';
252 $self->CreateByTemplate( $self->TicketObj );
253 $self->UpdateByTemplate( $self->TicketObj );
262 unless ( $self->TemplateObj ) {
263 $RT::Logger->warning("No template object handed to $self");
266 unless ( $self->TransactionObj ) {
267 $RT::Logger->warning("No transaction object handed to $self");
271 unless ( $self->TicketObj ) {
272 $RT::Logger->warning("No ticket object handed to $self");
277 if ( $self->TemplateObj->Type eq 'Perl' ) {
280 RT->Logger->info(sprintf(
281 "Template #%d is type %s. You most likely want to use a Perl template instead.",
282 $self->TemplateObj->id, $self->TemplateObj->Type
287 Content => $self->TemplateObj->Content,
288 _ActiveContent => $active,
296 sub CreateByTemplate {
300 $RT::Logger->debug("In CreateByTemplate");
304 # XXX: cargo cult programming that works. i'll be back.
306 local %T::Tickets = %T::Tickets;
307 local $T::TOP = $T::TOP;
308 local $T::ID = $T::ID;
309 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
310 local $T::TransactionObj = $self->TransactionObj;
313 my ( @links, @postponed );
314 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
315 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
318 $T::ID = $template_id;
319 @T::AllID = @{ $self->{'create_tickets'} };
321 ( $T::Tickets{$template_id}, $ticketargs )
322 = $self->ParseLines( $template_id, \@links, \@postponed );
324 # Now we have a %args to work with.
325 # Make sure we have at least the minimum set of
326 # reasonable data and do our thang
328 my ( $id, $transid, $msg )
329 = $T::Tickets{$template_id}->Create(%$ticketargs);
331 foreach my $res ( split( '\n', $msg ) ) {
333 $T::Tickets{$template_id}
334 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
338 if ( $self->TicketObj ) {
339 $msg = "Couldn't create related ticket $template_id for "
340 . $self->TicketObj->Id . " "
343 $msg = "Couldn't create ticket $template_id " . $msg;
346 $RT::Logger->error($msg);
350 $RT::Logger->debug("Assigned $template_id with $id");
353 $self->PostProcess( \@links, \@postponed );
358 sub UpdateByTemplate {
362 # XXX: cargo cult programming that works. i'll be back.
365 local %T::Tickets = %T::Tickets;
366 local $T::ID = $T::ID;
369 my ( @links, @postponed );
370 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
371 $RT::Logger->debug("Update Workflow: processing $template_id");
373 $T::ID = $template_id;
374 @T::AllID = @{ $self->{'update_tickets'} };
376 ( $T::Tickets{$template_id}, $ticketargs )
377 = $self->ParseLines( $template_id, \@links, \@postponed );
379 # Now we have a %args to work with.
380 # Make sure we have at least the minimum set of
381 # reasonable data and do our thang
398 my $id = $template_id;
399 $id =~ s/update-(\d+).*/$1/;
400 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
403 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
404 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
408 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
410 $template_id =~ m/^update-(.*)/;
411 my $base_id = "base-$1";
412 my $base = $self->{'templates'}->{$base_id};
416 $current =~ s/\n+$//;
418 # If we have no base template, set what we can.
419 if ( $base ne $current ) {
421 "Could not update ticket "
422 . $T::Tickets{$template_id}->Id
423 . ": Ticket has changed";
427 push @results, $T::Tickets{$template_id}->Update(
428 AttributesRef => \@attribs,
429 ARGSRef => $ticketargs
432 if ( $ticketargs->{'Owner'} ) {
433 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
434 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
438 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
441 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
443 next unless $ticketargs->{'MIMEObj'};
444 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
445 my ( $Transaction, $Description, $Object )
446 = $T::Tickets{$template_id}->Comment(
447 BccMessageTo => $ticketargs->{'Bcc'},
448 MIMEObj => $ticketargs->{'MIMEObj'},
449 TimeTaken => $ticketargs->{'TimeWorked'}
452 $T::Tickets{$template_id}
453 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
456 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
457 my ( $Transaction, $Description, $Object )
458 = $T::Tickets{$template_id}->Correspond(
459 BccMessageTo => $ticketargs->{'Bcc'},
460 MIMEObj => $ticketargs->{'MIMEObj'},
461 TimeTaken => $ticketargs->{'TimeWorked'}
464 $T::Tickets{$template_id}
465 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
471 $T::Tickets{$template_id}->loc(
472 "Update type was neither correspondence nor comment.")
474 . $T::Tickets{$template_id}->loc("Update not recorded.")
479 $self->PostProcess( \@links, \@postponed );
486 Takes (in order) template content, a default queue, a default requestor, and
487 active (a boolean flag).
489 Parses a template in the template content, defaulting queue and requestor if
490 unspecified in the template to the values provided as arguments.
492 If the active flag is true, then we'll use L<Text::Template> to parse the
493 templates, allowing you to embed active Perl in your templates.
503 _ActiveContent => undef,
507 if ( $args{'_ActiveContent'} ) {
508 $self->{'UsePerlTextTemplate'} = 1;
511 $self->{'UsePerlTextTemplate'} = 0;
514 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
515 $self->_ParseMultilineTemplate(%args);
516 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
517 $self->_ParseXSVTemplate(%args);
519 RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}");
523 =head2 _ParseMultilineTemplate
525 Parses mulitline templates. Things like:
527 ===Create-Ticket: ...
529 Takes the same arguments as L</Parse>.
533 sub _ParseMultilineTemplate {
540 my ( $queue, $requestor );
541 $RT::Logger->debug("Line: ===");
542 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
544 $RT::Logger->debug( "Line: " . utf8::is_utf8($line)
545 ? Encode::encode_utf8($line)
547 if ( $line =~ /^===/ ) {
548 if ( $template_id && !$queue && $args{'Queue'} ) {
549 $self->{'templates'}->{$template_id}
550 .= "Queue: $args{'Queue'}\n";
552 if ( $template_id && !$requestor && $args{'Requestor'} ) {
553 $self->{'templates'}->{$template_id}
554 .= "Requestor: $args{'Requestor'}\n";
559 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
560 $template_id = "create-$1";
561 $RT::Logger->debug("**** Create ticket: $template_id");
562 push @{ $self->{'create_tickets'} }, $template_id;
563 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
564 $template_id = "update-$1";
565 $RT::Logger->debug("**** Update ticket: $template_id");
566 push @{ $self->{'update_tickets'} }, $template_id;
567 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
568 $template_id = "base-$1";
569 $RT::Logger->debug("**** Base ticket: $template_id");
570 push @{ $self->{'base_tickets'} }, $template_id;
571 } elsif ( $line =~ /^===#.*$/ ) { # a comment
574 if ( $line =~ /^Queue:(.*)/i ) {
579 if ( !$value && $args{'Queue'} ) {
580 $value = $args{'Queue'};
581 $line = "Queue: $value";
584 if ( $line =~ /^Requestors?:(.*)/i ) {
589 if ( !$value && $args{'Requestor'} ) {
590 $value = $args{'Requestor'};
591 $line = "Requestor: $value";
594 $self->{'templates'}->{$template_id} .= $line . "\n";
597 if ( $template_id && !$queue && $args{'Queue'} ) {
598 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
604 my $template_id = shift;
606 my $postponed = shift;
608 my $content = $self->{'templates'}->{$template_id};
610 if ( $self->{'UsePerlTextTemplate'} ) {
613 "Workflow: evaluating\n$self->{templates}{$template_id}");
615 my $template = Text::Template->new(
621 $content = $template->fill_in(
624 $err = {@_}->{error};
628 $RT::Logger->debug("Workflow: yielding $content");
631 $RT::Logger->error( "Ticket creation failed: " . $err );
636 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
640 my @lines = ( split( /\n/, $content ) );
641 while ( defined( my $line = shift @lines ) ) {
642 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
644 my $original_tag = $1;
645 my $tag = lc($original_tag);
647 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
649 $original_tags{$tag} = $original_tag;
651 if ( ref( $args{$tag} ) )
652 { #If it's an array, we want to push the value
653 push @{ $args{$tag} }, $value;
654 } elsif ( defined( $args{$tag} ) )
655 { #if we're about to get a second value, make it an array
656 $args{$tag} = [ $args{$tag}, $value ];
657 } else { #if there's nothing there, just set the value
658 $args{$tag} = $value;
661 if ( $tag =~ /^content$/i ) { #just build up the content
662 # convert it to an array
663 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
664 while ( defined( my $l = shift @lines ) ) {
665 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
666 push @{ $args{'content'} }, $l . "\n";
669 # if it's not content, strip leading and trailing spaces
671 $args{$tag} =~ s/^\s+//g;
672 $args{$tag} =~ s/\s+$//g;
675 ($tag =~ /^(requestor|cc|admincc)(group)?$/i
676 or grep {lc $_ eq $tag} keys %RT::Link::TYPEMAP)
677 and $args{$tag} =~ /,/
679 $args{$tag} = [ split /,\s*/, $args{$tag} ];
685 foreach my $date (qw(due starts started resolved)) {
686 my $dateobj = RT::Date->new( $self->CurrentUser );
687 next unless $args{$date};
688 if ( $args{$date} =~ /^\d+$/ ) {
689 $dateobj->Set( Format => 'unix', Value => $args{$date} );
692 $dateobj->Set( Format => 'iso', Value => $args{$date} );
694 if ($@ or $dateobj->Unix <= 0) {
695 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
698 $args{$date} = $dateobj->ISO;
701 foreach my $role (qw(requestor cc admincc)) {
702 next unless my $value = $args{ $role . 'group' };
704 my $group = RT::Group->new( $self->CurrentUser );
705 $group->LoadUserDefinedGroup( $value );
706 unless ( $group->id ) {
707 $RT::Logger->error("Couldn't load group '$value'");
711 $args{ $role } = $args{ $role } ? [$args{ $role }] : []
712 unless ref $args{ $role };
713 push @{ $args{ $role } }, $group->PrincipalObj->id;
716 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
719 $args{'type'} ||= 'ticket';
722 Queue => $args{'queue'},
723 Subject => $args{'subject'},
724 Status => $args{'status'} || 'new',
726 Starts => $args{'starts'},
727 Started => $args{'started'},
728 Resolved => $args{'resolved'},
729 Owner => $args{'owner'},
730 Requestor => $args{'requestor'},
732 AdminCc => $args{'admincc'},
733 TimeWorked => $args{'timeworked'},
734 TimeEstimated => $args{'timeestimated'},
735 TimeLeft => $args{'timeleft'},
736 InitialPriority => $args{'initialpriority'} || 0,
737 FinalPriority => $args{'finalpriority'} || 0,
738 SquelchMailTo => $args{'squelchmailto'},
739 Type => $args{'type'},
742 if ( $args{content} ) {
743 my $mimeobj = MIME::Entity->new();
745 Type => $args{'contenttype'} || 'text/plain',
746 Data => $args{'content'}
748 $ticketargs{MIMEObj} = $mimeobj;
749 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
752 foreach my $tag ( keys(%args) ) {
753 # if the tag was added later, skip it
754 my $orig_tag = $original_tags{$tag} or next;
755 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
756 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
757 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
758 my $cf = RT::CustomField->new( $self->CurrentUser );
759 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
760 $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
762 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
763 } elsif ($orig_tag) {
764 my $cf = RT::CustomField->new( $self->CurrentUser );
765 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
766 $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
768 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
773 $self->GetDeferred( \%args, $template_id, $links, $postponed );
775 return $TicketObj, \%ticketargs;
779 =head2 _ParseXSVTemplate
781 Parses a tab or comma delimited template. Should only ever be called by
786 sub _ParseXSVTemplate {
790 use Regexp::Common qw(delimited);
791 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
794 if ( $first =~ /\t/ ) {
799 my @fields = split( /$delimiter/, $first );
801 my $delimiter_re = qr[$delimiter];
802 my $justquoted = qr[$RE{quoted}];
804 # Used to generate automatic template ids
809 $content =~ s/^(\s*\r?\n)+//;
811 # Keep track of Queue and Requestor, so we can provide defaults
815 # The template for this line
818 # What column we're on
821 # If the last iteration was the end of the line
828 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
831 # Strip off quotes, if they exist
833 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
834 substr( $value, 0, 1 ) = "";
835 substr( $value, -1, 1 ) = "";
838 # What column is this?
839 my $field = $fields[$i++];
840 next COLUMN unless $field =~ /\S/;
844 if ( $field =~ /^id$/i ) {
845 # Special case if this is the ID column
846 if ( $value =~ /^\d+$/ ) {
847 $template_id = 'update-' . $value;
848 push @{ $self->{'update_tickets'} }, $template_id;
849 } elsif ( $value =~ /^#base-(\d+)$/ ) {
850 $template_id = 'base-' . $1;
851 push @{ $self->{'base_tickets'} }, $template_id;
852 } elsif ( $value =~ /\S/ ) {
853 $template_id = 'create-' . $value;
854 push @{ $self->{'create_tickets'} }, $template_id;
858 if ( $field =~ /^Body$/i
859 || $field =~ /^Data$/i
860 || $field =~ /^Message$/i )
863 } elsif ( $field =~ /^Summary$/i ) {
865 } elsif ( $field =~ /^Queue$/i ) {
866 # Note that we found a queue
868 $value ||= $args{'Queue'};
869 } elsif ( $field =~ /^Requestors?$/i ) {
870 $field = 'Requestor'; # Remove plural
871 # Note that we found a requestor
873 $value ||= $args{'Requestor'};
876 # Tack onto the end of the template
877 $template .= $field . ": ";
878 $template .= (defined $value ? $value : "");
880 $template .= "ENDOFCONTENT\n"
881 if $field =~ /^Content$/i;
886 next unless $template;
888 # If we didn't find a queue of requestor, tack on the defaults
889 if ( !$queue && $args{'Queue'} ) {
890 $template .= "Queue: $args{'Queue'}\n";
892 if ( !$requestor && $args{'Requestor'} ) {
893 $template .= "Requestor: $args{'Requestor'}\n";
896 # If we never found an ID, come up with one
897 unless ($template_id) {
898 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
899 $template_id = "create-auto-$autoid";
900 # Also, it's a ticket to create
901 push @{ $self->{'create_tickets'} }, $template_id;
904 # Save the template we generated
905 $self->{'templates'}->{$template_id} = $template;
915 my $postponed = shift;
917 # Deferred processing
921 { DependsOn => $args->{'dependson'},
922 DependedOnBy => $args->{'dependedonby'},
923 RefersTo => $args->{'refersto'},
924 ReferredToBy => $args->{'referredtoby'},
925 Children => $args->{'children'},
926 Parents => $args->{'parents'},
932 # Status is postponed so we don't violate dependencies
933 $id, { Status => $args->{'status'}, }
937 sub GetUpdateTemplate {
942 $string .= "Queue: " . $t->QueueObj->Name . "\n";
943 $string .= "Subject: " . $t->Subject . "\n";
944 $string .= "Status: " . $t->Status . "\n";
945 $string .= "UpdateType: correspond\n";
946 $string .= "Content: \n";
947 $string .= "ENDOFCONTENT\n";
948 $string .= "Due: " . $t->DueObj->AsString . "\n";
949 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
950 $string .= "Started: " . $t->StartedObj->AsString . "\n";
951 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
952 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
953 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
954 $string .= "Cc: " . $t->CcAddresses . "\n";
955 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
956 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
957 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
958 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
959 $string .= "InitialPriority: " . $t->Priority . "\n";
960 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
962 foreach my $type ( RT::Link->DisplayTypes ) {
963 $string .= "$type: ";
965 my $mode = $RT::Link::TYPEMAP{$type}->{Mode};
966 my $method = $RT::Link::TYPEMAP{$type}->{Type};
969 while ( my $link = $t->$method->Next ) {
970 $links .= ", " if $links;
972 my $object = $mode . "Obj";
973 my $member = $link->$object;
974 $links .= $member->Id if $member;
983 sub GetBaseTemplate {
988 $string .= "Queue: " . $t->Queue . "\n";
989 $string .= "Subject: " . $t->Subject . "\n";
990 $string .= "Status: " . $t->Status . "\n";
991 $string .= "Due: " . $t->DueObj->Unix . "\n";
992 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
993 $string .= "Started: " . $t->StartedObj->Unix . "\n";
994 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
995 $string .= "Owner: " . $t->Owner . "\n";
996 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
997 $string .= "Cc: " . $t->CcAddresses . "\n";
998 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
999 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1000 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1001 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1002 $string .= "InitialPriority: " . $t->Priority . "\n";
1003 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1008 sub GetCreateTemplate {
1013 $string .= "Queue: General\n";
1014 $string .= "Subject: \n";
1015 $string .= "Status: new\n";
1016 $string .= "Content: \n";
1017 $string .= "ENDOFCONTENT\n";
1018 $string .= "Due: \n";
1019 $string .= "Starts: \n";
1020 $string .= "Started: \n";
1021 $string .= "Resolved: \n";
1022 $string .= "Owner: \n";
1023 $string .= "Requestor: \n";
1024 $string .= "Cc: \n";
1025 $string .= "AdminCc:\n";
1026 $string .= "TimeWorked: \n";
1027 $string .= "TimeEstimated: \n";
1028 $string .= "TimeLeft: \n";
1029 $string .= "InitialPriority: \n";
1030 $string .= "FinalPriority: \n";
1032 foreach my $type ( RT::Link->DisplayTypes ) {
1033 $string .= "$type: \n";
1038 sub UpdateWatchers {
1045 foreach my $type (qw(Requestor Cc AdminCc)) {
1046 my $method = $type . 'Addresses';
1047 my $oldaddr = $ticket->$method;
1049 # Skip unless we have a defined field
1050 next unless defined $args->{$type};
1051 my $newaddr = $args->{$type};
1053 my @old = split( /,\s*/, $oldaddr );
1055 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1056 # Sometimes these are email addresses, sometimes they're
1057 # users. Try to guess which is which, as we want to deal
1058 # with email addresses if at all possible.
1062 # It doesn't look like an email address. Try to load it.
1063 my $user = RT::User->new($self->CurrentUser);
1066 push @new, $user->EmailAddress;
1073 my %oldhash = map { $_ => 1 } @old;
1074 my %newhash = map { $_ => 1 } @new;
1076 my @add = grep( !defined $oldhash{$_}, @new );
1077 my @delete = grep( !defined $newhash{$_}, @old );
1080 my ( $val, $msg ) = $ticket->AddWatcher(
1086 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1090 my ( $val, $msg ) = $ticket->DeleteWatcher(
1095 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1101 sub UpdateCustomFields {
1107 foreach my $arg (keys %{$args}) {
1108 next unless $arg =~ /^CustomField-(\d+)$/;
1111 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1112 $CustomFieldObj->SetContextObject( $ticket );
1113 $CustomFieldObj->LoadById($cf);
1116 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1117 @values = ($args->{$arg});
1119 @values = split /\n/, $args->{$arg};
1122 if ( ($CustomFieldObj->Type eq 'Freeform'
1123 && ! $CustomFieldObj->SingleValue) ||
1124 $CustomFieldObj->Type =~ /text/i) {
1125 foreach my $val (@values) {
1130 foreach my $value (@values) {
1131 next unless length($value);
1132 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1136 push ( @results, $msg );
1145 my $postponed = shift;
1147 # postprocessing: add links
1149 while ( my $template_id = shift(@$links) ) {
1150 my $ticket = $T::Tickets{$template_id};
1151 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1152 my %args = %{ shift(@$links) };
1154 foreach my $type ( keys %RT::Link::TYPEMAP ) {
1155 next unless ( defined $args{$type} );
1157 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1161 if ( $link =~ /^TOP$/i ) {
1162 $RT::Logger->debug( "Building $type link for $link: "
1163 . $T::Tickets{TOP}->Id );
1164 $link = $T::Tickets{TOP}->Id;
1166 } elsif ( $link !~ m/^\d+$/ ) {
1167 my $key = "create-$link";
1168 if ( !exists $T::Tickets{$key} ) {
1170 "Skipping $type link for $key (non-existent)");
1173 $RT::Logger->debug( "Building $type link for $link: "
1174 . $T::Tickets{$key}->Id );
1175 $link = $T::Tickets{$key}->Id;
1177 $RT::Logger->debug("Building $type link for $link");
1180 my ( $wval, $wmsg ) = $ticket->AddLink(
1181 Type => $RT::Link::TYPEMAP{$type}->{'Type'},
1182 $RT::Link::TYPEMAP{$type}->{'Mode'} => $link,
1186 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1189 # push @non_fatal_errors, $wmsg unless ($wval);
1195 # postponed actions -- Status only, currently
1196 while ( my $template_id = shift(@$postponed) ) {
1197 my $ticket = $T::Tickets{$template_id};
1198 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1199 my %args = %{ shift(@$postponed) };
1200 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1205 RT::Base->_ImportOverlays();