1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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
61 Create one or more tickets according to an externally supplied template.
66 ===Create-Ticket codereview
67 Subject: Code review for {$Tickets{'TOP'}->Subject}
69 Content: Someone has created a ticket. you should review and approve it,
70 so they can finish their work
76 Using the "CreateTickets" ScripAction and mandatory dependencies, RT now has
77 the ability to model complex workflow. When a ticket is created in a queue
78 that has a "CreateTickets" scripaction, that ScripAction parses its "Template"
84 CreateTickets uses the template as a template for an ordered set of tickets
85 to create. The basic format is as follows:
88 ===Create-Ticket: identifier
102 Each ===Create-Ticket: section is evaluated as its own
103 Text::Template object, which means that you can embed snippets
104 of perl inside the Text::Template using {} delimiters, but that
105 such sections absolutely can not span a ===Create-Ticket boundary.
107 After each ticket is created, it's stuffed into a hash called %Tickets
108 so as to be available during the creation of other tickets during the
109 same ScripAction, using the key 'create-identifier', where
110 C<identifier> is the id you put after C<===Create-Ticket:>. The hash
111 is prepopulated with the ticket which triggered the ScripAction as
112 $Tickets{'TOP'}; you can also access that ticket using the shorthand
117 ===Create-Ticket: codereview
118 Subject: Code review for {$Tickets{'TOP'}->Subject}
120 Content: Someone has created a ticket. you should review and approve it,
121 so they can finish their work
128 ===Create-Ticket: approval
129 { # Find out who the administrators of the group called "HR"
130 # of which the creator of this ticket is a member
133 my $groups = RT::Groups->new(RT->SystemUser);
134 $groups->LimitToUserDefinedGroups();
135 $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name");
136 $groups->WithMember($TransactionObj->CreatorObj->Id);
138 my $groupid = $groups->First->Id;
140 my $adminccs = RT::Users->new(RT->SystemUser);
141 $adminccs->WhoHaveRight(
142 Right => "AdminGroup",
143 Object =>$groups->First,
144 IncludeSystemRights => undef,
145 IncludeSuperusers => 0,
146 IncludeSubgroupMembers => 0,
150 while (my $admin = $adminccs->Next) {
151 push (@admins, $admin->EmailAddress);
156 AdminCc: {join ("\nAdminCc: ",@admins) }
159 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
161 Content-Type: text/plain
162 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
166 ===Create-Ticket: two
167 Subject: Manager approval
170 Refers-To: {$Tickets{"create-approval"}->Id}
172 Content-Type: text/plain
174 Your approval is requred for this ticket, too.
177 =head2 Acceptable fields
179 A complete list of acceptable fields for this beastie:
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.
194 Owner => Username or id of an RT user who can and should own
195 this ticket; forces the owner if necessary
196 + Requestor => Email address
197 + Cc => Email address
198 + AdminCc => Email address
199 + RequestorGroup => Group name
200 + CcGroup => Group name
201 + AdminCcGroup => Group name
214 Content => content. Can extend to multiple lines. Everything
215 within a template after a Content: header is treated
216 as content until we hit a line containing only
218 ContentType => the content-type of the Content field. Defaults to
220 UpdateType => 'correspond' or 'comment'; used in conjunction with
221 'content' if this is an update. Defaults to
224 CustomField-<id#> => custom field value
225 CF-name => custom field value
226 CustomField-name => custom field value
228 Fields marked with an * are required.
230 Fields marked with a + may have multiple values, simply
231 by repeating the fieldname on a new line with an additional value.
233 Fields marked with a ! are postponed to be processed after all
234 tickets in the same actions are created. Except for 'Status', those
235 field can also take a ticket name within the same action (i.e.
236 the identifiers after ===Create-Ticket), instead of raw Ticket ID
239 When parsed, field names are converted to lowercase and have -s stripped.
240 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
241 be treated as the same thing.
248 Jesse Vincent <jesse@bestpractical.com>
297 #Do what we need to do and send it out.
301 # Create all the tickets we care about
302 return (1) unless $self->TicketObj->Type eq 'ticket';
304 $self->CreateByTemplate( $self->TicketObj );
305 $self->UpdateByTemplate( $self->TicketObj );
314 unless ( $self->TemplateObj ) {
315 $RT::Logger->warning("No template object handed to $self");
318 unless ( $self->TransactionObj ) {
319 $RT::Logger->warning("No transaction object handed to $self");
323 unless ( $self->TicketObj ) {
324 $RT::Logger->warning("No ticket object handed to $self");
329 if ( $self->TemplateObj->Type eq 'Perl' ) {
332 RT->Logger->info(sprintf(
333 "Template #%d is type %s. You most likely want to use a Perl template instead.",
334 $self->TemplateObj->id, $self->TemplateObj->Type
339 Content => $self->TemplateObj->Content,
340 _ActiveContent => $active,
348 sub CreateByTemplate {
352 $RT::Logger->debug("In CreateByTemplate");
356 # XXX: cargo cult programming that works. i'll be back.
358 local %T::Tickets = %T::Tickets;
359 local $T::TOP = $T::TOP;
360 local $T::ID = $T::ID;
361 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
362 local $T::TransactionObj = $self->TransactionObj;
365 my ( @links, @postponed );
366 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
367 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
370 $T::ID = $template_id;
371 @T::AllID = @{ $self->{'create_tickets'} };
373 ( $T::Tickets{$template_id}, $ticketargs )
374 = $self->ParseLines( $template_id, \@links, \@postponed );
376 # Now we have a %args to work with.
377 # Make sure we have at least the minimum set of
378 # reasonable data and do our thang
380 my ( $id, $transid, $msg )
381 = $T::Tickets{$template_id}->Create(%$ticketargs);
383 foreach my $res ( split( '\n', $msg ) ) {
385 $T::Tickets{$template_id}
386 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
390 if ( $self->TicketObj ) {
391 $msg = "Couldn't create related ticket $template_id for "
392 . $self->TicketObj->Id . " "
395 $msg = "Couldn't create ticket $template_id " . $msg;
398 $RT::Logger->error($msg);
402 $RT::Logger->debug("Assigned $template_id with $id");
403 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
405 && $T::Tickets{$template_id}->can('SetOriginObj');
409 $self->PostProcess( \@links, \@postponed );
414 sub UpdateByTemplate {
418 # XXX: cargo cult programming that works. i'll be back.
421 local %T::Tickets = %T::Tickets;
422 local $T::ID = $T::ID;
425 my ( @links, @postponed );
426 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
427 $RT::Logger->debug("Update Workflow: processing $template_id");
429 $T::ID = $template_id;
430 @T::AllID = @{ $self->{'update_tickets'} };
432 ( $T::Tickets{$template_id}, $ticketargs )
433 = $self->ParseLines( $template_id, \@links, \@postponed );
435 # Now we have a %args to work with.
436 # Make sure we have at least the minimum set of
437 # reasonable data and do our thang
454 my $id = $template_id;
455 $id =~ s/update-(\d+).*/$1/;
456 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
459 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
460 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
464 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
466 $template_id =~ m/^update-(.*)/;
467 my $base_id = "base-$1";
468 my $base = $self->{'templates'}->{$base_id};
472 $current =~ s/\n+$//;
474 # If we have no base template, set what we can.
475 if ( $base ne $current ) {
477 "Could not update ticket "
478 . $T::Tickets{$template_id}->Id
479 . ": Ticket has changed";
483 push @results, $T::Tickets{$template_id}->Update(
484 AttributesRef => \@attribs,
485 ARGSRef => $ticketargs
488 if ( $ticketargs->{'Owner'} ) {
489 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
490 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
494 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
497 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
499 next unless $ticketargs->{'MIMEObj'};
500 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
501 my ( $Transaction, $Description, $Object )
502 = $T::Tickets{$template_id}->Comment(
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 )
512 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
513 my ( $Transaction, $Description, $Object )
514 = $T::Tickets{$template_id}->Correspond(
515 BccMessageTo => $ticketargs->{'Bcc'},
516 MIMEObj => $ticketargs->{'MIMEObj'},
517 TimeTaken => $ticketargs->{'TimeWorked'}
520 $T::Tickets{$template_id}
521 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
527 $T::Tickets{$template_id}->loc(
528 "Update type was neither correspondence nor comment.")
530 . $T::Tickets{$template_id}->loc("Update not recorded.")
535 $self->PostProcess( \@links, \@postponed );
540 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
542 Parse a template from TEMPLATE_CONTENT
544 If $active is set to true, then we'll use Text::Template to parse the templates,
545 allowing you to embed active perl in your templates.
555 _ActiveContent => undef,
559 if ( $args{'_ActiveContent'} ) {
560 $self->{'UsePerlTextTemplate'} = 1;
563 $self->{'UsePerlTextTemplate'} = 0;
566 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
567 $self->_ParseMultilineTemplate(%args);
568 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
569 $self->_ParseXSVTemplate(%args);
571 RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}");
575 =head2 _ParseMultilineTemplate
577 Parses mulitline templates. Things like:
581 Takes the same arguments as Parse
585 sub _ParseMultilineTemplate {
592 my ( $queue, $requestor );
593 $RT::Logger->debug("Line: ===");
594 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
596 $RT::Logger->debug( "Line: " . utf8::is_utf8($line)
597 ? Encode::encode_utf8($line)
599 if ( $line =~ /^===/ ) {
600 if ( $template_id && !$queue && $args{'Queue'} ) {
601 $self->{'templates'}->{$template_id}
602 .= "Queue: $args{'Queue'}\n";
604 if ( $template_id && !$requestor && $args{'Requestor'} ) {
605 $self->{'templates'}->{$template_id}
606 .= "Requestor: $args{'Requestor'}\n";
611 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
612 $template_id = "create-$1";
613 $RT::Logger->debug("**** Create ticket: $template_id");
614 push @{ $self->{'create_tickets'} }, $template_id;
615 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
616 $template_id = "update-$1";
617 $RT::Logger->debug("**** Update ticket: $template_id");
618 push @{ $self->{'update_tickets'} }, $template_id;
619 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
620 $template_id = "base-$1";
621 $RT::Logger->debug("**** Base ticket: $template_id");
622 push @{ $self->{'base_tickets'} }, $template_id;
623 } elsif ( $line =~ /^===#.*$/ ) { # a comment
626 if ( $line =~ /^Queue:(.*)/i ) {
631 if ( !$value && $args{'Queue'} ) {
632 $value = $args{'Queue'};
633 $line = "Queue: $value";
636 if ( $line =~ /^Requestors?:(.*)/i ) {
641 if ( !$value && $args{'Requestor'} ) {
642 $value = $args{'Requestor'};
643 $line = "Requestor: $value";
646 $self->{'templates'}->{$template_id} .= $line . "\n";
649 if ( $template_id && !$queue && $args{'Queue'} ) {
650 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
656 my $template_id = shift;
658 my $postponed = shift;
660 my $content = $self->{'templates'}->{$template_id};
662 if ( $self->{'UsePerlTextTemplate'} ) {
665 "Workflow: evaluating\n$self->{templates}{$template_id}");
667 my $template = Text::Template->new(
673 $content = $template->fill_in(
676 $err = {@_}->{error};
680 $RT::Logger->debug("Workflow: yielding $content");
683 $RT::Logger->error( "Ticket creation failed: " . $err );
684 while ( my ( $k, $v ) = each %T::X ) {
686 "Eliminating $template_id from ${k}'s parents.");
687 delete $v->{$template_id};
693 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
697 my @lines = ( split( /\n/, $content ) );
698 while ( defined( my $line = shift @lines ) ) {
699 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
701 my $original_tag = $1;
702 my $tag = lc($original_tag);
704 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
706 $original_tags{$tag} = $original_tag;
708 if ( ref( $args{$tag} ) )
709 { #If it's an array, we want to push the value
710 push @{ $args{$tag} }, $value;
711 } elsif ( defined( $args{$tag} ) )
712 { #if we're about to get a second value, make it an array
713 $args{$tag} = [ $args{$tag}, $value ];
714 } else { #if there's nothing there, just set the value
715 $args{$tag} = $value;
718 if ( $tag =~ /^content$/i ) { #just build up the content
719 # convert it to an array
720 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
721 while ( defined( my $l = shift @lines ) ) {
722 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
723 push @{ $args{'content'} }, $l . "\n";
726 # if it's not content, strip leading and trailing spaces
728 $args{$tag} =~ s/^\s+//g;
729 $args{$tag} =~ s/\s+$//g;
732 ($tag =~ /^(requestor|cc|admincc)(group)?$/i
733 or grep {lc $_ eq $tag} keys %LINKTYPEMAP)
734 and $args{$tag} =~ /,/
736 $args{$tag} = [ split /,\s*/, $args{$tag} ];
742 foreach my $date (qw(due starts started resolved)) {
743 my $dateobj = RT::Date->new( $self->CurrentUser );
744 next unless $args{$date};
745 if ( $args{$date} =~ /^\d+$/ ) {
746 $dateobj->Set( Format => 'unix', Value => $args{$date} );
749 $dateobj->Set( Format => 'iso', Value => $args{$date} );
751 if ($@ or $dateobj->Unix <= 0) {
752 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
755 $args{$date} = $dateobj->ISO;
758 foreach my $role (qw(requestor cc admincc)) {
759 next unless my $value = $args{ $role . 'group' };
761 my $group = RT::Group->new( $self->CurrentUser );
762 $group->LoadUserDefinedGroup( $value );
763 unless ( $group->id ) {
764 $RT::Logger->error("Couldn't load group '$value'");
768 $args{ $role } = $args{ $role } ? [$args{ $role }] : []
769 unless ref $args{ $role };
770 push @{ $args{ $role } }, $group->PrincipalObj->id;
773 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
776 $args{'type'} ||= 'ticket';
779 Queue => $args{'queue'},
780 Subject => $args{'subject'},
781 Status => $args{'status'} || 'new',
783 Starts => $args{'starts'},
784 Started => $args{'started'},
785 Resolved => $args{'resolved'},
786 Owner => $args{'owner'},
787 Requestor => $args{'requestor'},
789 AdminCc => $args{'admincc'},
790 TimeWorked => $args{'timeworked'},
791 TimeEstimated => $args{'timeestimated'},
792 TimeLeft => $args{'timeleft'},
793 InitialPriority => $args{'initialpriority'} || 0,
794 FinalPriority => $args{'finalpriority'} || 0,
795 SquelchMailTo => $args{'squelchmailto'},
796 Type => $args{'type'},
800 if ( $args{content} ) {
801 my $mimeobj = MIME::Entity->new();
803 Type => $args{'contenttype'} || 'text/plain',
804 Data => $args{'content'}
806 $ticketargs{MIMEObj} = $mimeobj;
807 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
810 foreach my $tag ( keys(%args) ) {
811 # if the tag was added later, skip it
812 my $orig_tag = $original_tags{$tag} or next;
813 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
814 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
815 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
816 my $cf = RT::CustomField->new( $self->CurrentUser );
817 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
818 $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
820 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
821 } elsif ($orig_tag) {
822 my $cf = RT::CustomField->new( $self->CurrentUser );
823 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
824 $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
826 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
831 $self->GetDeferred( \%args, $template_id, $links, $postponed );
833 return $TicketObj, \%ticketargs;
837 =head2 _ParseXSVTemplate
839 Parses a tab or comma delimited template. Should only ever be called by Parse
843 sub _ParseXSVTemplate {
847 use Regexp::Common qw(delimited);
848 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
851 if ( $first =~ /\t/ ) {
856 my @fields = split( /$delimiter/, $first );
858 my $delimiter_re = qr[$delimiter];
859 my $justquoted = qr[$RE{quoted}];
861 # Used to generate automatic template ids
866 $content =~ s/^(\s*\r?\n)+//;
868 # Keep track of Queue and Requestor, so we can provide defaults
872 # The template for this line
875 # What column we're on
878 # If the last iteration was the end of the line
885 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
888 # Strip off quotes, if they exist
890 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
891 substr( $value, 0, 1 ) = "";
892 substr( $value, -1, 1 ) = "";
895 # What column is this?
896 my $field = $fields[$i++];
897 next COLUMN unless $field =~ /\S/;
901 if ( $field =~ /^id$/i ) {
902 # Special case if this is the ID column
903 if ( $value =~ /^\d+$/ ) {
904 $template_id = 'update-' . $value;
905 push @{ $self->{'update_tickets'} }, $template_id;
906 } elsif ( $value =~ /^#base-(\d+)$/ ) {
907 $template_id = 'base-' . $1;
908 push @{ $self->{'base_tickets'} }, $template_id;
909 } elsif ( $value =~ /\S/ ) {
910 $template_id = 'create-' . $value;
911 push @{ $self->{'create_tickets'} }, $template_id;
915 if ( $field =~ /^Body$/i
916 || $field =~ /^Data$/i
917 || $field =~ /^Message$/i )
920 } elsif ( $field =~ /^Summary$/i ) {
922 } elsif ( $field =~ /^Queue$/i ) {
923 # Note that we found a queue
925 $value ||= $args{'Queue'};
926 } elsif ( $field =~ /^Requestors?$/i ) {
927 $field = 'Requestor'; # Remove plural
928 # Note that we found a requestor
930 $value ||= $args{'Requestor'};
933 # Tack onto the end of the template
934 $template .= $field . ": ";
935 $template .= (defined $value ? $value : "");
937 $template .= "ENDOFCONTENT\n"
938 if $field =~ /^Content$/i;
943 next unless $template;
945 # If we didn't find a queue of requestor, tack on the defaults
946 if ( !$queue && $args{'Queue'} ) {
947 $template .= "Queue: $args{'Queue'}\n";
949 if ( !$requestor && $args{'Requestor'} ) {
950 $template .= "Requestor: $args{'Requestor'}\n";
953 # If we never found an ID, come up with one
954 unless ($template_id) {
955 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
956 $template_id = "create-auto-$autoid";
957 # Also, it's a ticket to create
958 push @{ $self->{'create_tickets'} }, $template_id;
961 # Save the template we generated
962 $self->{'templates'}->{$template_id} = $template;
972 my $postponed = shift;
974 # Deferred processing
978 { DependsOn => $args->{'dependson'},
979 DependedOnBy => $args->{'dependedonby'},
980 RefersTo => $args->{'refersto'},
981 ReferredToBy => $args->{'referredtoby'},
982 Children => $args->{'children'},
983 Parents => $args->{'parents'},
989 # Status is postponed so we don't violate dependencies
990 $id, { Status => $args->{'status'}, }
994 sub GetUpdateTemplate {
999 $string .= "Queue: " . $t->QueueObj->Name . "\n";
1000 $string .= "Subject: " . $t->Subject . "\n";
1001 $string .= "Status: " . $t->Status . "\n";
1002 $string .= "UpdateType: correspond\n";
1003 $string .= "Content: \n";
1004 $string .= "ENDOFCONTENT\n";
1005 $string .= "Due: " . $t->DueObj->AsString . "\n";
1006 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1007 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1008 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1009 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
1010 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1011 $string .= "Cc: " . $t->CcAddresses . "\n";
1012 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1013 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1014 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1015 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1016 $string .= "InitialPriority: " . $t->Priority . "\n";
1017 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1019 foreach my $type ( sort keys %LINKTYPEMAP ) {
1021 # don't display duplicates
1022 if ( $type eq "HasMember"
1023 || $type eq "Members"
1024 || $type eq "MemberOf" )
1028 $string .= "$type: ";
1030 my $mode = $LINKTYPEMAP{$type}->{Mode};
1031 my $method = $LINKTYPEMAP{$type}->{Type};
1034 while ( my $link = $t->$method->Next ) {
1035 $links .= ", " if $links;
1037 my $object = $mode . "Obj";
1038 my $member = $link->$object;
1039 $links .= $member->Id if $member;
1048 sub GetBaseTemplate {
1053 $string .= "Queue: " . $t->Queue . "\n";
1054 $string .= "Subject: " . $t->Subject . "\n";
1055 $string .= "Status: " . $t->Status . "\n";
1056 $string .= "Due: " . $t->DueObj->Unix . "\n";
1057 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1058 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1059 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1060 $string .= "Owner: " . $t->Owner . "\n";
1061 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1062 $string .= "Cc: " . $t->CcAddresses . "\n";
1063 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1064 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1065 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1066 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1067 $string .= "InitialPriority: " . $t->Priority . "\n";
1068 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1073 sub GetCreateTemplate {
1078 $string .= "Queue: General\n";
1079 $string .= "Subject: \n";
1080 $string .= "Status: new\n";
1081 $string .= "Content: \n";
1082 $string .= "ENDOFCONTENT\n";
1083 $string .= "Due: \n";
1084 $string .= "Starts: \n";
1085 $string .= "Started: \n";
1086 $string .= "Resolved: \n";
1087 $string .= "Owner: \n";
1088 $string .= "Requestor: \n";
1089 $string .= "Cc: \n";
1090 $string .= "AdminCc:\n";
1091 $string .= "TimeWorked: \n";
1092 $string .= "TimeEstimated: \n";
1093 $string .= "TimeLeft: \n";
1094 $string .= "InitialPriority: \n";
1095 $string .= "FinalPriority: \n";
1097 foreach my $type ( keys %LINKTYPEMAP ) {
1099 # don't display duplicates
1100 if ( $type eq "HasMember"
1101 || $type eq 'Members'
1102 || $type eq 'MemberOf' )
1106 $string .= "$type: \n";
1111 sub UpdateWatchers {
1118 foreach my $type (qw(Requestor Cc AdminCc)) {
1119 my $method = $type . 'Addresses';
1120 my $oldaddr = $ticket->$method;
1122 # Skip unless we have a defined field
1123 next unless defined $args->{$type};
1124 my $newaddr = $args->{$type};
1126 my @old = split( /,\s*/, $oldaddr );
1128 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1129 # Sometimes these are email addresses, sometimes they're
1130 # users. Try to guess which is which, as we want to deal
1131 # with email addresses if at all possible.
1135 # It doesn't look like an email address. Try to load it.
1136 my $user = RT::User->new($self->CurrentUser);
1139 push @new, $user->EmailAddress;
1146 my %oldhash = map { $_ => 1 } @old;
1147 my %newhash = map { $_ => 1 } @new;
1149 my @add = grep( !defined $oldhash{$_}, @new );
1150 my @delete = grep( !defined $newhash{$_}, @old );
1153 my ( $val, $msg ) = $ticket->AddWatcher(
1159 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1163 my ( $val, $msg ) = $ticket->DeleteWatcher(
1168 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1174 sub UpdateCustomFields {
1180 foreach my $arg (keys %{$args}) {
1181 next unless $arg =~ /^CustomField-(\d+)$/;
1184 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1185 $CustomFieldObj->SetContextObject( $ticket );
1186 $CustomFieldObj->LoadById($cf);
1189 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1190 @values = ($args->{$arg});
1192 @values = split /\n/, $args->{$arg};
1195 if ( ($CustomFieldObj->Type eq 'Freeform'
1196 && ! $CustomFieldObj->SingleValue) ||
1197 $CustomFieldObj->Type =~ /text/i) {
1198 foreach my $val (@values) {
1203 foreach my $value (@values) {
1204 next unless length($value);
1205 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1209 push ( @results, $msg );
1218 my $postponed = shift;
1220 # postprocessing: add links
1222 while ( my $template_id = shift(@$links) ) {
1223 my $ticket = $T::Tickets{$template_id};
1224 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1225 my %args = %{ shift(@$links) };
1227 foreach my $type ( keys %LINKTYPEMAP ) {
1228 next unless ( defined $args{$type} );
1230 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1234 if ( $link =~ /^TOP$/i ) {
1235 $RT::Logger->debug( "Building $type link for $link: "
1236 . $T::Tickets{TOP}->Id );
1237 $link = $T::Tickets{TOP}->Id;
1239 } elsif ( $link !~ m/^\d+$/ ) {
1240 my $key = "create-$link";
1241 if ( !exists $T::Tickets{$key} ) {
1243 "Skipping $type link for $key (non-existent)");
1246 $RT::Logger->debug( "Building $type link for $link: "
1247 . $T::Tickets{$key}->Id );
1248 $link = $T::Tickets{$key}->Id;
1250 $RT::Logger->debug("Building $type link for $link");
1253 my ( $wval, $wmsg ) = $ticket->AddLink(
1254 Type => $LINKTYPEMAP{$type}->{'Type'},
1255 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1259 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1262 # push @non_fatal_errors, $wmsg unless ($wval);
1268 # postponed actions -- Status only, currently
1269 while ( my $template_id = shift(@$postponed) ) {
1270 my $ticket = $T::Tickets{$template_id};
1271 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1272 my %args = %{ shift(@$postponed) };
1273 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1280 my $queues = RT::Queues->new($self->CurrentUser);
1283 while (my $queue = $queues->Next) {
1284 push @names, $queue->Id, $queue->Name;
1289 'label' => 'In queue',
1291 'options' => \@names
1296 RT::Base->_ImportOverlays();