1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2016 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 {
538 my ( $queue, $requestor );
539 $RT::Logger->debug("Line: ===");
540 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
542 $RT::Logger->debug( "Line: $line" );
543 if ( $line =~ /^===/ ) {
544 if ( $template_id && !$queue && $args{'Queue'} ) {
545 $self->{'templates'}->{$template_id}
546 .= "Queue: $args{'Queue'}\n";
548 if ( $template_id && !$requestor && $args{'Requestor'} ) {
549 $self->{'templates'}->{$template_id}
550 .= "Requestor: $args{'Requestor'}\n";
555 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
556 $template_id = "create-$1";
557 $RT::Logger->debug("**** Create ticket: $template_id");
558 push @{ $self->{'create_tickets'} }, $template_id;
559 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
560 $template_id = "update-$1";
561 $RT::Logger->debug("**** Update ticket: $template_id");
562 push @{ $self->{'update_tickets'} }, $template_id;
563 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
564 $template_id = "base-$1";
565 $RT::Logger->debug("**** Base ticket: $template_id");
566 push @{ $self->{'base_tickets'} }, $template_id;
567 } elsif ( $line =~ /^===#.*$/ ) { # a comment
570 if ( $line =~ /^Queue:(.*)/i ) {
575 if ( !$value && $args{'Queue'} ) {
576 $value = $args{'Queue'};
577 $line = "Queue: $value";
580 if ( $line =~ /^Requestors?:(.*)/i ) {
585 if ( !$value && $args{'Requestor'} ) {
586 $value = $args{'Requestor'};
587 $line = "Requestor: $value";
590 $self->{'templates'}->{$template_id} .= $line . "\n";
593 if ( $template_id && !$queue && $args{'Queue'} ) {
594 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
600 my $template_id = shift;
602 my $postponed = shift;
604 my $content = $self->{'templates'}->{$template_id};
606 if ( $self->{'UsePerlTextTemplate'} ) {
609 "Workflow: evaluating\n$self->{templates}{$template_id}");
611 my $template = Text::Template->new(
617 $content = $template->fill_in(
620 $err = {@_}->{error};
624 $RT::Logger->debug("Workflow: yielding $content");
627 $RT::Logger->error( "Ticket creation failed: " . $err );
632 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
636 my @lines = ( split( /\n/, $content ) );
637 while ( defined( my $line = shift @lines ) ) {
638 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
640 my $original_tag = $1;
641 my $tag = lc($original_tag);
643 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
645 $original_tags{$tag} = $original_tag;
647 if ( ref( $args{$tag} ) )
648 { #If it's an array, we want to push the value
649 push @{ $args{$tag} }, $value;
650 } elsif ( defined( $args{$tag} ) )
651 { #if we're about to get a second value, make it an array
652 $args{$tag} = [ $args{$tag}, $value ];
653 } else { #if there's nothing there, just set the value
654 $args{$tag} = $value;
657 if ( $tag =~ /^content$/i ) { #just build up the content
658 # convert it to an array
659 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
660 while ( defined( my $l = shift @lines ) ) {
661 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
662 push @{ $args{'content'} }, $l . "\n";
665 # if it's not content, strip leading and trailing spaces
667 $args{$tag} =~ s/^\s+//g;
668 $args{$tag} =~ s/\s+$//g;
671 ($tag =~ /^(requestor|cc|admincc)(group)?$/i
672 or grep {lc $_ eq $tag} keys %RT::Link::TYPEMAP)
673 and $args{$tag} =~ /,/
675 $args{$tag} = [ split /,\s*/, $args{$tag} ];
681 foreach my $date (qw(due starts started resolved)) {
682 my $dateobj = RT::Date->new( $self->CurrentUser );
683 next unless $args{$date};
684 if ( $args{$date} =~ /^\d+$/ ) {
685 $dateobj->Set( Format => 'unix', Value => $args{$date} );
688 $dateobj->Set( Format => 'iso', Value => $args{$date} );
690 if ($@ or not $dateobj->IsSet) {
691 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
694 $args{$date} = $dateobj->ISO;
697 foreach my $role (qw(requestor cc admincc)) {
698 next unless my $value = $args{ $role . 'group' };
700 my $group = RT::Group->new( $self->CurrentUser );
701 $group->LoadUserDefinedGroup( $value );
702 unless ( $group->id ) {
703 $RT::Logger->error("Couldn't load group '$value'");
707 $args{ $role } = $args{ $role } ? [$args{ $role }] : []
708 unless ref $args{ $role };
709 push @{ $args{ $role } }, $group->PrincipalObj->id;
712 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
715 $args{'type'} ||= 'ticket';
718 Queue => $args{'queue'},
719 Subject => $args{'subject'},
720 Status => $args{'status'} || 'new',
722 Starts => $args{'starts'},
723 Started => $args{'started'},
724 Resolved => $args{'resolved'},
725 Owner => $args{'owner'},
726 Requestor => $args{'requestor'},
728 AdminCc => $args{'admincc'},
729 TimeWorked => $args{'timeworked'},
730 TimeEstimated => $args{'timeestimated'},
731 TimeLeft => $args{'timeleft'},
732 InitialPriority => $args{'initialpriority'} || 0,
733 FinalPriority => $args{'finalpriority'} || 0,
734 SquelchMailTo => $args{'squelchmailto'},
735 Type => $args{'type'},
738 if ( $args{content} ) {
739 my $mimeobj = MIME::Entity->build(
740 Type => $args{'contenttype'} || 'text/plain',
742 Data => [ map {Encode::encode( "UTF-8", $_ )} @{$args{'content'}} ],
744 $ticketargs{MIMEObj} = $mimeobj;
745 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
748 foreach my $tag ( keys(%args) ) {
749 # if the tag was added later, skip it
750 my $orig_tag = $original_tags{$tag} or next;
751 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
752 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
753 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
754 my $cf = RT::CustomField->new( $self->CurrentUser );
757 LookupType => RT::Ticket->CustomFieldLookupType,
758 ObjectId => $ticketargs{Queue},
762 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
763 } elsif ($orig_tag) {
764 my $cf = RT::CustomField->new( $self->CurrentUser );
767 LookupType => RT::Ticket->CustomFieldLookupType,
768 ObjectId => $ticketargs{Queue},
772 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
777 $self->GetDeferred( \%args, $template_id, $links, $postponed );
779 return $TicketObj, \%ticketargs;
783 =head2 _ParseXSVTemplate
785 Parses a tab or comma delimited template. Should only ever be called by
790 sub _ParseXSVTemplate {
794 use Regexp::Common qw(delimited);
795 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
798 if ( $first =~ /\t/ ) {
803 my @fields = split( /$delimiter/, $first );
805 my $delimiter_re = qr[$delimiter];
806 my $justquoted = qr[$RE{quoted}];
808 # Used to generate automatic template ids
813 $content =~ s/^(\s*\r?\n)+//;
815 # Keep track of Queue and Requestor, so we can provide defaults
819 # The template for this line
822 # What column we're on
825 # If the last iteration was the end of the line
832 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
835 # Strip off quotes, if they exist
837 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
838 substr( $value, 0, 1 ) = "";
839 substr( $value, -1, 1 ) = "";
842 # What column is this?
843 my $field = $fields[$i++];
844 next COLUMN unless $field =~ /\S/;
848 if ( $field =~ /^id$/i ) {
849 # Special case if this is the ID column
850 if ( $value =~ /^\d+$/ ) {
851 $template_id = 'update-' . $value;
852 push @{ $self->{'update_tickets'} }, $template_id;
853 } elsif ( $value =~ /^#base-(\d+)$/ ) {
854 $template_id = 'base-' . $1;
855 push @{ $self->{'base_tickets'} }, $template_id;
856 } elsif ( $value =~ /\S/ ) {
857 $template_id = 'create-' . $value;
858 push @{ $self->{'create_tickets'} }, $template_id;
862 if ( $field =~ /^Body$/i
863 || $field =~ /^Data$/i
864 || $field =~ /^Message$/i )
867 } elsif ( $field =~ /^Summary$/i ) {
869 } elsif ( $field =~ /^Queue$/i ) {
870 # Note that we found a queue
872 $value ||= $args{'Queue'};
873 } elsif ( $field =~ /^Requestors?$/i ) {
874 $field = 'Requestor'; # Remove plural
875 # Note that we found a requestor
877 $value ||= $args{'Requestor'};
880 # Tack onto the end of the template
881 $template .= $field . ": ";
882 $template .= (defined $value ? $value : "");
884 $template .= "ENDOFCONTENT\n"
885 if $field =~ /^Content$/i;
890 next unless $template;
892 # If we didn't find a queue of requestor, tack on the defaults
893 if ( !$queue && $args{'Queue'} ) {
894 $template .= "Queue: $args{'Queue'}\n";
896 if ( !$requestor && $args{'Requestor'} ) {
897 $template .= "Requestor: $args{'Requestor'}\n";
900 # If we never found an ID, come up with one
901 unless ($template_id) {
902 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
903 $template_id = "create-auto-$autoid";
904 # Also, it's a ticket to create
905 push @{ $self->{'create_tickets'} }, $template_id;
908 # Save the template we generated
909 $self->{'templates'}->{$template_id} = $template;
919 my $postponed = shift;
921 # Unify the aliases for child/parent
922 $args->{$_} = [$args->{$_}]
923 for grep {$args->{$_} and not ref $args->{$_}} qw/members hasmember memberof/;
924 push @{$args->{'children'}}, @{delete $args->{'members'}} if $args->{'members'};
925 push @{$args->{'children'}}, @{delete $args->{'hasmember'}} if $args->{'hasmember'};
926 push @{$args->{'parents'}}, @{delete $args->{'memberof'}} if $args->{'memberof'};
928 # Deferred processing
932 { DependsOn => $args->{'dependson'},
933 DependedOnBy => $args->{'dependedonby'},
934 RefersTo => $args->{'refersto'},
935 ReferredToBy => $args->{'referredtoby'},
936 Children => $args->{'children'},
937 Parents => $args->{'parents'},
943 # Status is postponed so we don't violate dependencies
944 $id, { Status => $args->{'status'}, }
948 sub GetUpdateTemplate {
953 $string .= "Queue: " . $t->QueueObj->Name . "\n";
954 $string .= "Subject: " . $t->Subject . "\n";
955 $string .= "Status: " . $t->Status . "\n";
956 $string .= "UpdateType: correspond\n";
957 $string .= "Content: \n";
958 $string .= "ENDOFCONTENT\n";
959 $string .= "Due: " . $t->DueObj->AsString . "\n";
960 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
961 $string .= "Started: " . $t->StartedObj->AsString . "\n";
962 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
963 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
964 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
965 $string .= "Cc: " . $t->CcAddresses . "\n";
966 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
967 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
968 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
969 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
970 $string .= "InitialPriority: " . $t->Priority . "\n";
971 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
973 foreach my $type ( RT::Link->DisplayTypes ) {
974 $string .= "$type: ";
976 my $mode = $RT::Link::TYPEMAP{$type}->{Mode};
977 my $method = $RT::Link::TYPEMAP{$type}->{Type};
980 while ( my $link = $t->$method->Next ) {
981 $links .= ", " if $links;
983 my $object = $mode . "Obj";
984 my $member = $link->$object;
985 $links .= $member->Id if $member;
994 sub GetBaseTemplate {
999 $string .= "Queue: " . $t->Queue . "\n";
1000 $string .= "Subject: " . $t->Subject . "\n";
1001 $string .= "Status: " . $t->Status . "\n";
1002 $string .= "Due: " . $t->DueObj->Unix . "\n";
1003 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1004 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1005 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1006 $string .= "Owner: " . $t->Owner . "\n";
1007 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1008 $string .= "Cc: " . $t->CcAddresses . "\n";
1009 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1010 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1011 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1012 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1013 $string .= "InitialPriority: " . $t->Priority . "\n";
1014 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1019 sub GetCreateTemplate {
1024 $string .= "Queue: General\n";
1025 $string .= "Subject: \n";
1026 $string .= "Status: new\n";
1027 $string .= "Content: \n";
1028 $string .= "ENDOFCONTENT\n";
1029 $string .= "Due: \n";
1030 $string .= "Starts: \n";
1031 $string .= "Started: \n";
1032 $string .= "Resolved: \n";
1033 $string .= "Owner: \n";
1034 $string .= "Requestor: \n";
1035 $string .= "Cc: \n";
1036 $string .= "AdminCc:\n";
1037 $string .= "TimeWorked: \n";
1038 $string .= "TimeEstimated: \n";
1039 $string .= "TimeLeft: \n";
1040 $string .= "InitialPriority: \n";
1041 $string .= "FinalPriority: \n";
1043 foreach my $type ( RT::Link->DisplayTypes ) {
1044 $string .= "$type: \n";
1049 sub UpdateWatchers {
1056 foreach my $type (qw(Requestor Cc AdminCc)) {
1057 my $method = $type . 'Addresses';
1058 my $oldaddr = $ticket->$method;
1060 # Skip unless we have a defined field
1061 next unless defined $args->{$type};
1062 my $newaddr = $args->{$type};
1064 my @old = split( /,\s*/, $oldaddr );
1066 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1067 # Sometimes these are email addresses, sometimes they're
1068 # users. Try to guess which is which, as we want to deal
1069 # with email addresses if at all possible.
1073 # It doesn't look like an email address. Try to load it.
1074 my $user = RT::User->new($self->CurrentUser);
1077 push @new, $user->EmailAddress;
1084 my %oldhash = map { $_ => 1 } @old;
1085 my %newhash = map { $_ => 1 } @new;
1087 my @add = grep( !defined $oldhash{$_}, @new );
1088 my @delete = grep( !defined $newhash{$_}, @old );
1091 my ( $val, $msg ) = $ticket->AddWatcher(
1097 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1101 my ( $val, $msg ) = $ticket->DeleteWatcher(
1106 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1112 sub UpdateCustomFields {
1118 foreach my $arg (keys %{$args}) {
1119 next unless $arg =~ /^CustomField-(\d+)$/;
1122 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1123 $CustomFieldObj->SetContextObject( $ticket );
1124 $CustomFieldObj->LoadById($cf);
1127 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1128 @values = ($args->{$arg});
1130 @values = split /\n/, $args->{$arg};
1133 if ( ($CustomFieldObj->Type eq 'Freeform'
1134 && ! $CustomFieldObj->SingleValue) ||
1135 $CustomFieldObj->Type =~ /text/i) {
1136 foreach my $val (@values) {
1141 foreach my $value (@values) {
1142 next if $ticket->CustomFieldValueIsEmpty(
1146 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1150 push ( @results, $msg );
1159 my $postponed = shift;
1161 # postprocessing: add links
1163 while ( my $template_id = shift(@$links) ) {
1164 my $ticket = $T::Tickets{$template_id};
1165 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1166 my %args = %{ shift(@$links) };
1168 foreach my $type ( keys %RT::Link::TYPEMAP ) {
1169 next unless ( defined $args{$type} );
1171 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1175 if ( $link =~ /^TOP$/i ) {
1176 $RT::Logger->debug( "Building $type link for $link: "
1177 . $T::Tickets{TOP}->Id );
1178 $link = $T::Tickets{TOP}->Id;
1180 } elsif ( $link !~ m/^\d+$/ ) {
1181 my $key = "create-$link";
1182 if ( !exists $T::Tickets{$key} ) {
1184 "Skipping $type link for $key (non-existent)");
1187 $RT::Logger->debug( "Building $type link for $link: "
1188 . $T::Tickets{$key}->Id );
1189 $link = $T::Tickets{$key}->Id;
1191 $RT::Logger->debug("Building $type link for $link");
1194 my ( $wval, $wmsg ) = $ticket->AddLink(
1195 Type => $RT::Link::TYPEMAP{$type}->{'Type'},
1196 $RT::Link::TYPEMAP{$type}->{'Mode'} => $link,
1200 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1203 # push @non_fatal_errors, $wmsg unless ($wval);
1209 # postponed actions -- Status only, currently
1210 while ( my $template_id = shift(@$postponed) ) {
1211 my $ticket = $T::Tickets{$template_id};
1212 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1213 my %args = %{ shift(@$postponed) };
1214 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1219 RT::Base->_ImportOverlays();