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'},
799 if ( $args{content} ) {
800 my $mimeobj = MIME::Entity->new();
802 Type => $args{'contenttype'} || 'text/plain',
803 Data => $args{'content'}
805 $ticketargs{MIMEObj} = $mimeobj;
806 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
809 foreach my $tag ( keys(%args) ) {
810 # if the tag was added later, skip it
811 my $orig_tag = $original_tags{$tag} or next;
812 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
813 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
814 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
815 my $cf = RT::CustomField->new( $self->CurrentUser );
816 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
817 $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
819 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
820 } elsif ($orig_tag) {
821 my $cf = RT::CustomField->new( $self->CurrentUser );
822 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
823 $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
825 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
830 $self->GetDeferred( \%args, $template_id, $links, $postponed );
832 return $TicketObj, \%ticketargs;
836 =head2 _ParseXSVTemplate
838 Parses a tab or comma delimited template. Should only ever be called by Parse
842 sub _ParseXSVTemplate {
846 use Regexp::Common qw(delimited);
847 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
850 if ( $first =~ /\t/ ) {
855 my @fields = split( /$delimiter/, $first );
857 my $delimiter_re = qr[$delimiter];
858 my $justquoted = qr[$RE{quoted}];
860 # Used to generate automatic template ids
865 $content =~ s/^(\s*\r?\n)+//;
867 # Keep track of Queue and Requestor, so we can provide defaults
871 # The template for this line
874 # What column we're on
877 # If the last iteration was the end of the line
884 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
887 # Strip off quotes, if they exist
889 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
890 substr( $value, 0, 1 ) = "";
891 substr( $value, -1, 1 ) = "";
894 # What column is this?
895 my $field = $fields[$i++];
896 next COLUMN unless $field =~ /\S/;
900 if ( $field =~ /^id$/i ) {
901 # Special case if this is the ID column
902 if ( $value =~ /^\d+$/ ) {
903 $template_id = 'update-' . $value;
904 push @{ $self->{'update_tickets'} }, $template_id;
905 } elsif ( $value =~ /^#base-(\d+)$/ ) {
906 $template_id = 'base-' . $1;
907 push @{ $self->{'base_tickets'} }, $template_id;
908 } elsif ( $value =~ /\S/ ) {
909 $template_id = 'create-' . $value;
910 push @{ $self->{'create_tickets'} }, $template_id;
914 if ( $field =~ /^Body$/i
915 || $field =~ /^Data$/i
916 || $field =~ /^Message$/i )
919 } elsif ( $field =~ /^Summary$/i ) {
921 } elsif ( $field =~ /^Queue$/i ) {
922 # Note that we found a queue
924 $value ||= $args{'Queue'};
925 } elsif ( $field =~ /^Requestors?$/i ) {
926 $field = 'Requestor'; # Remove plural
927 # Note that we found a requestor
929 $value ||= $args{'Requestor'};
932 # Tack onto the end of the template
933 $template .= $field . ": ";
934 $template .= (defined $value ? $value : "");
936 $template .= "ENDOFCONTENT\n"
937 if $field =~ /^Content$/i;
942 next unless $template;
944 # If we didn't find a queue of requestor, tack on the defaults
945 if ( !$queue && $args{'Queue'} ) {
946 $template .= "Queue: $args{'Queue'}\n";
948 if ( !$requestor && $args{'Requestor'} ) {
949 $template .= "Requestor: $args{'Requestor'}\n";
952 # If we never found an ID, come up with one
953 unless ($template_id) {
954 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
955 $template_id = "create-auto-$autoid";
956 # Also, it's a ticket to create
957 push @{ $self->{'create_tickets'} }, $template_id;
960 # Save the template we generated
961 $self->{'templates'}->{$template_id} = $template;
971 my $postponed = shift;
973 # Deferred processing
977 { DependsOn => $args->{'dependson'},
978 DependedOnBy => $args->{'dependedonby'},
979 RefersTo => $args->{'refersto'},
980 ReferredToBy => $args->{'referredtoby'},
981 Children => $args->{'children'},
982 Parents => $args->{'parents'},
988 # Status is postponed so we don't violate dependencies
989 $id, { Status => $args->{'status'}, }
993 sub GetUpdateTemplate {
998 $string .= "Queue: " . $t->QueueObj->Name . "\n";
999 $string .= "Subject: " . $t->Subject . "\n";
1000 $string .= "Status: " . $t->Status . "\n";
1001 $string .= "UpdateType: correspond\n";
1002 $string .= "Content: \n";
1003 $string .= "ENDOFCONTENT\n";
1004 $string .= "Due: " . $t->DueObj->AsString . "\n";
1005 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
1006 $string .= "Started: " . $t->StartedObj->AsString . "\n";
1007 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
1008 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
1009 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1010 $string .= "Cc: " . $t->CcAddresses . "\n";
1011 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1012 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1013 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1014 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1015 $string .= "InitialPriority: " . $t->Priority . "\n";
1016 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1018 foreach my $type ( sort keys %LINKTYPEMAP ) {
1020 # don't display duplicates
1021 if ( $type eq "HasMember"
1022 || $type eq "Members"
1023 || $type eq "MemberOf" )
1027 $string .= "$type: ";
1029 my $mode = $LINKTYPEMAP{$type}->{Mode};
1030 my $method = $LINKTYPEMAP{$type}->{Type};
1033 while ( my $link = $t->$method->Next ) {
1034 $links .= ", " if $links;
1036 my $object = $mode . "Obj";
1037 my $member = $link->$object;
1038 $links .= $member->Id if $member;
1047 sub GetBaseTemplate {
1052 $string .= "Queue: " . $t->Queue . "\n";
1053 $string .= "Subject: " . $t->Subject . "\n";
1054 $string .= "Status: " . $t->Status . "\n";
1055 $string .= "Due: " . $t->DueObj->Unix . "\n";
1056 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1057 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1058 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1059 $string .= "Owner: " . $t->Owner . "\n";
1060 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1061 $string .= "Cc: " . $t->CcAddresses . "\n";
1062 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1063 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1064 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1065 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1066 $string .= "InitialPriority: " . $t->Priority . "\n";
1067 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1072 sub GetCreateTemplate {
1077 $string .= "Queue: General\n";
1078 $string .= "Subject: \n";
1079 $string .= "Status: new\n";
1080 $string .= "Content: \n";
1081 $string .= "ENDOFCONTENT\n";
1082 $string .= "Due: \n";
1083 $string .= "Starts: \n";
1084 $string .= "Started: \n";
1085 $string .= "Resolved: \n";
1086 $string .= "Owner: \n";
1087 $string .= "Requestor: \n";
1088 $string .= "Cc: \n";
1089 $string .= "AdminCc:\n";
1090 $string .= "TimeWorked: \n";
1091 $string .= "TimeEstimated: \n";
1092 $string .= "TimeLeft: \n";
1093 $string .= "InitialPriority: \n";
1094 $string .= "FinalPriority: \n";
1096 foreach my $type ( keys %LINKTYPEMAP ) {
1098 # don't display duplicates
1099 if ( $type eq "HasMember"
1100 || $type eq 'Members'
1101 || $type eq 'MemberOf' )
1105 $string .= "$type: \n";
1110 sub UpdateWatchers {
1117 foreach my $type (qw(Requestor Cc AdminCc)) {
1118 my $method = $type . 'Addresses';
1119 my $oldaddr = $ticket->$method;
1121 # Skip unless we have a defined field
1122 next unless defined $args->{$type};
1123 my $newaddr = $args->{$type};
1125 my @old = split( /,\s*/, $oldaddr );
1127 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1128 # Sometimes these are email addresses, sometimes they're
1129 # users. Try to guess which is which, as we want to deal
1130 # with email addresses if at all possible.
1134 # It doesn't look like an email address. Try to load it.
1135 my $user = RT::User->new($self->CurrentUser);
1138 push @new, $user->EmailAddress;
1145 my %oldhash = map { $_ => 1 } @old;
1146 my %newhash = map { $_ => 1 } @new;
1148 my @add = grep( !defined $oldhash{$_}, @new );
1149 my @delete = grep( !defined $newhash{$_}, @old );
1152 my ( $val, $msg ) = $ticket->AddWatcher(
1158 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1162 my ( $val, $msg ) = $ticket->DeleteWatcher(
1167 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1173 sub UpdateCustomFields {
1179 foreach my $arg (keys %{$args}) {
1180 next unless $arg =~ /^CustomField-(\d+)$/;
1183 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1184 $CustomFieldObj->SetContextObject( $ticket );
1185 $CustomFieldObj->LoadById($cf);
1188 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1189 @values = ($args->{$arg});
1191 @values = split /\n/, $args->{$arg};
1194 if ( ($CustomFieldObj->Type eq 'Freeform'
1195 && ! $CustomFieldObj->SingleValue) ||
1196 $CustomFieldObj->Type =~ /text/i) {
1197 foreach my $val (@values) {
1202 foreach my $value (@values) {
1203 next unless length($value);
1204 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1208 push ( @results, $msg );
1217 my $postponed = shift;
1219 # postprocessing: add links
1221 while ( my $template_id = shift(@$links) ) {
1222 my $ticket = $T::Tickets{$template_id};
1223 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1224 my %args = %{ shift(@$links) };
1226 foreach my $type ( keys %LINKTYPEMAP ) {
1227 next unless ( defined $args{$type} );
1229 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1233 if ( $link =~ /^TOP$/i ) {
1234 $RT::Logger->debug( "Building $type link for $link: "
1235 . $T::Tickets{TOP}->Id );
1236 $link = $T::Tickets{TOP}->Id;
1238 } elsif ( $link !~ m/^\d+$/ ) {
1239 my $key = "create-$link";
1240 if ( !exists $T::Tickets{$key} ) {
1242 "Skipping $type link for $key (non-existent)");
1245 $RT::Logger->debug( "Building $type link for $link: "
1246 . $T::Tickets{$key}->Id );
1247 $link = $T::Tickets{$key}->Id;
1249 $RT::Logger->debug("Building $type link for $link");
1252 my ( $wval, $wmsg ) = $ticket->AddLink(
1253 Type => $LINKTYPEMAP{$type}->{'Type'},
1254 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1258 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1261 # push @non_fatal_errors, $wmsg unless ($wval);
1267 # postponed actions -- Status only, currently
1268 while ( my $template_id = shift(@$postponed) ) {
1269 my $ticket = $T::Tickets{$template_id};
1270 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1271 my %args = %{ shift(@$postponed) };
1272 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1279 my $queues = RT::Queues->new($self->CurrentUser);
1282 while (my $queue = $queues->Next) {
1283 push @names, $queue->Id, $queue->Name;
1288 'label' => 'In queue',
1290 'options' => \@names
1295 RT::Base->_ImportOverlays();