1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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
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;
355 local $T::TransactionObj = $self->TransactionObj;
358 my ( @links, @postponed );
359 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
360 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
363 $T::ID = $template_id;
364 @T::AllID = @{ $self->{'create_tickets'} };
366 ( $T::Tickets{$template_id}, $ticketargs )
367 = $self->ParseLines( $template_id, \@links, \@postponed );
369 # Now we have a %args to work with.
370 # Make sure we have at least the minimum set of
371 # reasonable data and do our thang
373 my ( $id, $transid, $msg )
374 = $T::Tickets{$template_id}->Create(%$ticketargs);
376 foreach my $res ( split( '\n', $msg ) ) {
378 $T::Tickets{$template_id}
379 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
383 if ( $self->TicketObj ) {
384 $msg = "Couldn't create related ticket $template_id for "
385 . $self->TicketObj->Id . " "
388 $msg = "Couldn't create ticket $template_id " . $msg;
391 $RT::Logger->error($msg);
395 $RT::Logger->debug("Assigned $template_id with $id");
396 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
398 && $T::Tickets{$template_id}->can('SetOriginObj');
402 $self->PostProcess( \@links, \@postponed );
407 sub UpdateByTemplate {
411 # XXX: cargo cult programming that works. i'll be back.
414 local %T::Tickets = %T::Tickets;
415 local $T::ID = $T::ID;
418 my ( @links, @postponed );
419 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
420 $RT::Logger->debug("Update Workflow: processing $template_id");
422 $T::ID = $template_id;
423 @T::AllID = @{ $self->{'update_tickets'} };
425 ( $T::Tickets{$template_id}, $ticketargs )
426 = $self->ParseLines( $template_id, \@links, \@postponed );
428 # Now we have a %args to work with.
429 # Make sure we have at least the minimum set of
430 # reasonable data and do our thang
447 my $id = $template_id;
448 $id =~ s/update-(\d+).*/$1/;
449 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
452 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
453 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
457 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
459 $template_id =~ m/^update-(.*)/;
460 my $base_id = "base-$1";
461 my $base = $self->{'templates'}->{$base_id};
465 $current =~ s/\n+$//;
467 # If we have no base template, set what we can.
468 if ( $base ne $current ) {
470 "Could not update ticket "
471 . $T::Tickets{$template_id}->Id
472 . ": Ticket has changed";
476 push @results, $T::Tickets{$template_id}->Update(
477 AttributesRef => \@attribs,
478 ARGSRef => $ticketargs
481 if ( $ticketargs->{'Owner'} ) {
482 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
483 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
487 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
490 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
492 next unless $ticketargs->{'MIMEObj'};
493 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
494 my ( $Transaction, $Description, $Object )
495 = $T::Tickets{$template_id}->Comment(
496 BccMessageTo => $ticketargs->{'Bcc'},
497 MIMEObj => $ticketargs->{'MIMEObj'},
498 TimeTaken => $ticketargs->{'TimeWorked'}
501 $T::Tickets{$template_id}
502 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
505 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
506 my ( $Transaction, $Description, $Object )
507 = $T::Tickets{$template_id}->Correspond(
508 BccMessageTo => $ticketargs->{'Bcc'},
509 MIMEObj => $ticketargs->{'MIMEObj'},
510 TimeTaken => $ticketargs->{'TimeWorked'}
513 $T::Tickets{$template_id}
514 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
520 $T::Tickets{$template_id}->loc(
521 "Update type was neither correspondence nor comment.")
523 . $T::Tickets{$template_id}->loc("Update not recorded.")
528 $self->PostProcess( \@links, \@postponed );
533 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
535 Parse a template from TEMPLATE_CONTENT
537 If $active is set to true, then we'll use Text::Template to parse the templates,
538 allowing you to embed active perl in your templates.
548 _ActiveContent => undef,
552 if ( $args{'_ActiveContent'} ) {
553 $self->{'UsePerlTextTemplate'} = 1;
556 $self->{'UsePerlTextTemplate'} = 0;
559 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
560 $self->_ParseMultilineTemplate(%args);
561 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
562 $self->_ParseXSVTemplate(%args);
567 =head2 _ParseMultilineTemplate
569 Parses mulitline templates. Things like:
573 Takes the same arguments as Parse
577 sub _ParseMultilineTemplate {
582 my ( $queue, $requestor );
583 $RT::Logger->debug("Line: ===");
584 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
586 $RT::Logger->debug("Line: $line");
587 if ( $line =~ /^===/ ) {
588 if ( $template_id && !$queue && $args{'Queue'} ) {
589 $self->{'templates'}->{$template_id}
590 .= "Queue: $args{'Queue'}\n";
592 if ( $template_id && !$requestor && $args{'Requestor'} ) {
593 $self->{'templates'}->{$template_id}
594 .= "Requestor: $args{'Requestor'}\n";
599 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
600 $template_id = "create-$1";
601 $RT::Logger->debug("**** Create ticket: $template_id");
602 push @{ $self->{'create_tickets'} }, $template_id;
603 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
604 $template_id = "update-$1";
605 $RT::Logger->debug("**** Update ticket: $template_id");
606 push @{ $self->{'update_tickets'} }, $template_id;
607 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
608 $template_id = "base-$1";
609 $RT::Logger->debug("**** Base ticket: $template_id");
610 push @{ $self->{'base_tickets'} }, $template_id;
611 } elsif ( $line =~ /^===#.*$/ ) { # a comment
614 if ( $line =~ /^Queue:(.*)/i ) {
619 if ( !$value && $args{'Queue'} ) {
620 $value = $args{'Queue'};
621 $line = "Queue: $value";
624 if ( $line =~ /^Requestors?:(.*)/i ) {
629 if ( !$value && $args{'Requestor'} ) {
630 $value = $args{'Requestor'};
631 $line = "Requestor: $value";
634 $self->{'templates'}->{$template_id} .= $line . "\n";
637 if ( $template_id && !$queue && $args{'Queue'} ) {
638 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
644 my $template_id = shift;
646 my $postponed = shift;
648 my $content = $self->{'templates'}->{$template_id};
650 if ( $self->{'UsePerlTextTemplate'} ) {
653 "Workflow: evaluating\n$self->{templates}{$template_id}");
655 my $template = Text::Template->new(
661 $content = $template->fill_in(
664 $err = {@_}->{error};
668 $RT::Logger->debug("Workflow: yielding $content");
671 $RT::Logger->error( "Ticket creation failed: " . $err );
672 while ( my ( $k, $v ) = each %T::X ) {
674 "Eliminating $template_id from ${k}'s parents.");
675 delete $v->{$template_id};
681 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
685 my @lines = ( split( /\n/, $content ) );
686 while ( defined( my $line = shift @lines ) ) {
687 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
689 my $original_tag = $1;
690 my $tag = lc($original_tag);
692 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
694 $original_tags{$tag} = $original_tag;
696 if ( ref( $args{$tag} ) )
697 { #If it's an array, we want to push the value
698 push @{ $args{$tag} }, $value;
699 } elsif ( defined( $args{$tag} ) )
700 { #if we're about to get a second value, make it an array
701 $args{$tag} = [ $args{$tag}, $value ];
702 } else { #if there's nothing there, just set the value
703 $args{$tag} = $value;
706 if ( $tag =~ /^content$/i ) { #just build up the content
707 # convert it to an array
708 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
709 while ( defined( my $l = shift @lines ) ) {
710 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
711 push @{ $args{'content'} }, $l . "\n";
714 # if it's not content, strip leading and trailing spaces
716 $args{$tag} =~ s/^\s+//g;
717 $args{$tag} =~ s/\s+$//g;
719 if (($tag =~ /^(requestor|cc|admincc)$/i or grep {lc $_ eq $tag} keys %LINKTYPEMAP) and $args{$tag} =~ /,/) {
720 $args{$tag} = [ split /,\s*/, $args{$tag} ];
726 foreach my $date (qw(due starts started resolved)) {
727 my $dateobj = RT::Date->new( $self->CurrentUser );
728 next unless $args{$date};
729 if ( $args{$date} =~ /^\d+$/ ) {
730 $dateobj->Set( Format => 'unix', Value => $args{$date} );
733 $dateobj->Set( Format => 'iso', Value => $args{$date} );
735 if ($@ or $dateobj->Unix <= 0) {
736 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
739 $args{$date} = $dateobj->ISO;
742 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
745 $args{'type'} ||= 'ticket';
748 Queue => $args{'queue'},
749 Subject => $args{'subject'},
750 Status => $args{'status'} || 'new',
752 Starts => $args{'starts'},
753 Started => $args{'started'},
754 Resolved => $args{'resolved'},
755 Owner => $args{'owner'},
756 Requestor => $args{'requestor'},
758 AdminCc => $args{'admincc'},
759 TimeWorked => $args{'timeworked'},
760 TimeEstimated => $args{'timeestimated'},
761 TimeLeft => $args{'timeleft'},
762 InitialPriority => $args{'initialpriority'} || 0,
763 FinalPriority => $args{'finalpriority'} || 0,
764 SquelchMailTo => $args{'squelchmailto'},
765 Type => $args{'type'},
769 if ( $args{content} ) {
770 my $mimeobj = MIME::Entity->new();
772 Type => $args{'contenttype'} || 'text/plain',
773 Data => $args{'content'}
775 $ticketargs{MIMEObj} = $mimeobj;
776 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
779 foreach my $tag ( keys(%args) ) {
780 # if the tag was added later, skip it
781 my $orig_tag = $original_tags{$tag} or next;
782 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
783 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
784 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.*)$/i ) {
785 my $cf = RT::CustomField->new( $self->CurrentUser );
786 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
787 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
788 } elsif ($orig_tag) {
789 my $cf = RT::CustomField->new( $self->CurrentUser );
790 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
791 next unless ($cf->id) ;
792 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
797 $self->GetDeferred( \%args, $template_id, $links, $postponed );
799 return $TicketObj, \%ticketargs;
803 =head2 _ParseXSVTemplate
805 Parses a tab or comma delimited template. Should only ever be called by Parse
809 sub _ParseXSVTemplate {
813 use Regexp::Common qw(delimited);
814 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
817 if ( $first =~ /\t/ ) {
822 my @fields = split( /$delimiter/, $first );
824 my $delimiter_re = qr[$delimiter];
825 my $justquoted = qr[$RE{quoted}];
827 # Used to generate automatic template ids
832 $content =~ s/^(\s*\r?\n)+//;
834 # Keep track of Queue and Requestor, so we can provide defaults
838 # The template for this line
841 # What column we're on
844 # If the last iteration was the end of the line
851 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
854 # Strip off quotes, if they exist
856 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
857 substr( $value, 0, 1 ) = "";
858 substr( $value, -1, 1 ) = "";
861 # What column is this?
862 my $field = $fields[$i++];
863 next COLUMN unless $field =~ /\S/;
867 if ( $field =~ /^id$/i ) {
868 # Special case if this is the ID column
869 if ( $value =~ /^\d+$/ ) {
870 $template_id = 'update-' . $value;
871 push @{ $self->{'update_tickets'} }, $template_id;
872 } elsif ( $value =~ /^#base-(\d+)$/ ) {
873 $template_id = 'base-' . $1;
874 push @{ $self->{'base_tickets'} }, $template_id;
875 } elsif ( $value =~ /\S/ ) {
876 $template_id = 'create-' . $value;
877 push @{ $self->{'create_tickets'} }, $template_id;
881 if ( $field =~ /^Body$/i
882 || $field =~ /^Data$/i
883 || $field =~ /^Message$/i )
886 } elsif ( $field =~ /^Summary$/i ) {
888 } elsif ( $field =~ /^Queue$/i ) {
889 # Note that we found a queue
891 $value ||= $args{'Queue'};
892 } elsif ( $field =~ /^Requestors?$/i ) {
893 $field = 'Requestor'; # Remove plural
894 # Note that we found a requestor
896 $value ||= $args{'Requestor'};
899 # Tack onto the end of the template
900 $template .= $field . ": ";
901 $template .= (defined $value ? $value : "");
903 $template .= "ENDOFCONTENT\n"
904 if $field =~ /^Content$/i;
909 next unless $template;
911 # If we didn't find a queue of requestor, tack on the defaults
912 if ( !$queue && $args{'Queue'} ) {
913 $template .= "Queue: $args{'Queue'}\n";
915 if ( !$requestor && $args{'Requestor'} ) {
916 $template .= "Requestor: $args{'Requestor'}\n";
919 # If we never found an ID, come up with one
920 unless ($template_id) {
921 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
922 $template_id = "create-auto-$autoid";
923 # Also, it's a ticket to create
924 push @{ $self->{'create_tickets'} }, $template_id;
927 # Save the template we generated
928 $self->{'templates'}->{$template_id} = $template;
938 my $postponed = shift;
940 # Deferred processing
944 { DependsOn => $args->{'dependson'},
945 DependedOnBy => $args->{'dependedonby'},
946 RefersTo => $args->{'refersto'},
947 ReferredToBy => $args->{'referredtoby'},
948 Children => $args->{'children'},
949 Parents => $args->{'parents'},
955 # Status is postponed so we don't violate dependencies
956 $id, { Status => $args->{'status'}, }
960 sub GetUpdateTemplate {
965 $string .= "Queue: " . $t->QueueObj->Name . "\n";
966 $string .= "Subject: " . $t->Subject . "\n";
967 $string .= "Status: " . $t->Status . "\n";
968 $string .= "UpdateType: correspond\n";
969 $string .= "Content: \n";
970 $string .= "ENDOFCONTENT\n";
971 $string .= "Due: " . $t->DueObj->AsString . "\n";
972 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
973 $string .= "Started: " . $t->StartedObj->AsString . "\n";
974 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
975 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
976 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
977 $string .= "Cc: " . $t->CcAddresses . "\n";
978 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
979 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
980 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
981 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
982 $string .= "InitialPriority: " . $t->Priority . "\n";
983 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
985 foreach my $type ( sort keys %LINKTYPEMAP ) {
987 # don't display duplicates
988 if ( $type eq "HasMember"
989 || $type eq "Members"
990 || $type eq "MemberOf" )
994 $string .= "$type: ";
996 my $mode = $LINKTYPEMAP{$type}->{Mode};
997 my $method = $LINKTYPEMAP{$type}->{Type};
1000 while ( my $link = $t->$method->Next ) {
1001 $links .= ", " if $links;
1003 my $object = $mode . "Obj";
1004 my $member = $link->$object;
1005 $links .= $member->Id if $member;
1014 sub GetBaseTemplate {
1019 $string .= "Queue: " . $t->Queue . "\n";
1020 $string .= "Subject: " . $t->Subject . "\n";
1021 $string .= "Status: " . $t->Status . "\n";
1022 $string .= "Due: " . $t->DueObj->Unix . "\n";
1023 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1024 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1025 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1026 $string .= "Owner: " . $t->Owner . "\n";
1027 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1028 $string .= "Cc: " . $t->CcAddresses . "\n";
1029 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1030 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1031 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1032 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1033 $string .= "InitialPriority: " . $t->Priority . "\n";
1034 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1039 sub GetCreateTemplate {
1044 $string .= "Queue: General\n";
1045 $string .= "Subject: \n";
1046 $string .= "Status: new\n";
1047 $string .= "Content: \n";
1048 $string .= "ENDOFCONTENT\n";
1049 $string .= "Due: \n";
1050 $string .= "Starts: \n";
1051 $string .= "Started: \n";
1052 $string .= "Resolved: \n";
1053 $string .= "Owner: \n";
1054 $string .= "Requestor: \n";
1055 $string .= "Cc: \n";
1056 $string .= "AdminCc:\n";
1057 $string .= "TimeWorked: \n";
1058 $string .= "TimeEstimated: \n";
1059 $string .= "TimeLeft: \n";
1060 $string .= "InitialPriority: \n";
1061 $string .= "FinalPriority: \n";
1063 foreach my $type ( keys %LINKTYPEMAP ) {
1065 # don't display duplicates
1066 if ( $type eq "HasMember"
1067 || $type eq 'Members'
1068 || $type eq 'MemberOf' )
1072 $string .= "$type: \n";
1077 sub UpdateWatchers {
1084 foreach my $type (qw(Requestor Cc AdminCc)) {
1085 my $method = $type . 'Addresses';
1086 my $oldaddr = $ticket->$method;
1088 # Skip unless we have a defined field
1089 next unless defined $args->{$type};
1090 my $newaddr = $args->{$type};
1092 my @old = split( /,\s*/, $oldaddr );
1094 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1095 # Sometimes these are email addresses, sometimes they're
1096 # users. Try to guess which is which, as we want to deal
1097 # with email addresses if at all possible.
1101 # It doesn't look like an email address. Try to load it.
1102 my $user = RT::User->new($self->CurrentUser);
1105 push @new, $user->EmailAddress;
1112 my %oldhash = map { $_ => 1 } @old;
1113 my %newhash = map { $_ => 1 } @new;
1115 my @add = grep( !defined $oldhash{$_}, @new );
1116 my @delete = grep( !defined $newhash{$_}, @old );
1119 my ( $val, $msg ) = $ticket->AddWatcher(
1125 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1129 my ( $val, $msg ) = $ticket->DeleteWatcher(
1134 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1140 sub UpdateCustomFields {
1146 foreach my $arg (keys %{$args}) {
1147 next unless $arg =~ /^CustomField-(\d+)$/;
1150 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1151 $CustomFieldObj->SetContextObject( $ticket );
1152 $CustomFieldObj->LoadById($cf);
1155 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1156 @values = ($args->{$arg});
1158 @values = split /\n/, $args->{$arg};
1161 if ( ($CustomFieldObj->Type eq 'Freeform'
1162 && ! $CustomFieldObj->SingleValue) ||
1163 $CustomFieldObj->Type =~ /text/i) {
1164 foreach my $val (@values) {
1169 foreach my $value (@values) {
1170 next unless length($value);
1171 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1175 push ( @results, $msg );
1184 my $postponed = shift;
1186 # postprocessing: add links
1188 while ( my $template_id = shift(@$links) ) {
1189 my $ticket = $T::Tickets{$template_id};
1190 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1191 my %args = %{ shift(@$links) };
1193 foreach my $type ( keys %LINKTYPEMAP ) {
1194 next unless ( defined $args{$type} );
1196 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1200 if ( $link =~ /^TOP$/i ) {
1201 $RT::Logger->debug( "Building $type link for $link: "
1202 . $T::Tickets{TOP}->Id );
1203 $link = $T::Tickets{TOP}->Id;
1205 } elsif ( $link !~ m/^\d+$/ ) {
1206 my $key = "create-$link";
1207 if ( !exists $T::Tickets{$key} ) {
1209 "Skipping $type link for $key (non-existent)");
1212 $RT::Logger->debug( "Building $type link for $link: "
1213 . $T::Tickets{$key}->Id );
1214 $link = $T::Tickets{$key}->Id;
1216 $RT::Logger->debug("Building $type link for $link");
1219 my ( $wval, $wmsg ) = $ticket->AddLink(
1220 Type => $LINKTYPEMAP{$type}->{'Type'},
1221 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1225 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1228 # push @non_fatal_errors, $wmsg unless ($wval);
1234 # postponed actions -- Status only, currently
1235 while ( my $template_id = shift(@$postponed) ) {
1236 my $ticket = $T::Tickets{$template_id};
1237 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1238 my %args = %{ shift(@$postponed) };
1239 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1246 my $queues = RT::Queues->new($self->CurrentUser);
1249 while (my $queue = $queues->Next) {
1250 push @names, $queue->Id, $queue->Name;
1255 'label' => 'In queue',
1257 'options' => \@names
1262 RT::Base->_ImportOverlays();