1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 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'},
739 if ( $args{content} ) {
740 my $mimeobj = MIME::Entity->build(
741 Type => $args{'contenttype'} || 'text/plain',
743 Data => [ map {Encode::encode( "UTF-8", $_ )} @{$args{'content'}} ],
745 $ticketargs{MIMEObj} = $mimeobj;
746 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
749 foreach my $tag ( keys(%args) ) {
750 # if the tag was added later, skip it
751 my $orig_tag = $original_tags{$tag} or next;
752 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
753 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
754 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
755 my $cf = RT::CustomField->new( $self->CurrentUser );
758 LookupType => RT::Ticket->CustomFieldLookupType,
759 ObjectId => $ticketargs{Queue},
763 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
764 } elsif ($orig_tag) {
765 my $cf = RT::CustomField->new( $self->CurrentUser );
768 LookupType => RT::Ticket->CustomFieldLookupType,
769 ObjectId => $ticketargs{Queue},
773 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
778 $self->GetDeferred( \%args, $template_id, $links, $postponed );
780 return $TicketObj, \%ticketargs;
784 =head2 _ParseXSVTemplate
786 Parses a tab or comma delimited template. Should only ever be called by
791 sub _ParseXSVTemplate {
795 use Regexp::Common qw(delimited);
796 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
799 if ( $first =~ /\t/ ) {
804 my @fields = split( /$delimiter/, $first );
806 my $delimiter_re = qr[$delimiter];
807 my $justquoted = qr[$RE{quoted}];
809 # Used to generate automatic template ids
814 $content =~ s/^(\s*\r?\n)+//;
816 # Keep track of Queue and Requestor, so we can provide defaults
820 # The template for this line
823 # What column we're on
826 # If the last iteration was the end of the line
833 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
836 # Strip off quotes, if they exist
838 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
839 substr( $value, 0, 1 ) = "";
840 substr( $value, -1, 1 ) = "";
843 # What column is this?
844 my $field = $fields[$i++];
845 next COLUMN unless $field =~ /\S/;
849 if ( $field =~ /^id$/i ) {
850 # Special case if this is the ID column
851 if ( $value =~ /^\d+$/ ) {
852 $template_id = 'update-' . $value;
853 push @{ $self->{'update_tickets'} }, $template_id;
854 } elsif ( $value =~ /^#base-(\d+)$/ ) {
855 $template_id = 'base-' . $1;
856 push @{ $self->{'base_tickets'} }, $template_id;
857 } elsif ( $value =~ /\S/ ) {
858 $template_id = 'create-' . $value;
859 push @{ $self->{'create_tickets'} }, $template_id;
863 if ( $field =~ /^Body$/i
864 || $field =~ /^Data$/i
865 || $field =~ /^Message$/i )
868 } elsif ( $field =~ /^Summary$/i ) {
870 } elsif ( $field =~ /^Queue$/i ) {
871 # Note that we found a queue
873 $value ||= $args{'Queue'};
874 } elsif ( $field =~ /^Requestors?$/i ) {
875 $field = 'Requestor'; # Remove plural
876 # Note that we found a requestor
878 $value ||= $args{'Requestor'};
881 # Tack onto the end of the template
882 $template .= $field . ": ";
883 $template .= (defined $value ? $value : "");
885 $template .= "ENDOFCONTENT\n"
886 if $field =~ /^Content$/i;
891 next unless $template;
893 # If we didn't find a queue of requestor, tack on the defaults
894 if ( !$queue && $args{'Queue'} ) {
895 $template .= "Queue: $args{'Queue'}\n";
897 if ( !$requestor && $args{'Requestor'} ) {
898 $template .= "Requestor: $args{'Requestor'}\n";
901 # If we never found an ID, come up with one
902 unless ($template_id) {
903 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
904 $template_id = "create-auto-$autoid";
905 # Also, it's a ticket to create
906 push @{ $self->{'create_tickets'} }, $template_id;
909 # Save the template we generated
910 $self->{'templates'}->{$template_id} = $template;
920 my $postponed = shift;
922 # Unify the aliases for child/parent
923 $args->{$_} = [$args->{$_}]
924 for grep {$args->{$_} and not ref $args->{$_}} qw/members hasmember memberof/;
925 push @{$args->{'children'}}, @{delete $args->{'members'}} if $args->{'members'};
926 push @{$args->{'children'}}, @{delete $args->{'hasmember'}} if $args->{'hasmember'};
927 push @{$args->{'parents'}}, @{delete $args->{'memberof'}} if $args->{'memberof'};
929 # Deferred processing
933 { DependsOn => $args->{'dependson'},
934 DependedOnBy => $args->{'dependedonby'},
935 RefersTo => $args->{'refersto'},
936 ReferredToBy => $args->{'referredtoby'},
937 Children => $args->{'children'},
938 Parents => $args->{'parents'},
944 # Status is postponed so we don't violate dependencies
945 $id, { Status => $args->{'status'}, }
949 sub GetUpdateTemplate {
954 $string .= "Queue: " . $t->QueueObj->Name . "\n";
955 $string .= "Subject: " . $t->Subject . "\n";
956 $string .= "Status: " . $t->Status . "\n";
957 $string .= "UpdateType: correspond\n";
958 $string .= "Content: \n";
959 $string .= "ENDOFCONTENT\n";
960 $string .= "Due: " . $t->DueObj->AsString . "\n";
961 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
962 $string .= "Started: " . $t->StartedObj->AsString . "\n";
963 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
964 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
965 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
966 $string .= "Cc: " . $t->CcAddresses . "\n";
967 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
968 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
969 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
970 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
971 $string .= "InitialPriority: " . $t->Priority . "\n";
972 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
974 foreach my $type ( RT::Link->DisplayTypes ) {
975 $string .= "$type: ";
977 my $mode = $RT::Link::TYPEMAP{$type}->{Mode};
978 my $method = $RT::Link::TYPEMAP{$type}->{Type};
981 while ( my $link = $t->$method->Next ) {
982 $links .= ", " if $links;
984 my $object = $mode . "Obj";
985 my $member = $link->$object;
986 $links .= $member->Id if $member;
995 sub GetBaseTemplate {
1000 $string .= "Queue: " . $t->Queue . "\n";
1001 $string .= "Subject: " . $t->Subject . "\n";
1002 $string .= "Status: " . $t->Status . "\n";
1003 $string .= "Due: " . $t->DueObj->Unix . "\n";
1004 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1005 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1006 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1007 $string .= "Owner: " . $t->Owner . "\n";
1008 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1009 $string .= "Cc: " . $t->CcAddresses . "\n";
1010 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1011 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1012 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1013 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1014 $string .= "InitialPriority: " . $t->Priority . "\n";
1015 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1020 sub GetCreateTemplate {
1025 $string .= "Queue: General\n";
1026 $string .= "Subject: \n";
1027 $string .= "Status: new\n";
1028 $string .= "Content: \n";
1029 $string .= "ENDOFCONTENT\n";
1030 $string .= "Due: \n";
1031 $string .= "Starts: \n";
1032 $string .= "Started: \n";
1033 $string .= "Resolved: \n";
1034 $string .= "Owner: \n";
1035 $string .= "Requestor: \n";
1036 $string .= "Cc: \n";
1037 $string .= "AdminCc:\n";
1038 $string .= "TimeWorked: \n";
1039 $string .= "TimeEstimated: \n";
1040 $string .= "TimeLeft: \n";
1041 $string .= "InitialPriority: \n";
1042 $string .= "FinalPriority: \n";
1044 foreach my $type ( RT::Link->DisplayTypes ) {
1045 $string .= "$type: \n";
1050 sub UpdateWatchers {
1057 foreach my $type (qw(Requestor Cc AdminCc)) {
1058 my $method = $type . 'Addresses';
1059 my $oldaddr = $ticket->$method;
1061 # Skip unless we have a defined field
1062 next unless defined $args->{$type};
1063 my $newaddr = $args->{$type};
1065 my @old = split( /,\s*/, $oldaddr );
1067 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1068 # Sometimes these are email addresses, sometimes they're
1069 # users. Try to guess which is which, as we want to deal
1070 # with email addresses if at all possible.
1074 # It doesn't look like an email address. Try to load it.
1075 my $user = RT::User->new($self->CurrentUser);
1078 push @new, $user->EmailAddress;
1085 my %oldhash = map { $_ => 1 } @old;
1086 my %newhash = map { $_ => 1 } @new;
1088 my @add = grep( !defined $oldhash{$_}, @new );
1089 my @delete = grep( !defined $newhash{$_}, @old );
1092 my ( $val, $msg ) = $ticket->AddWatcher(
1098 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1102 my ( $val, $msg ) = $ticket->DeleteWatcher(
1107 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1113 sub UpdateCustomFields {
1119 foreach my $arg (keys %{$args}) {
1120 next unless $arg =~ /^CustomField-(\d+)$/;
1123 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1124 $CustomFieldObj->SetContextObject( $ticket );
1125 $CustomFieldObj->LoadById($cf);
1128 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1129 @values = ($args->{$arg});
1131 @values = split /\n/, $args->{$arg};
1134 if ( ($CustomFieldObj->Type eq 'Freeform'
1135 && ! $CustomFieldObj->SingleValue) ||
1136 $CustomFieldObj->Type =~ /text/i) {
1137 foreach my $val (@values) {
1142 foreach my $value (@values) {
1143 next unless length($value);
1144 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1148 push ( @results, $msg );
1157 my $postponed = shift;
1159 # postprocessing: add links
1161 while ( my $template_id = shift(@$links) ) {
1162 my $ticket = $T::Tickets{$template_id};
1163 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1164 my %args = %{ shift(@$links) };
1166 foreach my $type ( keys %RT::Link::TYPEMAP ) {
1167 next unless ( defined $args{$type} );
1169 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1173 if ( $link =~ /^TOP$/i ) {
1174 $RT::Logger->debug( "Building $type link for $link: "
1175 . $T::Tickets{TOP}->Id );
1176 $link = $T::Tickets{TOP}->Id;
1178 } elsif ( $link !~ m/^\d+$/ ) {
1179 my $key = "create-$link";
1180 if ( !exists $T::Tickets{$key} ) {
1182 "Skipping $type link for $key (non-existent)");
1185 $RT::Logger->debug( "Building $type link for $link: "
1186 . $T::Tickets{$key}->Id );
1187 $link = $T::Tickets{$key}->Id;
1189 $RT::Logger->debug("Building $type link for $link");
1192 my ( $wval, $wmsg ) = $ticket->AddLink(
1193 Type => $RT::Link::TYPEMAP{$type}->{'Type'},
1194 $RT::Link::TYPEMAP{$type}->{'Mode'} => $link,
1198 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1201 # push @non_fatal_errors, $wmsg unless ($wval);
1207 # postponed actions -- Status only, currently
1208 while ( my $template_id = shift(@$postponed) ) {
1209 my $ticket = $T::Tickets{$template_id};
1210 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1211 my %args = %{ shift(@$postponed) };
1212 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1219 my $queues = RT::Queues->new($self->CurrentUser);
1222 while (my $queue = $queues->Next) {
1223 push @names, $queue->Id, $queue->Name;
1228 'label' => 'In queue',
1230 'options' => \@names
1235 RT::Base->_ImportOverlays();