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 Content => $self->TemplateObj->Content,
338 sub CreateByTemplate {
342 $RT::Logger->debug("In CreateByTemplate");
346 # XXX: cargo cult programming that works. i'll be back.
348 local %T::Tickets = %T::Tickets;
349 local $T::TOP = $T::TOP;
350 local $T::ID = $T::ID;
351 $T::Tickets{'TOP'} = $T::TOP = $top if $top;
352 local $T::TransactionObj = $self->TransactionObj;
355 my ( @links, @postponed );
356 foreach my $template_id ( @{ $self->{'create_tickets'} } ) {
357 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP")
360 $T::ID = $template_id;
361 @T::AllID = @{ $self->{'create_tickets'} };
363 ( $T::Tickets{$template_id}, $ticketargs )
364 = $self->ParseLines( $template_id, \@links, \@postponed );
366 # Now we have a %args to work with.
367 # Make sure we have at least the minimum set of
368 # reasonable data and do our thang
370 my ( $id, $transid, $msg )
371 = $T::Tickets{$template_id}->Create(%$ticketargs);
373 foreach my $res ( split( '\n', $msg ) ) {
375 $T::Tickets{$template_id}
376 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': '
380 if ( $self->TicketObj ) {
381 $msg = "Couldn't create related ticket $template_id for "
382 . $self->TicketObj->Id . " "
385 $msg = "Couldn't create ticket $template_id " . $msg;
388 $RT::Logger->error($msg);
392 $RT::Logger->debug("Assigned $template_id with $id");
393 $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj )
395 && $T::Tickets{$template_id}->can('SetOriginObj');
399 $self->PostProcess( \@links, \@postponed );
404 sub UpdateByTemplate {
408 # XXX: cargo cult programming that works. i'll be back.
411 local %T::Tickets = %T::Tickets;
412 local $T::ID = $T::ID;
415 my ( @links, @postponed );
416 foreach my $template_id ( @{ $self->{'update_tickets'} } ) {
417 $RT::Logger->debug("Update Workflow: processing $template_id");
419 $T::ID = $template_id;
420 @T::AllID = @{ $self->{'update_tickets'} };
422 ( $T::Tickets{$template_id}, $ticketargs )
423 = $self->ParseLines( $template_id, \@links, \@postponed );
425 # Now we have a %args to work with.
426 # Make sure we have at least the minimum set of
427 # reasonable data and do our thang
444 my $id = $template_id;
445 $id =~ s/update-(\d+).*/$1/;
446 my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id);
449 $RT::Logger->error("Couldn't update ticket $template_id: " . $msg);
450 push @results, $self->loc( "Couldn't load ticket '[_1]'", $id );
454 my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} );
456 $template_id =~ m/^update-(.*)/;
457 my $base_id = "base-$1";
458 my $base = $self->{'templates'}->{$base_id};
462 $current =~ s/\n+$//;
464 # If we have no base template, set what we can.
465 if ( $base ne $current ) {
467 "Could not update ticket "
468 . $T::Tickets{$template_id}->Id
469 . ": Ticket has changed";
473 push @results, $T::Tickets{$template_id}->Update(
474 AttributesRef => \@attribs,
475 ARGSRef => $ticketargs
478 if ( $ticketargs->{'Owner'} ) {
479 ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force");
480 push @results, $msg unless $msg eq $self->loc("That user already owns that ticket");
484 $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs );
487 $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs );
489 next unless $ticketargs->{'MIMEObj'};
490 if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) {
491 my ( $Transaction, $Description, $Object )
492 = $T::Tickets{$template_id}->Comment(
493 BccMessageTo => $ticketargs->{'Bcc'},
494 MIMEObj => $ticketargs->{'MIMEObj'},
495 TimeTaken => $ticketargs->{'TimeWorked'}
498 $T::Tickets{$template_id}
499 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
502 } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) {
503 my ( $Transaction, $Description, $Object )
504 = $T::Tickets{$template_id}->Correspond(
505 BccMessageTo => $ticketargs->{'Bcc'},
506 MIMEObj => $ticketargs->{'MIMEObj'},
507 TimeTaken => $ticketargs->{'TimeWorked'}
510 $T::Tickets{$template_id}
511 ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id )
517 $T::Tickets{$template_id}->loc(
518 "Update type was neither correspondence nor comment.")
520 . $T::Tickets{$template_id}->loc("Update not recorded.")
525 $self->PostProcess( \@links, \@postponed );
530 =head2 Parse TEMPLATE_CONTENT, DEFAULT_QUEUE, DEFAULT_REQEUESTOR ACTIVE
532 Parse a template from TEMPLATE_CONTENT
534 If $active is set to true, then we'll use Text::Template to parse the templates,
535 allowing you to embed active perl in your templates.
545 _ActiveContent => undef,
549 if ( $args{'_ActiveContent'} ) {
550 $self->{'UsePerlTextTemplate'} = 1;
553 $self->{'UsePerlTextTemplate'} = 0;
556 if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) {
557 $self->_ParseMultilineTemplate(%args);
558 } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) {
559 $self->_ParseXSVTemplate(%args);
564 =head2 _ParseMultilineTemplate
566 Parses mulitline templates. Things like:
570 Takes the same arguments as Parse
574 sub _ParseMultilineTemplate {
581 my ( $queue, $requestor );
582 $RT::Logger->debug("Line: ===");
583 foreach my $line ( split( /\n/, $args{'Content'} ) ) {
585 $RT::Logger->debug( "Line: " . utf8::is_utf8($line)
586 ? Encode::encode_utf8($line)
588 if ( $line =~ /^===/ ) {
589 if ( $template_id && !$queue && $args{'Queue'} ) {
590 $self->{'templates'}->{$template_id}
591 .= "Queue: $args{'Queue'}\n";
593 if ( $template_id && !$requestor && $args{'Requestor'} ) {
594 $self->{'templates'}->{$template_id}
595 .= "Requestor: $args{'Requestor'}\n";
600 if ( $line =~ /^===Create-Ticket: (.*)$/ ) {
601 $template_id = "create-$1";
602 $RT::Logger->debug("**** Create ticket: $template_id");
603 push @{ $self->{'create_tickets'} }, $template_id;
604 } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) {
605 $template_id = "update-$1";
606 $RT::Logger->debug("**** Update ticket: $template_id");
607 push @{ $self->{'update_tickets'} }, $template_id;
608 } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) {
609 $template_id = "base-$1";
610 $RT::Logger->debug("**** Base ticket: $template_id");
611 push @{ $self->{'base_tickets'} }, $template_id;
612 } elsif ( $line =~ /^===#.*$/ ) { # a comment
615 if ( $line =~ /^Queue:(.*)/i ) {
620 if ( !$value && $args{'Queue'} ) {
621 $value = $args{'Queue'};
622 $line = "Queue: $value";
625 if ( $line =~ /^Requestors?:(.*)/i ) {
630 if ( !$value && $args{'Requestor'} ) {
631 $value = $args{'Requestor'};
632 $line = "Requestor: $value";
635 $self->{'templates'}->{$template_id} .= $line . "\n";
638 if ( $template_id && !$queue && $args{'Queue'} ) {
639 $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n";
645 my $template_id = shift;
647 my $postponed = shift;
649 my $content = $self->{'templates'}->{$template_id};
651 if ( $self->{'UsePerlTextTemplate'} ) {
654 "Workflow: evaluating\n$self->{templates}{$template_id}");
656 my $template = Text::Template->new(
662 $content = $template->fill_in(
665 $err = {@_}->{error};
669 $RT::Logger->debug("Workflow: yielding $content");
672 $RT::Logger->error( "Ticket creation failed: " . $err );
673 while ( my ( $k, $v ) = each %T::X ) {
675 "Eliminating $template_id from ${k}'s parents.");
676 delete $v->{$template_id};
682 my $TicketObj ||= RT::Ticket->new( $self->CurrentUser );
686 my @lines = ( split( /\n/, $content ) );
687 while ( defined( my $line = shift @lines ) ) {
688 if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) {
690 my $original_tag = $1;
691 my $tag = lc($original_tag);
693 $tag =~ s/^(requestor|cc|admincc)s?$/$1/i;
695 $original_tags{$tag} = $original_tag;
697 if ( ref( $args{$tag} ) )
698 { #If it's an array, we want to push the value
699 push @{ $args{$tag} }, $value;
700 } elsif ( defined( $args{$tag} ) )
701 { #if we're about to get a second value, make it an array
702 $args{$tag} = [ $args{$tag}, $value ];
703 } else { #if there's nothing there, just set the value
704 $args{$tag} = $value;
707 if ( $tag =~ /^content$/i ) { #just build up the content
708 # convert it to an array
709 $args{$tag} = defined($value) ? [ $value . "\n" ] : [];
710 while ( defined( my $l = shift @lines ) ) {
711 last if ( $l =~ /^ENDOFCONTENT\s*$/ );
712 push @{ $args{'content'} }, $l . "\n";
715 # if it's not content, strip leading and trailing spaces
717 $args{$tag} =~ s/^\s+//g;
718 $args{$tag} =~ s/\s+$//g;
721 ($tag =~ /^(requestor|cc|admincc)(group)?$/i
722 or grep {lc $_ eq $tag} keys %LINKTYPEMAP)
723 and $args{$tag} =~ /,/
725 $args{$tag} = [ split /,\s*/, $args{$tag} ];
731 foreach my $date (qw(due starts started resolved)) {
732 my $dateobj = RT::Date->new( $self->CurrentUser );
733 next unless $args{$date};
734 if ( $args{$date} =~ /^\d+$/ ) {
735 $dateobj->Set( Format => 'unix', Value => $args{$date} );
738 $dateobj->Set( Format => 'iso', Value => $args{$date} );
740 if ($@ or $dateobj->Unix <= 0) {
741 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
744 $args{$date} = $dateobj->ISO;
747 foreach my $role (qw(requestor cc admincc)) {
748 next unless my $value = $args{ $role . 'group' };
750 my $group = RT::Group->new( $self->CurrentUser );
751 $group->LoadUserDefinedGroup( $value );
752 unless ( $group->id ) {
753 $RT::Logger->error("Couldn't load group '$value'");
757 $args{ $role } = $args{ $role } ? [$args{ $role }] : []
758 unless ref $args{ $role };
759 push @{ $args{ $role } }, $group->PrincipalObj->id;
762 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses
765 $args{'type'} ||= 'ticket';
768 Queue => $args{'queue'},
769 Subject => $args{'subject'},
770 Status => $args{'status'} || 'new',
772 Starts => $args{'starts'},
773 Started => $args{'started'},
774 Resolved => $args{'resolved'},
775 Owner => $args{'owner'},
776 Requestor => $args{'requestor'},
778 AdminCc => $args{'admincc'},
779 TimeWorked => $args{'timeworked'},
780 TimeEstimated => $args{'timeestimated'},
781 TimeLeft => $args{'timeleft'},
782 InitialPriority => $args{'initialpriority'} || 0,
783 FinalPriority => $args{'finalpriority'} || 0,
784 SquelchMailTo => $args{'squelchmailto'},
785 Type => $args{'type'},
789 if ( $args{content} ) {
790 my $mimeobj = MIME::Entity->new();
792 Type => $args{'contenttype'} || 'text/plain',
793 Data => $args{'content'}
795 $ticketargs{MIMEObj} = $mimeobj;
796 $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
799 foreach my $tag ( keys(%args) ) {
800 # if the tag was added later, skip it
801 my $orig_tag = $original_tags{$tag} or next;
802 if ( $orig_tag =~ /^customfield-?(\d+)$/i ) {
803 $ticketargs{ "CustomField-" . $1 } = $args{$tag};
804 } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
805 my $cf = RT::CustomField->new( $self->CurrentUser );
806 $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
807 $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
809 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
810 } elsif ($orig_tag) {
811 my $cf = RT::CustomField->new( $self->CurrentUser );
812 $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
813 $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
815 $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
820 $self->GetDeferred( \%args, $template_id, $links, $postponed );
822 return $TicketObj, \%ticketargs;
826 =head2 _ParseXSVTemplate
828 Parses a tab or comma delimited template. Should only ever be called by Parse
832 sub _ParseXSVTemplate {
836 use Regexp::Common qw(delimited);
837 my($first, $content) = split(/\r?\n/, $args{'Content'}, 2);
840 if ( $first =~ /\t/ ) {
845 my @fields = split( /$delimiter/, $first );
847 my $delimiter_re = qr[$delimiter];
848 my $justquoted = qr[$RE{quoted}];
850 # Used to generate automatic template ids
855 $content =~ s/^(\s*\r?\n)+//;
857 # Keep track of Queue and Requestor, so we can provide defaults
861 # The template for this line
864 # What column we're on
867 # If the last iteration was the end of the line
874 while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) {
877 # Strip off quotes, if they exist
879 if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) {
880 substr( $value, 0, 1 ) = "";
881 substr( $value, -1, 1 ) = "";
884 # What column is this?
885 my $field = $fields[$i++];
886 next COLUMN unless $field =~ /\S/;
890 if ( $field =~ /^id$/i ) {
891 # Special case if this is the ID column
892 if ( $value =~ /^\d+$/ ) {
893 $template_id = 'update-' . $value;
894 push @{ $self->{'update_tickets'} }, $template_id;
895 } elsif ( $value =~ /^#base-(\d+)$/ ) {
896 $template_id = 'base-' . $1;
897 push @{ $self->{'base_tickets'} }, $template_id;
898 } elsif ( $value =~ /\S/ ) {
899 $template_id = 'create-' . $value;
900 push @{ $self->{'create_tickets'} }, $template_id;
904 if ( $field =~ /^Body$/i
905 || $field =~ /^Data$/i
906 || $field =~ /^Message$/i )
909 } elsif ( $field =~ /^Summary$/i ) {
911 } elsif ( $field =~ /^Queue$/i ) {
912 # Note that we found a queue
914 $value ||= $args{'Queue'};
915 } elsif ( $field =~ /^Requestors?$/i ) {
916 $field = 'Requestor'; # Remove plural
917 # Note that we found a requestor
919 $value ||= $args{'Requestor'};
922 # Tack onto the end of the template
923 $template .= $field . ": ";
924 $template .= (defined $value ? $value : "");
926 $template .= "ENDOFCONTENT\n"
927 if $field =~ /^Content$/i;
932 next unless $template;
934 # If we didn't find a queue of requestor, tack on the defaults
935 if ( !$queue && $args{'Queue'} ) {
936 $template .= "Queue: $args{'Queue'}\n";
938 if ( !$requestor && $args{'Requestor'} ) {
939 $template .= "Requestor: $args{'Requestor'}\n";
942 # If we never found an ID, come up with one
943 unless ($template_id) {
944 $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"};
945 $template_id = "create-auto-$autoid";
946 # Also, it's a ticket to create
947 push @{ $self->{'create_tickets'} }, $template_id;
950 # Save the template we generated
951 $self->{'templates'}->{$template_id} = $template;
961 my $postponed = shift;
963 # Deferred processing
967 { DependsOn => $args->{'dependson'},
968 DependedOnBy => $args->{'dependedonby'},
969 RefersTo => $args->{'refersto'},
970 ReferredToBy => $args->{'referredtoby'},
971 Children => $args->{'children'},
972 Parents => $args->{'parents'},
978 # Status is postponed so we don't violate dependencies
979 $id, { Status => $args->{'status'}, }
983 sub GetUpdateTemplate {
988 $string .= "Queue: " . $t->QueueObj->Name . "\n";
989 $string .= "Subject: " . $t->Subject . "\n";
990 $string .= "Status: " . $t->Status . "\n";
991 $string .= "UpdateType: correspond\n";
992 $string .= "Content: \n";
993 $string .= "ENDOFCONTENT\n";
994 $string .= "Due: " . $t->DueObj->AsString . "\n";
995 $string .= "Starts: " . $t->StartsObj->AsString . "\n";
996 $string .= "Started: " . $t->StartedObj->AsString . "\n";
997 $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n";
998 $string .= "Owner: " . $t->OwnerObj->Name . "\n";
999 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1000 $string .= "Cc: " . $t->CcAddresses . "\n";
1001 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1002 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1003 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1004 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1005 $string .= "InitialPriority: " . $t->Priority . "\n";
1006 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1008 foreach my $type ( sort keys %LINKTYPEMAP ) {
1010 # don't display duplicates
1011 if ( $type eq "HasMember"
1012 || $type eq "Members"
1013 || $type eq "MemberOf" )
1017 $string .= "$type: ";
1019 my $mode = $LINKTYPEMAP{$type}->{Mode};
1020 my $method = $LINKTYPEMAP{$type}->{Type};
1023 while ( my $link = $t->$method->Next ) {
1024 $links .= ", " if $links;
1026 my $object = $mode . "Obj";
1027 my $member = $link->$object;
1028 $links .= $member->Id if $member;
1037 sub GetBaseTemplate {
1042 $string .= "Queue: " . $t->Queue . "\n";
1043 $string .= "Subject: " . $t->Subject . "\n";
1044 $string .= "Status: " . $t->Status . "\n";
1045 $string .= "Due: " . $t->DueObj->Unix . "\n";
1046 $string .= "Starts: " . $t->StartsObj->Unix . "\n";
1047 $string .= "Started: " . $t->StartedObj->Unix . "\n";
1048 $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n";
1049 $string .= "Owner: " . $t->Owner . "\n";
1050 $string .= "Requestor: " . $t->RequestorAddresses . "\n";
1051 $string .= "Cc: " . $t->CcAddresses . "\n";
1052 $string .= "AdminCc: " . $t->AdminCcAddresses . "\n";
1053 $string .= "TimeWorked: " . $t->TimeWorked . "\n";
1054 $string .= "TimeEstimated: " . $t->TimeEstimated . "\n";
1055 $string .= "TimeLeft: " . $t->TimeLeft . "\n";
1056 $string .= "InitialPriority: " . $t->Priority . "\n";
1057 $string .= "FinalPriority: " . $t->FinalPriority . "\n";
1062 sub GetCreateTemplate {
1067 $string .= "Queue: General\n";
1068 $string .= "Subject: \n";
1069 $string .= "Status: new\n";
1070 $string .= "Content: \n";
1071 $string .= "ENDOFCONTENT\n";
1072 $string .= "Due: \n";
1073 $string .= "Starts: \n";
1074 $string .= "Started: \n";
1075 $string .= "Resolved: \n";
1076 $string .= "Owner: \n";
1077 $string .= "Requestor: \n";
1078 $string .= "Cc: \n";
1079 $string .= "AdminCc:\n";
1080 $string .= "TimeWorked: \n";
1081 $string .= "TimeEstimated: \n";
1082 $string .= "TimeLeft: \n";
1083 $string .= "InitialPriority: \n";
1084 $string .= "FinalPriority: \n";
1086 foreach my $type ( keys %LINKTYPEMAP ) {
1088 # don't display duplicates
1089 if ( $type eq "HasMember"
1090 || $type eq 'Members'
1091 || $type eq 'MemberOf' )
1095 $string .= "$type: \n";
1100 sub UpdateWatchers {
1107 foreach my $type (qw(Requestor Cc AdminCc)) {
1108 my $method = $type . 'Addresses';
1109 my $oldaddr = $ticket->$method;
1111 # Skip unless we have a defined field
1112 next unless defined $args->{$type};
1113 my $newaddr = $args->{$type};
1115 my @old = split( /,\s*/, $oldaddr );
1117 for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) {
1118 # Sometimes these are email addresses, sometimes they're
1119 # users. Try to guess which is which, as we want to deal
1120 # with email addresses if at all possible.
1124 # It doesn't look like an email address. Try to load it.
1125 my $user = RT::User->new($self->CurrentUser);
1128 push @new, $user->EmailAddress;
1135 my %oldhash = map { $_ => 1 } @old;
1136 my %newhash = map { $_ => 1 } @new;
1138 my @add = grep( !defined $oldhash{$_}, @new );
1139 my @delete = grep( !defined $newhash{$_}, @old );
1142 my ( $val, $msg ) = $ticket->AddWatcher(
1148 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1152 my ( $val, $msg ) = $ticket->DeleteWatcher(
1157 $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg;
1163 sub UpdateCustomFields {
1169 foreach my $arg (keys %{$args}) {
1170 next unless $arg =~ /^CustomField-(\d+)$/;
1173 my $CustomFieldObj = RT::CustomField->new($self->CurrentUser);
1174 $CustomFieldObj->LoadById($cf);
1177 if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext
1178 @values = ($args->{$arg});
1180 @values = split /\n/, $args->{$arg};
1183 if ( ($CustomFieldObj->Type eq 'Freeform'
1184 && ! $CustomFieldObj->SingleValue) ||
1185 $CustomFieldObj->Type =~ /text/i) {
1186 foreach my $val (@values) {
1191 foreach my $value (@values) {
1192 next unless length($value);
1193 my ( $val, $msg ) = $ticket->AddCustomFieldValue(
1197 push ( @results, $msg );
1206 my $postponed = shift;
1208 # postprocessing: add links
1210 while ( my $template_id = shift(@$links) ) {
1211 my $ticket = $T::Tickets{$template_id};
1212 $RT::Logger->debug( "Handling links for " . $ticket->Id );
1213 my %args = %{ shift(@$links) };
1215 foreach my $type ( keys %LINKTYPEMAP ) {
1216 next unless ( defined $args{$type} );
1218 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
1222 if ( $link =~ /^TOP$/i ) {
1223 $RT::Logger->debug( "Building $type link for $link: "
1224 . $T::Tickets{TOP}->Id );
1225 $link = $T::Tickets{TOP}->Id;
1227 } elsif ( $link !~ m/^\d+$/ ) {
1228 my $key = "create-$link";
1229 if ( !exists $T::Tickets{$key} ) {
1231 "Skipping $type link for $key (non-existent)");
1234 $RT::Logger->debug( "Building $type link for $link: "
1235 . $T::Tickets{$key}->Id );
1236 $link = $T::Tickets{$key}->Id;
1238 $RT::Logger->debug("Building $type link for $link");
1241 my ( $wval, $wmsg ) = $ticket->AddLink(
1242 Type => $LINKTYPEMAP{$type}->{'Type'},
1243 $LINKTYPEMAP{$type}->{'Mode'} => $link,
1247 $RT::Logger->warning("AddLink thru $link failed: $wmsg")
1250 # push @non_fatal_errors, $wmsg unless ($wval);
1256 # postponed actions -- Status only, currently
1257 while ( my $template_id = shift(@$postponed) ) {
1258 my $ticket = $T::Tickets{$template_id};
1259 $RT::Logger->debug( "Handling postponed actions for " . $ticket->id );
1260 my %args = %{ shift(@$postponed) };
1261 $ticket->SetStatus( $args{Status} ) if defined $args{Status};
1268 my $queues = RT::Queues->new($self->CurrentUser);
1271 while (my $queue = $queues->Next) {
1272 push @names, $queue->Id, $queue->Name;
1277 'label' => 'In queue',
1279 'options' => \@names
1284 RT::Base->_ImportOverlays();