3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
24 package RT::Action::CreateTickets;
25 require RT::Action::Generic;
29 @ISA = qw(RT::Action::Generic);
35 RT::Action::CreateTickets
37 Create one or more tickets according to an externally supplied template.
42 ===Create-Ticket: codereview
43 Subject: Code review for {$Tickets{'TOP'}->Subject}
45 Content: Someone has created a ticket. you should review and approve it,
46 so they can finish their work
52 Using the "CreateTickets" ScripAction and mandatory dependencies, RT now has
53 the ability to model complex workflow. When a ticket is created in a queue
54 that has a "CreateTickets" scripaction, that ScripAction parses its "Template"
60 CreateTickets uses the template as a template for an ordered set of tickets
61 to create. The basic format is as follows:
64 ===Create-Ticket: identifier
78 Each ===Create-Ticket: section is evaluated as its own
79 Text::Template object, which means that you can embed snippets
80 of perl inside the Text::Template using {} delimiters, but that
81 such sections absolutely can not span a ===Create-Ticket boundary.
83 After each ticket is created, it's stuffed into a hash called %Tickets
84 so as to be available during the creation of other tickets during the same
85 ScripAction. The hash is prepopulated with the ticket which triggered the
86 ScripAction as $Tickets{'TOP'}; you can also access that ticket using the
91 ===Create-Ticket: codereview
92 Subject: Code review for {$Tickets{'TOP'}->Subject}
94 Content: Someone has created a ticket. you should review and approve it,
95 so they can finish their work
102 ===Create-Ticket: approval
103 { # Find out who the administrators of the group called "HR"
104 # of which the creator of this ticket is a member
107 my $groups = RT::Groups->new($RT::SystemUser);
108 $groups->LimitToUserDefinedGroups();
109 $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name");
110 $groups->WithMember($TransactionObj->CreatorObj->Id);
112 my $groupid = $groups->First->Id;
114 my $adminccs = RT::Users->new($RT::SystemUser);
115 $adminccs->WhoHaveRight(
116 Right => "AdminGroup",
117 Object =>$groups->First,
118 IncludeSystemRights => undef,
119 IncludeSuperusers => 0,
120 IncludeSubgroupMembers => 0,
124 while (my $admin = $adminccs->Next) {
125 push (@admins, $admin->EmailAddress);
130 AdminCc: {join ("\nAdminCc: ",@admins) }
133 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
135 Content-Type: text/plain
136 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
140 ===Create-Ticket: two
141 Subject: Manager approval
143 Refers-On: {$Tickets{"approval"}->Id}
145 Content-Type: text/plain
147 Your approval is requred for this ticket, too.
150 =head2 Acceptable fields
152 A complete list of acceptable fields for this beastie:
155 * Queue => Name or id# of a queue
156 Subject => A text string
157 ! Status => A valid status. defaults to 'new'
158 Due => Dates can be specified in seconds since the epoch
159 to be handled literally or in a semi-free textual
160 format which RT will attempt to parse.
167 Owner => Username or id of an RT user who can and should own
169 + Requestor => Email address
170 + Cc => Email address
171 + AdminCc => Email address
184 Content => content. Can extend to multiple lines. Everything
185 within a template after a Content: header is treated
186 as content until we hit a line containing only
188 ContentType => the content-type of the Content field
189 CustomField-<id#> => custom field value
191 Fields marked with an * are required.
193 Fields marked with a + man have multiple values, simply
194 by repeating the fieldname on a new line with an additional value.
196 Fields marked with a ! are postponed to be processed after all
197 tickets in the same actions are created. Except for 'Status', those
198 field can also take a ticket name within the same action (i.e.
199 the identifiers after ==Create-Ticket), instead of raw Ticket ID
202 When parsed, field names are converted to lowercase and have -s stripped.
203 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
204 be treated as the same thing.
209 ok (require RT::Action::CreateTickets);
211 use_ok(RT::Template);
212 use_ok(RT::ScripAction);
213 use_ok(RT::ScripCondition);
216 my $approvalsq = RT::Queue->new($RT::SystemUser);
217 $approvalsq->Create(Name => 'Approvals');
218 ok ($approvalsq->Id, "Created Approvals test queue");
222 '===Create-Ticket: approval
224 my $groups = RT::Groups->new($RT::SystemUser);
225 $groups->LimitToUserDefinedGroups();
226 $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name");
227 $groups->WithMember($Transaction->CreatorObj->Id);
229 my $groupid = $groups->First->Id;
231 my $adminccs = RT::Users->new($RT::SystemUser);
232 $adminccs->WhoHaveRight(Right => "AdminGroup", IncludeSystemRights => undef, IncludeSuperusers => 0, IncludeSubgroupMembers => 0, Object => $groups->First);
235 while (my $admin = $adminccs->Next) {
236 push (@admins, $admin->EmailAddress);
241 AdminCc: {join ("\nAdminCc: ",@admins) }
244 Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
246 Content-Type: text/plain
247 Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
251 ===Create-Ticket: two
252 Subject: Manager approval.
253 Depends-On: {$Tickets{"approval"}->Id}
255 Content-Type: text/plain
257 Your minion approved this ticket. you ok with that?
261 ok ($approvals =~ /Content/, "Read in the approvals template");
263 my $apptemp = RT::Template->new($RT::SystemUser);
264 $apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0");
268 my $q = RT::Queue->new($RT::SystemUser);
269 $q->Create(Name => 'WorkflowTest');
270 ok ($q->Id, "Created workflow test queue");
272 my $scrip = RT::Scrip->new($RT::SystemUser);
273 my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction',
274 ScripAction => 'Create Tickets',
275 Template => 'Approvals',
278 ok ($scrip->Id, "Created the scrip");
279 ok ($scrip->TemplateObj->Id, "Created the scrip template");
280 ok ($scrip->ConditionObj->Id, "Created the scrip condition");
281 ok ($scrip->ActionObj->Id, "Created the scrip action");
283 my $t = RT::Ticket->new($RT::SystemUser);
284 $t->Create(Subject => "Sample workflow test",
294 Jesse Vincent <jesse@bestpractical.com>
303 MemberOf => { Type => 'MemberOf',
305 Members => { Type => 'MemberOf',
307 HasMember => { Type => 'MemberOf',
309 RefersTo => { Type => 'RefersTo',
311 ReferredToBy => { Type => 'RefersTo',
313 DependsOn => { Type => 'DependsOn',
315 DependedOnBy => { Type => 'DependsOn',
320 # {{{ Scrip methods (Commit, Prepare)
323 #Do what we need to do and send it out.
326 my (@links, @postponed);
328 # XXX: cargo cult programming that works. i'll be back.
331 # Create all the tickets we care about
332 return(1) unless $self->TicketObj->Type eq 'ticket';
336 foreach my $template_id ( @{ $self->{'template_order'} } ) {
337 $T::Tickets{'TOP'} = $T::TOP = $self->TicketObj;
338 $RT::Logger->debug("Workflow: processing $template_id of $T::TOP");
340 $T::ID = $template_id;
341 @T::AllID = @{ $self->{'template_order'} };
343 my $template = Text::Template->new(
345 SOURCE => $self->{'templates'}->{$template_id}
348 $RT::Logger->debug("Workflow: evaluating\n$self->{templates}{$template_id}");
351 my $filled_in = $template->fill_in( PACKAGE => 'T', BROKEN => sub {
352 $err = { @_ }->{error};
355 $RT::Logger->debug("Workflow: yielding\n$filled_in");
358 $RT::Logger->error("Ticket creation failed for ".$self->TicketObj->Id." ".$err);
359 while (my ($k, $v) = each %T::X) {
360 $RT::Logger->debug("Eliminating $template_id from ${k}'s parents.");
361 delete $v->{$template_id};
367 my @lines = ( split ( /\n/, $filled_in ) );
368 while ( defined(my $line = shift @lines) ) {
369 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
374 if (ref($args{$tag})) { #If it's an array, we want to push the value
375 push @{$args{$tag}}, $value;
377 elsif (defined ($args{$tag})) { #if we're about to get a second value, make it an array
378 $args{$tag} = [$args{$tag}, $value];
380 else { #if there's nothing there, just set the value
381 $args{ $tag } = $value;
384 if ( $tag eq 'content' ) { #just build up the content
385 # convert it to an array
386 $args{$tag} = defined($value) ? [ $value."\n" ] : [];
387 while ( defined(my $l = shift @lines) ) {
388 last if ($l =~ /^ENDOFCONTENT\s*$/) ;
389 push @{$args{'content'}}, $l."\n";
395 foreach my $date qw(due starts started resolved) {
396 my $dateobj = RT::Date->new($RT::SystemUser);
397 next unless $args{$date};
398 if ($args{$date} =~ /^\d+$/) {
399 $dateobj->Set(Format => 'unix', Value => $args{$date});
401 $dateobj->Set(Format => 'unknown', Value => $args{$date});
403 $args{$date} = $dateobj->ISO;
405 my $mimeobj = MIME::Entity->new();
406 $mimeobj->build(Type => $args{'contenttype'},
407 Data => $args{'content'});
408 # Now we have a %args to work with.
409 # Make sure we have at least the minimum set of
410 # reasonable data and do our thang
411 $T::Tickets{$template_id} ||= RT::Ticket->new($RT::SystemUser);
413 # Deferred processing
415 $T::Tickets{$template_id}, {
416 DependsOn => $args{'dependson'},
417 DependedOnBy => $args{'dependedonby'},
418 RefersTo => $args{'refersto'},
419 ReferredToBy => $args{'referredtoby'},
420 Members => $args{'members'},
421 MemberOf => $args{'memberof'},
426 # Status is postponed so we don't violate dependencies
427 $T::Tickets{$template_id}, {
428 Status => $args{'status'},
432 $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses;
434 $args{'type'} ||= 'ticket';
436 my %ticketargs = ( Queue => $args{'queue'},
437 Subject=> $args{'subject'},
440 Starts => $args{'starts'},
441 Started => $args{'started'},
442 Resolved => $args{'resolved'},
443 Owner => $args{'owner'},
444 Requestor => $args{'requestor'},
446 AdminCc=> $args{'admincc'},
447 TimeWorked =>$args{'timeworked'},
448 TimeEstimated =>$args{'timeestimated'},
449 TimeLeft =>$args{'timeleft'},
450 InitialPriority => $args{'initialpriority'},
451 FinalPriority => $args{'finalpriority'},
452 Type => $args{'type'},
453 MIMEObj => $mimeobj);
456 foreach my $key (keys(%args)) {
457 $key =~ /^customfield(\d+)$/ or next;
458 $ticketargs{ "CustomField-" . $1 } = $args{$key};
461 my ($id, $transid, $msg) = $T::Tickets{$template_id}->Create(%ticketargs);
464 "Couldn't create related ticket $template_id for ".
465 $self->TicketObj->Id." ".$msg
470 $RT::Logger->debug("Assigned $template_id with $id");
471 $T::Tickets{$template_id}->SetOriginObj($self->TicketObj)
472 if $T::Tickets{$template_id}->can('SetOriginObj');
475 # postprocessing: add links
477 while (my $ticket = shift(@links)) {
478 $RT::Logger->debug("Handling links for " . $ticket->Id);
479 my %args = %{shift(@links)};
481 foreach my $type ( keys %LINKTYPEMAP ) {
482 next unless (defined $args{$type});
484 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
486 if (!exists $T::Tickets{$link}) {
487 $RT::Logger->debug("Skipping $type link for $link (non-existent)");
490 $RT::Logger->debug("Building $type link for $link: " . $T::Tickets{$link}->Id);
491 $link = $T::Tickets{$link}->Id;
493 my ( $wval, $wmsg ) = $ticket->AddLink(
494 Type => $LINKTYPEMAP{$type}->{'Type'},
495 $LINKTYPEMAP{$type}->{'Mode'} => $link,
499 $RT::Logger->warning("AddLink thru $link failed: $wmsg") unless $wval;
500 # push @non_fatal_errors, $wmsg unless ($wval);
506 # postponed actions -- Status only, currently
507 while (my $ticket = shift(@postponed)) {
508 $RT::Logger->debug("Handling postponed actions for $ticket");
509 my %args = %{shift(@postponed)};
511 $ticket->SetStatus($args{Status}) if defined $args{Status};
523 unless ($self->TemplateObj) {
524 $RT::Logger->warning("No template object handed to $self\n");
527 unless ($self->TransactionObj) {
528 $RT::Logger->warning("No transaction object handed to $self\n");
532 unless ($self->TicketObj) {
533 $RT::Logger->warning("No ticket object handed to $self\n");
541 foreach my $line (split(/\n/,$self->TemplateObj->Content)) {
542 if ($line =~ /^===Create-Ticket: (.*)$/) {
544 push @{$self->{'template_order'}},$template_id;
546 $self->{'templates'}->{$template_id} .= $line."\n";
560 eval "require RT::Action::CreateTickets_Vendor";
561 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Vendor.pm});
562 eval "require RT::Action::CreateTickets_Local";
563 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/CreateTickets_Local.pm});