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);
574 =head2 _ParseMultilineTemplate
576 Parses mulitline templates. Things like:
580 Takes the same arguments as Parse
584 sub _ParseMultilineTemplate {
591 my ( $queue, $requestor );
592 $RT::Logger->debug("Line: ===");
593 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
595 $RT::Logger->debug( "Line: " . utf8::is_utf8($line)
596 ? Encode::encode_utf8($line)
598 if ( $line =~ /^===/ ) {
599 if ( $template_id && !$queue && $args{'Queue'} ) {
600 $self->{'templates'}->{$template_id}
601 .= "Queue: $args{'Queue'}\n";
603 if ( $template_id && !$requestor && $args{'Requestor'} ) {
604 $self->{'templates'}->{$template_id}
605 .= "Requestor: $args{'Requestor'}\n";
610 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
611 $template_id = "create-$1";
612 $RT::Logger->debug("**** Create ticket: $template_id");
613 push @{ $self->{'create_tickets'} }, $template_id;
614 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
615 $template_id = "update-$1";
616 $RT::Logger->debug("**** Update ticket: $template_id");
617 push @{ $self->{'update_tickets'} }, $template_id;
618 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
619 $template_id = "base-$1";
620 $RT::Logger->debug("**** Base ticket: $template_id");
621 push @{ $self->{'base_tickets'} }, $template_id;
622 } elsif ( $line =~ /^===#.*$/ ) { # a comment
625 if ( $line =~ /^Queue:(.*)/i ) {
630 if ( !$value && $args{'Queue'} ) {
631 $value = $args{'Queue'};
632 $line = "Queue: $value";
635 if ( $line =~ /^Requestors?:(.*)/i ) {
640 if ( !$value && $args{'Requestor'} ) {
641 $value = $args{'Requestor'};
642 $line = "Requestor: $value";
645 $self->{'templates'}->{$template_id} .= $line . "\n";
648 if ( $template_id && !$queue && $args{'Queue'} ) {
649 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
655 my $template_id = shift;
657 my $postponed = shift;
659 my $content = $self->{'templates'}->{$template_id};
661 if ( $self->{'UsePerlTextTemplate'} ) {
664 "Workflow: evaluating\n$self->{templates}{$template_id}");
666 my $template = Text::Template->new(
672 $content = $template->fill_in(
675 $err = {@_}->{error};
679 $RT::Logger->debug("Workflow: yielding $content");
682 $RT::Logger->error( "Ticket creation failed: " . $err );
683 while ( my ( $k, $v ) = each %T::X ) {
685 "Eliminating $template_id from ${k}'s parents.");
686 delete $v->{$template_id};
692 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
696 my @lines = ( split( /\n/, $content ) );
697 while ( defined( my $line = shift @lines ) ) {
698 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
700 my $original_tag = $1;
701 my $tag = lc($original_tag);
703 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
705 $original_tags{$tag} = $original_tag;
707 if ( ref( $args{$tag} ) )
708 { #If it's an array, we want to push the value
709 push @{ $args{$tag} }, $value;
710 } elsif ( defined( $args{$tag} ) )
711 { #if we're about to get a second value, make it an array
712 $args{$tag} = [ $args{$tag}, $value ];
713 } else { #if there's nothing there, just set the value
714 $args{$tag} = $value;
717 if ( $tag =~ /^content$/i ) { #just build up the content
718 # convert it to an array
719 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
720 while ( defined( my $l = shift @lines ) ) {
721 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
722 push @{ $args{'content'} }, $l . "\n";
725 # if it's not content, strip leading and trailing spaces
727 $args{$tag} =~ s/^\s+//g;
728 $args{$tag} =~ s/\s+$//g;
731 ($tag =~ /^(requestor|cc|admincc)(group)?$/i
732 or grep {lc $_ eq $tag} keys %LINKTYPEMAP)
733 and $args{$tag} =~ /,/
735 $args{$tag} = [ split /,\s*/, $args{$tag} ];
741 foreach my $date (qw(due starts started resolved)) {
742 my $dateobj = RT::Date->new( $self->CurrentUser );
743 next unless $args{$date};
744 if ( $args{$date} =~ /^\d+$/ ) {
745 $dateobj->Set( Format => 'unix', Value => $args{$date} );
748 $dateobj->Set( Format => 'iso', Value => $args{$date} );
750 if ($@ or $dateobj->Unix <= 0) {
751 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
754 $args{$date} = $dateobj->ISO;
757 foreach my $role (qw(requestor cc admincc)) {
758 next unless my $value = $args{ $role . 'group' };
760 my $group = RT::Group->new( $self->CurrentUser );
761 $group->LoadUserDefinedGroup( $value );
762 unless ( $group->id ) {
763 $RT::Logger->error("Couldn't load group '$value'");
767 $args{ $role } = $args{ $role } ? [$args{ $role }] : []
768 unless ref $args{ $role };
769 push @{ $args{ $role } }, $group->PrincipalObj->id;
772 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
775 $args{'type'} ||= 'ticket';
778 Queue => $args{'queue'},
779 Subject => $args{'subject'},
780 Status => $args{'status'} || 'new',
782 Starts => $args{'starts'},
783 Started => $args{'started'},
784 Resolved => $args{'resolved'},
785 Owner => $args{'owner'},
786 Requestor => $args{'requestor'},
788 AdminCc => $args{'admincc'},
789 TimeWorked => $args{'timeworked'},
790 TimeEstimated => $args{'timeestimated'},
791 TimeLeft => $args{'timeleft'},
792 InitialPriority => $args{'initialpriority'} || 0,
793 FinalPriority => $args{'finalpriority'} || 0,
794 SquelchMailTo => $args{'squelchmailto'},
795 Type => $args{'type'},
798 if ( $args{content} ) {
799 my $mimeobj = MIME::Entity->new();
801 Type => $args{'contenttype'} || 'text/plain',
802 Data => $args{'content'}
804 $ticketargs{MIMEObj} = $mimeobj;
805 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
808 foreach my $tag ( keys(%args) ) {
809 # if the tag was added later, skip it
810 my $orig_tag = $original_tags{$tag} or next;
811 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
812 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
813 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
814 my $cf = RT::CustomField->new( $self->CurrentUser );
815 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
816 $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
818 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
819 } elsif ($orig_tag) {
820 my $cf = RT::CustomField->new( $self->CurrentUser );
821 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
822 $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
824 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
829 $self->GetDeferred( \%args, $template_id, $links, $postponed );
831 return $TicketObj, \%ticketargs;
835 =head2 _ParseXSVTemplate
837 Parses a tab or comma delimited template. Should only ever be called by Parse
841 sub _ParseXSVTemplate {
845 use Regexp::Common qw(delimited);
846 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
849 if ( $first =~ /\t/ ) {
854 my @fields = split( /$delimiter/, $first );
856 my $delimiter_re = qr[$delimiter];
857 my $justquoted = qr[$RE{quoted}];
859 # Used to generate automatic template ids
864 $content =~ s/^(\s*\r?\n)+//;
866 # Keep track of Queue and Requestor, so we can provide defaults
870 # The template for this line
873 # What column we're on
876 # If the last iteration was the end of the line
883 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
886 # Strip off quotes, if they exist
888 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
889 substr( $value, 0, 1 ) = "";
890 substr( $value, -1, 1 ) = "";
893 # What column is this?
894 my $field = $fields[$i++];
895 next COLUMN unless $field =~ /\S/;
899 if ( $field =~ /^id$/i ) {
900 # Special case if this is the ID column
901 if ( $value =~ /^\d+$/ ) {
902 $template_id = 'update-' . $value;
903 push @{ $self->{'update_tickets'} }, $template_id;
904 } elsif ( $value =~ /^#base-(\d+)$/ ) {
905 $template_id = 'base-' . $1;
906 push @{ $self->{'base_tickets'} }, $template_id;
907 } elsif ( $value =~ /\S/ ) {
908 $template_id = 'create-' . $value;
909 push @{ $self->{'create_tickets'} }, $template_id;
913 if ( $field =~ /^Body$/i
914 || $field =~ /^Data$/i
915 || $field =~ /^Message$/i )
918 } elsif ( $field =~ /^Summary$/i ) {
920 } elsif ( $field =~ /^Queue$/i ) {
921 # Note that we found a queue
923 $value ||= $args{'Queue'};
924 } elsif ( $field =~ /^Requestors?$/i ) {
925 $field = 'Requestor'; # Remove plural
926 # Note that we found a requestor
928 $value ||= $args{'Requestor'};
931 # Tack onto the end of the template
932 $template .= $field . ": ";
933 $template .= (defined $value ? $value : "");
935 $template .= "ENDOFCONTENT\n"
936 if $field =~ /^Content$/i;
941 next unless $template;
943 # If we didn't find a queue of requestor, tack on the defaults
944 if ( !$queue && $args{'Queue'} ) {
945 $template .= "Queue: $args{'Queue'}\n";
947 if ( !$requestor && $args{'Requestor'} ) {
948 $template .= "Requestor: $args{'Requestor'}\n";
951 # If we never found an ID, come up with one
952 unless ($template_id) {
953 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
954 $template_id = "create-auto-$autoid";
955 # Also, it's a ticket to create
956 push @{ $self->{'create_tickets'} }, $template_id;
959 # Save the template we generated
960 $self->{'templates'}->{$template_id} = $template;
970 my $postponed = shift;
972 # Deferred processing
976 { DependsOn => $args->{'dependson'},
977 DependedOnBy => $args->{'dependedonby'},
978 RefersTo => $args->{'refersto'},
979 ReferredToBy => $args->{'referredtoby'},
980 Children => $args->{'children'},
981 Parents => $args->{'parents'},
987 # Status is postponed so we don't violate dependencies
988 $id, { Status => $args->{'status'}, }
992 sub GetUpdateTemplate {
997 $string .= "Queue: " . $t->QueueObj->Name . "\n";
998 $string .= "Subject: " . $t->Subject . "\n";
999 $string .= "Status: " . $t->Status . "\n";
1000 $string .= "UpdateType: correspond\n";
1001 $string .= "Content: \n";
1002 $string .= "ENDOFCONTENT\n";
1003 $string .= "Due: " . $t->DueObj->AsString . "\n";
1004 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1005 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1006 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1007 $string .= "Owner: " . $t->OwnerObj->Name . "\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";
1017 foreach my $type ( sort keys %LINKTYPEMAP ) {
1019 # don't display duplicates
1020 if ( $type eq "HasMember"
1021 || $type eq "Members"
1022 || $type eq "MemberOf" )
1026 $string .= "$type: ";
1028 my $mode = $LINKTYPEMAP{$type}->{Mode};
1029 my $method = $LINKTYPEMAP{$type}->{Type};
1032 while ( my $link = $t->$method->Next ) {
1033 $links .= ", " if $links;
1035 my $object = $mode . "Obj";
1036 my $member = $link->$object;
1037 $links .= $member->Id if $member;
1046 sub GetBaseTemplate {
1051 $string .= "Queue: " . $t->Queue . "\n";
1052 $string .= "Subject: " . $t->Subject . "\n";
1053 $string .= "Status: " . $t->Status . "\n";
1054 $string .= "Due: " . $t->DueObj->Unix . "\n";
1055 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1056 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1057 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1058 $string .= "Owner: " . $t->Owner . "\n";
1059 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1060 $string .= "Cc: " . $t->CcAddresses . "\n";
1061 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1062 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1063 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1064 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1065 $string .= "InitialPriority: " . $t->Priority . "\n";
1066 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1071 sub GetCreateTemplate {
1076 $string .= "Queue: General\n";
1077 $string .= "Subject: \n";
1078 $string .= "Status: new\n";
1079 $string .= "Content: \n";
1080 $string .= "ENDOFCONTENT\n";
1081 $string .= "Due: \n";
1082 $string .= "Starts: \n";
1083 $string .= "Started: \n";
1084 $string .= "Resolved: \n";
1085 $string .= "Owner: \n";
1086 $string .= "Requestor: \n";
1087 $string .= "Cc: \n";
1088 $string .= "AdminCc:\n";
1089 $string .= "TimeWorked: \n";
1090 $string .= "TimeEstimated: \n";
1091 $string .= "TimeLeft: \n";
1092 $string .= "InitialPriority: \n";
1093 $string .= "FinalPriority: \n";
1095 foreach my $type ( keys %LINKTYPEMAP ) {
1097 # don't display duplicates
1098 if ( $type eq "HasMember"
1099 || $type eq 'Members'
1100 || $type eq 'MemberOf' )
1104 $string .= "$type: \n";
1109 sub UpdateWatchers {
1116 foreach my $type (qw(Requestor Cc AdminCc)) {
1117 my $method = $type . 'Addresses';
1118 my $oldaddr = $ticket->$method;
1120 # Skip unless we have a defined field
1121 next unless defined $args->{$type};
1122 my $newaddr = $args->{$type};
1124 my @old = split( /,\s*/, $oldaddr );
1126 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1127 # Sometimes these are email addresses, sometimes they're
1128 # users. Try to guess which is which, as we want to deal
1129 # with email addresses if at all possible.
1133 # It doesn't look like an email address. Try to load it.
1134 my $user = RT::User->new($self->CurrentUser);
1137 push @new, $user->EmailAddress;
1144 my %oldhash = map { $_ => 1 } @old;
1145 my %newhash = map { $_ => 1 } @new;
1147 my @add = grep( !defined $oldhash{$_}, @new );
1148 my @delete = grep( !defined $newhash{$_}, @old );
1151 my ( $val, $msg ) = $ticket->AddWatcher(
1157 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1161 my ( $val, $msg ) = $ticket->DeleteWatcher(
1166 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1172 sub UpdateCustomFields {
1178 foreach my $arg (keys %{$args}) {
1179 next unless $arg =~ /^CustomField-(\d+)$/;
1182 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1183 $CustomFieldObj->SetContextObject( $ticket );
1184 $CustomFieldObj->LoadById($cf);
1187 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1188 @values = ($args->{$arg});
1190 @values = split /\n/, $args->{$arg};
1193 if ( ($CustomFieldObj->Type eq 'Freeform'
1194 && ! $CustomFieldObj->SingleValue) ||
1195 $CustomFieldObj->Type =~ /text/i) {
1196 foreach my $val (@values) {
1201 foreach my $value (@values) {
1202 next unless length($value);
1203 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1207 push ( @results, $msg );
1216 my $postponed = shift;
1218 # postprocessing: add links
1220 while ( my $template_id = shift(@$links) ) {
1221 my $ticket = $T::Tickets{$template_id};
1222 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1223 my %args = %{ shift(@$links) };
1225 foreach my $type ( keys %LINKTYPEMAP ) {
1226 next unless ( defined $args{$type} );
1228 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1232 if ( $link =~ /^TOP$/i ) {
1233 $RT::Logger->debug( "Building $type link for $link: "
1234 . $T::Tickets{TOP}->Id );
1235 $link = $T::Tickets{TOP}->Id;
1237 } elsif ( $link !~ m/^\d+$/ ) {
1238 my $key = "create-$link";
1239 if ( !exists $T::Tickets{$key} ) {
1241 "Skipping $type link for $key (non-existent)");
1244 $RT::Logger->debug( "Building $type link for $link: "
1245 . $T::Tickets{$key}->Id );
1246 $link = $T::Tickets{$key}->Id;
1248 $RT::Logger->debug("Building $type link for $link");
1251 my ( $wval, $wmsg ) = $ticket->AddLink(
1252 Type => $LINKTYPEMAP{$type}->{'Type'},
1253 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1257 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1260 # push @non_fatal_errors, $wmsg unless ($wval);
1266 # postponed actions -- Status only, currently
1267 while ( my $template_id = shift(@$postponed) ) {
1268 my $ticket = $T::Tickets{$template_id};
1269 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1270 my %args = %{ shift(@$postponed) };
1271 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1276 RT::Base->_ImportOverlays();