1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@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
211 Content => content. Can extend to multiple lines. Everything
212 within a template after a Content: header is treated
213 as content until we hit a line containing only
215 ContentType => the content-type of the Content field. Defaults to
217 UpdateType => 'correspond' or 'comment'; used in conjunction with
218 'content' if this is an update. Defaults to
221 CustomField-<id#> => custom field value
222 CF-name => custom field value
223 CustomField-name => custom field value
225 Fields marked with an * are required.
227 Fields marked with a + may have multiple values, simply
228 by repeating the fieldname on a new line with an additional value.
230 Fields marked with a ! are postponed to be processed after all
231 tickets in the same actions are created. Except for 'Status', those
232 field can also take a ticket name within the same action (i.e.
233 the identifiers after ==Create-Ticket), instead of raw Ticket ID
236 When parsed, field names are converted to lowercase and have -s stripped.
237 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
238 be treated as the same thing.
245 Jesse Vincent <jesse@bestpractical.com>
293 # {{{ Scrip methods (Commit, Prepare)
296 #Do what we need to do and send it out.
300 # Create all the tickets we care about
301 return (1) unless $self->TicketObj->Type eq 'ticket';
303 $self->CreateByTemplate( $self->TicketObj );
304 $self->UpdateByTemplate( $self->TicketObj );
315 unless ( $self->TemplateObj ) {
316 $RT::Logger->warning("No template object handed to $self");
319 unless ( $self->TransactionObj ) {
320 $RT::Logger->warning("No transaction object handed to $self");
324 unless ( $self->TicketObj ) {
325 $RT::Logger->warning("No ticket object handed to $self");
330 Content => $self->TemplateObj->Content,
341 sub CreateByTemplate {
345 $RT::Logger->debug("In CreateByTemplate");
349 # XXX: cargo cult programming that works. i'll be back.
351 local %T::Tickets = %T::Tickets;
352 local $T::TOP = $T::TOP;
353 local $T::ID = $T::ID;
354 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
357 my ( @links, @postponed );
358 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
359 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
362 $T::ID = $template_id;
363 @T::AllID = @{ $self->{'create_tickets'} };
365 ( $T::Tickets{$template_id}, $ticketargs )
366 = $self->ParseLines( $template_id, \@links, \@postponed );
368 # Now we have a %args to work with.
369 # Make sure we have at least the minimum set of
370 # reasonable data and do our thang
372 my ( $id, $transid, $msg )
373 = $T::Tickets{$template_id}->Create(%$ticketargs);
375 foreach my $res ( split( '\n', $msg ) ) {
377 $T::Tickets{$template_id}
378 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
382 if ( $self->TicketObj ) {
383 $msg = "Couldn't create related ticket $template_id for "
384 . $self->TicketObj->Id . " "
387 $msg = "Couldn't create ticket $template_id " . $msg;
390 $RT::Logger->error($msg);
394 $RT::Logger->debug("Assigned $template_id with $id");
395 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
397 && $T::Tickets{$template_id}->can('SetOriginObj');
401 $self->PostProcess( \@links, \@postponed );
406 sub UpdateByTemplate {
410 # XXX: cargo cult programming that works. i'll be back.
413 local %T::Tickets = %T::Tickets;
414 local $T::ID = $T::ID;
417 my ( @links, @postponed );
418 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
419 $RT::Logger->debug("Update Workflow: processing $template_id");
421 $T::ID = $template_id;
422 @T::AllID = @{ $self->{'update_tickets'} };
424 ( $T::Tickets{$template_id}, $ticketargs )
425 = $self->ParseLines( $template_id, \@links, \@postponed );
427 # Now we have a %args to work with.
428 # Make sure we have at least the minimum set of
429 # reasonable data and do our thang
446 my $id = $template_id;
447 $id =~ s/update-(\d+).*/$1/;
448 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
451 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
452 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
456 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
458 $template_id =~ m/^update-(.*)/;
459 my $base_id = "base-$1";
460 my $base = $self->{'templates'}->{$base_id};
464 $current =~ s/\n+$//;
466 # If we have no base template, set what we can.
467 if ( $base ne $current ) {
469 "Could not update ticket "
470 . $T::Tickets{$template_id}->Id
471 . ": Ticket has changed";
475 push @results, $T::Tickets{$template_id}->Update(
476 AttributesRef => \@attribs,
477 ARGSRef => $ticketargs
480 if ( $ticketargs->{'Owner'} ) {
481 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
482 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
486 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
489 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
491 next unless $ticketargs->{'MIMEObj'};
492 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
493 my ( $Transaction, $Description, $Object )
494 = $T::Tickets{$template_id}->Comment(
495 BccMessageTo => $ticketargs->{'Bcc'},
496 MIMEObj => $ticketargs->{'MIMEObj'},
497 TimeTaken => $ticketargs->{'TimeWorked'}
500 $T::Tickets{$template_id}
501 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
504 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
505 my ( $Transaction, $Description, $Object )
506 = $T::Tickets{$template_id}->Correspond(
507 BccMessageTo => $ticketargs->{'Bcc'},
508 MIMEObj => $ticketargs->{'MIMEObj'},
509 TimeTaken => $ticketargs->{'TimeWorked'}
512 $T::Tickets{$template_id}
513 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
519 $T::Tickets{$template_id}->loc(
520 "Update type was neither correspondence nor comment.")
522 . $T::Tickets{$template_id}->loc("Update not recorded.")
527 $self->PostProcess( \@links, \@postponed );
532 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
534 Parse a template from TEMPLATE_CONTENT
536 If $active is set to true, then we'll use Text::Template to parse the templates,
537 allowing you to embed active perl in your templates.
547 _ActiveContent => undef,
551 if ( $args{'_ActiveContent'} ) {
552 $self->{'UsePerlTextTemplate'} = 1;
555 $self->{'UsePerlTextTemplate'} = 0;
558 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
559 $self->_ParseMultilineTemplate(%args);
560 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
561 $self->_ParseXSVTemplate(%args);
566 =head2 _ParseMultilineTemplate
568 Parses mulitline templates. Things like:
572 Takes the same arguments as Parse
576 sub _ParseMultilineTemplate {
581 my ( $queue, $requestor );
582 $RT::Logger->debug("Line: ===");
583 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
585 $RT::Logger->debug("Line: $line");
586 if ( $line =~ /^===/ ) {
587 if ( $template_id && !$queue && $args{'Queue'} ) {
588 $self->{'templates'}->{$template_id}
589 .= "Queue: $args{'Queue'}\n";
591 if ( $template_id && !$requestor && $args{'Requestor'} ) {
592 $self->{'templates'}->{$template_id}
593 .= "Requestor: $args{'Requestor'}\n";
598 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
599 $template_id = "create-$1";
600 $RT::Logger->debug("**** Create ticket: $template_id");
601 push @{ $self->{'create_tickets'} }, $template_id;
602 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
603 $template_id = "update-$1";
604 $RT::Logger->debug("**** Update ticket: $template_id");
605 push @{ $self->{'update_tickets'} }, $template_id;
606 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
607 $template_id = "base-$1";
608 $RT::Logger->debug("**** Base ticket: $template_id");
609 push @{ $self->{'base_tickets'} }, $template_id;
610 } elsif ( $line =~ /^===#.*$/ ) { # a comment
613 if ( $line =~ /^Queue:(.*)/i ) {
618 if ( !$value && $args{'Queue'} ) {
619 $value = $args{'Queue'};
620 $line = "Queue: $value";
623 if ( $line =~ /^Requestors?:(.*)/i ) {
628 if ( !$value && $args{'Requestor'} ) {
629 $value = $args{'Requestor'};
630 $line = "Requestor: $value";
633 $self->{'templates'}->{$template_id} .= $line . "\n";
636 if ( $template_id && !$queue && $args{'Queue'} ) {
637 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
643 my $template_id = shift;
645 my $postponed = shift;
647 my $content = $self->{'templates'}->{$template_id};
649 if ( $self->{'UsePerlTextTemplate'} ) {
652 "Workflow: evaluating\n$self->{templates}{$template_id}");
654 my $template = Text::Template->new(
660 $content = $template->fill_in(
663 $err = {@_}->{error};
667 $RT::Logger->debug("Workflow: yielding $content");
670 $RT::Logger->error( "Ticket creation failed: " . $err );
671 while ( my ( $k, $v ) = each %T::X ) {
673 "Eliminating $template_id from ${k}'s parents.");
674 delete $v->{$template_id};
680 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
684 my @lines = ( split( /\n/, $content ) );
685 while ( defined( my $line = shift @lines ) ) {
686 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
688 my $original_tag = $1;
689 my $tag = lc($original_tag);
691 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
693 $original_tags{$tag} = $original_tag;
695 if ( ref( $args{$tag} ) )
696 { #If it's an array, we want to push the value
697 push @{ $args{$tag} }, $value;
698 } elsif ( defined( $args{$tag} ) )
699 { #if we're about to get a second value, make it an array
700 $args{$tag} = [ $args{$tag}, $value ];
701 } else { #if there's nothing there, just set the value
702 $args{$tag} = $value;
705 if ( $tag =~ /^content$/i ) { #just build up the content
706 # convert it to an array
707 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
708 while ( defined( my $l = shift @lines ) ) {
709 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
710 push @{ $args{'content'} }, $l . "\n";
713 # if it's not content, strip leading and trailing spaces
715 $args{$tag} =~ s/^\s+//g;
716 $args{$tag} =~ s/\s+$//g;
718 if (($tag =~ /^(requestor|cc|admincc)$/i or grep {lc $_ eq $tag} keys %LINKTYPEMAP) and $args{$tag} =~ /,/) {
719 $args{$tag} = [ split /,\s*/, $args{$tag} ];
725 foreach my $date qw(due starts started resolved) {
726 my $dateobj = RT::Date->new( $self->CurrentUser );
727 next unless $args{$date};
728 if ( $args{$date} =~ /^\d+$/ ) {
729 $dateobj->Set( Format => 'unix', Value => $args{$date} );
732 $dateobj->Set( Format => 'iso', Value => $args{$date} );
734 if ($@ or $dateobj->Unix <= 0) {
735 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
738 $args{$date} = $dateobj->ISO;
741 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
744 $args{'type'} ||= 'ticket';
747 Queue => $args{'queue'},
748 Subject => $args{'subject'},
749 Status => $args{'status'} || 'new',
751 Starts => $args{'starts'},
752 Started => $args{'started'},
753 Resolved => $args{'resolved'},
754 Owner => $args{'owner'},
755 Requestor => $args{'requestor'},
757 AdminCc => $args{'admincc'},
758 TimeWorked => $args{'timeworked'},
759 TimeEstimated => $args{'timeestimated'},
760 TimeLeft => $args{'timeleft'},
761 InitialPriority => $args{'initialpriority'} || 0,
762 FinalPriority => $args{'finalpriority'} || 0,
763 SquelchMailTo => $args{'squelchmailto'},
764 Type => $args{'type'},
768 if ( $args{content} ) {
769 my $mimeobj = MIME::Entity->new();
771 Type => $args{'contenttype'} || 'text/plain',
772 Data => $args{'content'}
774 $ticketargs{MIMEObj} = $mimeobj;
775 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
778 foreach my $tag ( keys(%args) ) {
779 # if the tag was added later, skip it
780 my $orig_tag = $original_tags{$tag} or next;
781 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
782 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
783 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.*)$/i ) {
784 my $cf = RT::CustomField->new( $self->CurrentUser );
785 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
786 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
787 } elsif ($orig_tag) {
788 my $cf = RT::CustomField->new( $self->CurrentUser );
789 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
790 next unless ($cf->id) ;
791 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
796 $self->GetDeferred( \%args, $template_id, $links, $postponed );
798 return $TicketObj, \%ticketargs;
802 =head2 _ParseXSVTemplate
804 Parses a tab or comma delimited template. Should only ever be called by Parse
808 sub _ParseXSVTemplate {
812 use Regexp::Common qw(delimited);
813 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
816 if ( $first =~ /\t/ ) {
821 my @fields = split( /$delimiter/, $first );
823 my $delimiter_re = qr[$delimiter];
824 my $justquoted = qr[$RE{quoted}];
826 # Used to generate automatic template ids
831 $content =~ s/^(\s*\r?\n)+//;
833 # Keep track of Queue and Requestor, so we can provide defaults
837 # The template for this line
840 # What column we're on
843 # If the last iteration was the end of the line
850 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
853 # Strip off quotes, if they exist
855 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
856 substr( $value, 0, 1 ) = "";
857 substr( $value, -1, 1 ) = "";
860 # What column is this?
861 my $field = $fields[$i++];
862 next COLUMN unless $field =~ /\S/;
866 if ( $field =~ /^id$/i ) {
867 # Special case if this is the ID column
868 if ( $value =~ /^\d+$/ ) {
869 $template_id = 'update-' . $value;
870 push @{ $self->{'update_tickets'} }, $template_id;
871 } elsif ( $value =~ /^#base-(\d+)$/ ) {
872 $template_id = 'base-' . $1;
873 push @{ $self->{'base_tickets'} }, $template_id;
874 } elsif ( $value =~ /\S/ ) {
875 $template_id = 'create-' . $value;
876 push @{ $self->{'create_tickets'} }, $template_id;
880 if ( $field =~ /^Body$/i
881 || $field =~ /^Data$/i
882 || $field =~ /^Message$/i )
885 } elsif ( $field =~ /^Summary$/i ) {
887 } elsif ( $field =~ /^Queue$/i ) {
888 # Note that we found a queue
890 $value ||= $args{'Queue'};
891 } elsif ( $field =~ /^Requestors?$/i ) {
892 $field = 'Requestor'; # Remove plural
893 # Note that we found a requestor
895 $value ||= $args{'Requestor'};
898 # Tack onto the end of the template
899 $template .= $field . ": ";
900 $template .= (defined $value ? $value : "");
902 $template .= "ENDOFCONTENT\n"
903 if $field =~ /^Content$/i;
908 next unless $template;
910 # If we didn't find a queue of requestor, tack on the defaults
911 if ( !$queue && $args{'Queue'} ) {
912 $template .= "Queue: $args{'Queue'}\n";
914 if ( !$requestor && $args{'Requestor'} ) {
915 $template .= "Requestor: $args{'Requestor'}\n";
918 # If we never found an ID, come up with one
919 unless ($template_id) {
920 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
921 $template_id = "create-auto-$autoid";
922 # Also, it's a ticket to create
923 push @{ $self->{'create_tickets'} }, $template_id;
926 # Save the template we generated
927 $self->{'templates'}->{$template_id} = $template;
937 my $postponed = shift;
939 # Deferred processing
943 { DependsOn => $args->{'dependson'},
944 DependedOnBy => $args->{'dependedonby'},
945 RefersTo => $args->{'refersto'},
946 ReferredToBy => $args->{'referredtoby'},
947 Children => $args->{'children'},
948 Parents => $args->{'parents'},
954 # Status is postponed so we don't violate dependencies
955 $id, { Status => $args->{'status'}, }
959 sub GetUpdateTemplate {
964 $string .= "Queue: " . $t->QueueObj->Name . "\n";
965 $string .= "Subject: " . $t->Subject . "\n";
966 $string .= "Status: " . $t->Status . "\n";
967 $string .= "UpdateType: correspond\n";
968 $string .= "Content: \n";
969 $string .= "ENDOFCONTENT\n";
970 $string .= "Due: " . $t->DueObj->AsString . "\n";
971 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
972 $string .= "Started: " . $t->StartedObj->AsString . "\n";
973 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
974 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
975 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
976 $string .= "Cc: " . $t->CcAddresses . "\n";
977 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
978 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
979 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
980 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
981 $string .= "InitialPriority: " . $t->Priority . "\n";
982 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
984 foreach my $type ( sort keys %LINKTYPEMAP ) {
986 # don't display duplicates
987 if ( $type eq "HasMember"
988 || $type eq "Members"
989 || $type eq "MemberOf" )
993 $string .= "$type: ";
995 my $mode = $LINKTYPEMAP{$type}->{Mode};
996 my $method = $LINKTYPEMAP{$type}->{Type};
999 while ( my $link = $t->$method->Next ) {
1000 $links .= ", " if $links;
1002 my $object = $mode . "Obj";
1003 my $member = $link->$object;
1004 $links .= $member->Id if $member;
1013 sub GetBaseTemplate {
1018 $string .= "Queue: " . $t->Queue . "\n";
1019 $string .= "Subject: " . $t->Subject . "\n";
1020 $string .= "Status: " . $t->Status . "\n";
1021 $string .= "Due: " . $t->DueObj->Unix . "\n";
1022 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1023 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1024 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1025 $string .= "Owner: " . $t->Owner . "\n";
1026 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1027 $string .= "Cc: " . $t->CcAddresses . "\n";
1028 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1029 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1030 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1031 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1032 $string .= "InitialPriority: " . $t->Priority . "\n";
1033 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1038 sub GetCreateTemplate {
1043 $string .= "Queue: General\n";
1044 $string .= "Subject: \n";
1045 $string .= "Status: new\n";
1046 $string .= "Content: \n";
1047 $string .= "ENDOFCONTENT\n";
1048 $string .= "Due: \n";
1049 $string .= "Starts: \n";
1050 $string .= "Started: \n";
1051 $string .= "Resolved: \n";
1052 $string .= "Owner: \n";
1053 $string .= "Requestor: \n";
1054 $string .= "Cc: \n";
1055 $string .= "AdminCc:\n";
1056 $string .= "TimeWorked: \n";
1057 $string .= "TimeEstimated: \n";
1058 $string .= "TimeLeft: \n";
1059 $string .= "InitialPriority: \n";
1060 $string .= "FinalPriority: \n";
1062 foreach my $type ( keys %LINKTYPEMAP ) {
1064 # don't display duplicates
1065 if ( $type eq "HasMember"
1066 || $type eq 'Members'
1067 || $type eq 'MemberOf' )
1071 $string .= "$type: \n";
1076 sub UpdateWatchers {
1083 foreach my $type qw(Requestor Cc AdminCc) {
1084 my $method = $type . 'Addresses';
1085 my $oldaddr = $ticket->$method;
1087 # Skip unless we have a defined field
1088 next unless defined $args->{$type};
1089 my $newaddr = $args->{$type};
1091 my @old = split( /,\s*/, $oldaddr );
1093 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1094 # Sometimes these are email addresses, sometimes they're
1095 # users. Try to guess which is which, as we want to deal
1096 # with email addresses if at all possible.
1100 # It doesn't look like an email address. Try to load it.
1101 my $user = RT::User->new($self->CurrentUser);
1104 push @new, $user->EmailAddress;
1111 my %oldhash = map { $_ => 1 } @old;
1112 my %newhash = map { $_ => 1 } @new;
1114 my @add = grep( !defined $oldhash{$_}, @new );
1115 my @delete = grep( !defined $newhash{$_}, @old );
1118 my ( $val, $msg ) = $ticket->AddWatcher(
1124 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1128 my ( $val, $msg ) = $ticket->DeleteWatcher(
1133 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1139 sub UpdateCustomFields {
1145 foreach my $arg (keys %{$args}) {
1146 next unless $arg =~ /^CustomField-(\d+)$/;
1149 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1150 $CustomFieldObj->LoadById($cf);
1153 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1154 @values = ($args->{$arg});
1156 @values = split /\n/, $args->{$arg};
1159 if ( ($CustomFieldObj->Type eq 'Freeform'
1160 && ! $CustomFieldObj->SingleValue) ||
1161 $CustomFieldObj->Type =~ /text/i) {
1162 foreach my $val (@values) {
1167 foreach my $value (@values) {
1168 next unless length($value);
1169 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1173 push ( @results, $msg );
1182 my $postponed = shift;
1184 # postprocessing: add links
1186 while ( my $template_id = shift(@$links) ) {
1187 my $ticket = $T::Tickets{$template_id};
1188 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1189 my %args = %{ shift(@$links) };
1191 foreach my $type ( keys %LINKTYPEMAP ) {
1192 next unless ( defined $args{$type} );
1194 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1198 if ( $link =~ /^TOP$/i ) {
1199 $RT::Logger->debug( "Building $type link for $link: "
1200 . $T::Tickets{TOP}->Id );
1201 $link = $T::Tickets{TOP}->Id;
1203 } elsif ( $link !~ m/^\d+$/ ) {
1204 my $key = "create-$link";
1205 if ( !exists $T::Tickets{$key} ) {
1207 "Skipping $type link for $key (non-existent)");
1210 $RT::Logger->debug( "Building $type link for $link: "
1211 . $T::Tickets{$key}->Id );
1212 $link = $T::Tickets{$key}->Id;
1214 $RT::Logger->debug("Building $type link for $link");
1217 my ( $wval, $wmsg ) = $ticket->AddLink(
1218 Type => $LINKTYPEMAP{$type}->{'Type'},
1219 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1223 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1226 # push @non_fatal_errors, $wmsg unless ($wval);
1232 # postponed actions -- Status only, currently
1233 while ( my $template_id = shift(@$postponed) ) {
1234 my $ticket = $T::Tickets{$template_id};
1235 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1236 my %args = %{ shift(@$postponed) };
1237 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1244 my $queues = RT::Queues->new($self->CurrentUser);
1247 while (my $queue = $queues->Next) {
1248 push @names, $queue->Id, $queue->Name;
1253 'label' => 'In queue',
1255 'options' => \@names
1260 eval "require RT::Action::CreateTickets_Vendor";
1261 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Vendor.pm} );
1262 eval "require RT::Action::CreateTickets_Local";
1263 die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Local.pm} );