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 }}}
54 my $ticket = new RT::Ticket($CurrentUser);
55 $ticket->Load($ticket_id);
59 This module lets you manipulate RT\'s ticket object.
71 no warnings qw(redefine);
82 use RT::URI::fsck_com_rt;
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
92 MemberOf => { Type => 'MemberOf',
94 Parents => { Type => 'MemberOf',
96 Members => { Type => 'MemberOf',
98 Children => { Type => 'MemberOf',
100 HasMember => { Type => 'MemberOf',
102 RefersTo => { Type => 'RefersTo',
104 ReferredToBy => { Type => 'RefersTo',
106 DependsOn => { Type => 'DependsOn',
108 DependedOnBy => { Type => 'DependsOn',
110 MergedInto => { Type => 'MergedInto',
118 # A helper table for links mapping to make it easier
119 # to build and parse links between tickets
122 MemberOf => { Base => 'MemberOf',
123 Target => 'HasMember', },
124 RefersTo => { Base => 'RefersTo',
125 Target => 'ReferredToBy', },
126 DependsOn => { Base => 'DependsOn',
127 Target => 'DependedOnBy', },
128 MergedInto => { Base => 'MergedInto',
129 Target => 'MergedInto', },
135 sub LINKTYPEMAP { return \%LINKTYPEMAP }
136 sub LINKDIRMAP { return \%LINKDIRMAP }
142 Takes a single argument. This can be a ticket id, ticket alias or
143 local ticket uri. If the ticket can't be loaded, returns undef.
144 Otherwise, returns the ticket id.
152 #TODO modify this routine to look at EffectiveId and do the recursive load
153 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
155 # FIXME: there is no TicketBaseURI option in config
156 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
157 #If it's a local URI, turn it into a ticket id
158 if ( $base_uri && defined $id && $id =~ /^$base_uri(\d+)$/ ) {
162 #If it's a remote URI, we're going to punt for now
163 elsif ( $id =~ '://' ) {
167 #If we have an integer URI, load the ticket
168 if ( defined $id && $id =~ /^\d+$/ ) {
169 my ($ticketid,$msg) = $self->LoadById($id);
172 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
177 #It's not a URI. It's not a numerical ticket ID. Punt!
179 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
183 #If we're merged, resolve the merge.
184 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
185 $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
186 return ( $self->Load( $self->EffectiveId ) );
189 #Ok. we're loaded. lets get outa here.
190 return ( $self->Id );
200 Arguments: ARGS is a hash of named parameters. Valid parameters are:
203 Queue - Either a Queue object or a Queue Name
204 Requestor - A reference to a list of email addresses or RT user Names
205 Cc - A reference to a list of email addresses or Names
206 AdminCc - A reference to a list of email addresses or Names
207 SquelchMailTo - A reference to a list of email addresses -
208 who should this ticket not mail
209 Type -- The ticket\'s type. ignore this for now
210 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
211 Subject -- A string describing the subject of the ticket
212 Priority -- an integer from 0 to 99
213 InitialPriority -- an integer from 0 to 99
214 FinalPriority -- an integer from 0 to 99
215 Status -- any valid status (Defined in RT::Queue)
216 TimeEstimated -- an integer. estimated time for this task in minutes
217 TimeWorked -- an integer. time worked so far in minutes
218 TimeLeft -- an integer. time remaining in minutes
219 Starts -- an ISO date describing the ticket\'s start date and time in GMT
220 Due -- an ISO date describing the ticket\'s due date and time in GMT
221 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
222 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
224 Ticket links can be set up during create by passing the link type as a hask key and
225 the ticket id to be linked to as a value (or a URI when linking to other objects).
226 Multiple links of the same type can be created by passing an array ref. For example:
229 DependsOn => [ 15, 22 ],
230 RefersTo => 'http://www.bestpractical.com',
232 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
233 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
234 C<Members> and C<Children> are aliases for C<HasMember>.
236 Returns: TICKETID, Transaction Object, Error Message
246 EffectiveId => undef,
251 SquelchMailTo => undef,
255 InitialPriority => undef,
256 FinalPriority => undef,
267 _RecordTransaction => 1,
272 my ($ErrStr, @non_fatal_errors);
274 my $QueueObj = RT::Queue->new( $RT::SystemUser );
275 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
276 $QueueObj->Load( $args{'Queue'}->Id );
278 elsif ( $args{'Queue'} ) {
279 $QueueObj->Load( $args{'Queue'} );
282 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
285 #Can't create a ticket without a queue.
286 unless ( $QueueObj->Id ) {
287 $RT::Logger->debug("$self No queue given for ticket creation.");
288 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
292 #Now that we have a queue, Check the ACLS
294 $self->CurrentUser->HasRight(
295 Right => 'CreateTicket',
302 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
305 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
306 return ( 0, 0, $self->loc('Invalid value for status') );
309 #Since we have a queue, we can set queue defaults
312 # If there's no queue default initial priority and it's not set, set it to 0
313 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
314 unless defined $args{'InitialPriority'};
317 # If there's no queue default final priority and it's not set, set it to 0
318 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
319 unless defined $args{'FinalPriority'};
321 # Priority may have changed from InitialPriority, for the case
322 # where we're importing tickets (eg, from an older RT version.)
323 $args{'Priority'} = $args{'InitialPriority'}
324 unless defined $args{'Priority'};
327 #TODO we should see what sort of due date we're getting, rather +
328 # than assuming it's in ISO format.
330 #Set the due date. if we didn't get fed one, use the queue default due in
331 my $Due = new RT::Date( $self->CurrentUser );
332 if ( defined $args{'Due'} ) {
333 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
335 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
337 $Due->AddDays( $due_in );
340 my $Starts = new RT::Date( $self->CurrentUser );
341 if ( defined $args{'Starts'} ) {
342 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
345 my $Started = new RT::Date( $self->CurrentUser );
346 if ( defined $args{'Started'} ) {
347 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
349 elsif ( $args{'Status'} ne 'new' ) {
353 my $Resolved = new RT::Date( $self->CurrentUser );
354 if ( defined $args{'Resolved'} ) {
355 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
358 #If the status is an inactive status, set the resolved date
359 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
361 $RT::Logger->debug( "Got a ". $args{'Status'}
362 ."(inactive) ticket with undefined resolved date. Setting to now."
369 # {{{ Dealing with time fields
371 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
372 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
373 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
377 # {{{ Deal with setting the owner
380 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
381 if ( $args{'Owner'}->id ) {
382 $Owner = $args{'Owner'};
384 $RT::Logger->error('passed not loaded owner object');
385 push @non_fatal_errors, $self->loc("Invalid owner object");
390 #If we've been handed something else, try to load the user.
391 elsif ( $args{'Owner'} ) {
392 $Owner = RT::User->new( $self->CurrentUser );
393 $Owner->Load( $args{'Owner'} );
394 $Owner->LoadByEmail( $args{'Owner'} )
396 unless ( $Owner->Id ) {
397 push @non_fatal_errors,
398 $self->loc("Owner could not be set.") . " "
399 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
404 #If we have a proposed owner and they don't have the right
405 #to own a ticket, scream about it and make them not the owner
408 if ( $Owner && $Owner->Id != $RT::Nobody->Id
409 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
411 $DeferOwner = $Owner;
413 $RT::Logger->debug('going to deffer setting owner');
417 #If we haven't been handed a valid owner, make it nobody.
418 unless ( defined($Owner) && $Owner->Id ) {
419 $Owner = new RT::User( $self->CurrentUser );
420 $Owner->Load( $RT::Nobody->Id );
425 # We attempt to load or create each of the people who might have a role for this ticket
426 # _outside_ the transaction, so we don't get into ticket creation races
427 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
428 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
429 foreach my $watcher ( splice @{ $args{$type} } ) {
430 next unless $watcher;
431 if ( $watcher =~ /^\d+$/ ) {
432 push @{ $args{$type} }, $watcher;
434 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
435 foreach my $address( @addresses ) {
436 my $user = RT::User->new( $RT::SystemUser );
437 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
439 push @non_fatal_errors,
440 $self->loc("Couldn't load or create user: [_1]", $msg);
442 push @{ $args{$type} }, $user->id;
449 $RT::Handle->BeginTransaction();
452 Queue => $QueueObj->Id,
454 Subject => $args{'Subject'},
455 InitialPriority => $args{'InitialPriority'},
456 FinalPriority => $args{'FinalPriority'},
457 Priority => $args{'Priority'},
458 Status => $args{'Status'},
459 TimeWorked => $args{'TimeWorked'},
460 TimeEstimated => $args{'TimeEstimated'},
461 TimeLeft => $args{'TimeLeft'},
462 Type => $args{'Type'},
463 Starts => $Starts->ISO,
464 Started => $Started->ISO,
465 Resolved => $Resolved->ISO,
469 # Parameters passed in during an import that we probably don't want to touch, otherwise
470 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
471 $params{$attr} = $args{$attr} if $args{$attr};
474 # Delete null integer parameters
476 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
478 delete $params{$attr}
479 unless ( exists $params{$attr} && $params{$attr} );
482 # Delete the time worked if we're counting it in the transaction
483 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
485 my ($id,$ticket_message) = $self->SUPER::Create( %params );
487 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
488 $RT::Handle->Rollback();
490 $self->loc("Ticket could not be created due to an internal error")
494 #Set the ticket's effective ID now that we've created it.
495 my ( $val, $msg ) = $self->__Set(
496 Field => 'EffectiveId',
497 Value => ( $args{'EffectiveId'} || $id )
500 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
501 $RT::Handle->Rollback;
503 $self->loc("Ticket could not be created due to an internal error")
507 my $create_groups_ret = $self->_CreateTicketGroups();
508 unless ($create_groups_ret) {
509 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
511 . ". aborting Ticket creation." );
512 $RT::Handle->Rollback();
514 $self->loc("Ticket could not be created due to an internal error")
518 # Set the owner in the Groups table
519 # We denormalize it into the Ticket table too because doing otherwise would
520 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
521 $self->OwnerGroup->_AddMember(
522 PrincipalId => $Owner->PrincipalId,
523 InsideTransaction => 1
524 ) unless $DeferOwner;
528 # {{{ Deal with setting up watchers
530 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
531 # we know it's an array ref
532 foreach my $watcher ( @{ $args{$type} } ) {
534 # Note that we're using AddWatcher, rather than _AddWatcher, as we
535 # actually _want_ that ACL check. Otherwise, random ticket creators
536 # could make themselves adminccs and maybe get ticket rights. that would
538 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
540 my ($val, $msg) = $self->$method(
542 PrincipalId => $watcher,
545 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
550 if ($args{'SquelchMailTo'}) {
551 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
552 : $args{'SquelchMailTo'};
553 $self->_SquelchMailTo( @squelch );
559 # {{{ Deal with auto-customer association
561 #unless we already have (a) customer(s)...
562 unless ( $self->Customers->Count ) {
564 #first find any requestors with emails but *without* customer targets
565 my @NoCust_Requestors =
566 grep { $_->EmailAddress && ! $_->Customers->Count }
567 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
569 for my $Requestor (@NoCust_Requestors) {
571 #perhaps the stuff in here should be in a User method??
573 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
575 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
577 ## false laziness w/RT/Interface/Web_Vendor.pm
578 my @link = ( 'Type' => 'MemberOf',
579 'Target' => "freeside://freeside/cust_main/$custnum",
582 my( $val, $msg ) = $Requestor->_AddLink(@link);
583 #XXX should do something with $msg# push @non_fatal_errors, $msg;
589 #find any requestors with customer targets
591 my %cust_target = ();
594 grep { $_->Customers->Count }
595 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
597 foreach my $Requestor ( @Requestors ) {
598 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
599 $cust_target{ $cust_link->Target } = 1;
603 #and then auto-associate this ticket with those customers
605 foreach my $cust_target ( keys %cust_target ) {
607 my @link = ( 'Type' => 'MemberOf',
608 #'Target' => "freeside://freeside/cust_main/$custnum",
609 'Target' => $cust_target,
612 my( $val, $msg ) = $self->_AddLink(@link);
613 push @non_fatal_errors, $msg;
621 # {{{ Add all the custom fields
623 foreach my $arg ( keys %args ) {
624 next unless $arg =~ /^CustomField-(\d+)$/i;
628 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
630 next unless defined $value && length $value;
632 # Allow passing in uploaded LargeContent etc by hash reference
633 my ($status, $msg) = $self->_AddCustomFieldValue(
634 (UNIVERSAL::isa( $value => 'HASH' )
639 RecordTransaction => 0,
641 push @non_fatal_errors, $msg unless $status;
647 # {{{ Deal with setting up links
649 # TODO: Adding link may fire scrips on other end and those scrips
650 # could create transactions on this ticket before 'Create' transaction.
652 # We should implement different schema: record 'Create' transaction,
653 # create links and only then fire create transaction's scrips.
655 # Ideal variant: add all links without firing scrips, record create
656 # transaction and only then fire scrips on the other ends of links.
660 foreach my $type ( keys %LINKTYPEMAP ) {
661 next unless ( defined $args{$type} );
663 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
665 # Check rights on the other end of the link if we must
666 # then run _AddLink that doesn't check for ACLs
667 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
668 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
670 push @non_fatal_errors, $msg;
673 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
674 push @non_fatal_errors, $self->loc('Linking. Permission denied');
679 my ( $wval, $wmsg ) = $self->_AddLink(
680 Type => $LINKTYPEMAP{$type}->{'Type'},
681 $LINKTYPEMAP{$type}->{'Mode'} => $link,
682 Silent => !$args{'_RecordTransaction'},
683 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
687 push @non_fatal_errors, $wmsg unless ($wval);
692 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
693 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
695 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
697 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
698 . ") was proposed as a ticket owner but has no rights to own "
699 . "tickets in " . $QueueObj->Name );
700 push @non_fatal_errors, $self->loc(
701 "Owner '[_1]' does not have rights to own this ticket.",
705 $Owner = $DeferOwner;
706 $self->__Set(Field => 'Owner', Value => $Owner->id);
708 $self->OwnerGroup->_AddMember(
709 PrincipalId => $Owner->PrincipalId,
710 InsideTransaction => 1
714 if ( $args{'_RecordTransaction'} ) {
716 # {{{ Add a transaction for the create
717 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
719 TimeTaken => $args{'TimeWorked'},
720 MIMEObj => $args{'MIMEObj'},
721 CommitScrips => !$args{'DryRun'},
724 if ( $self->Id && $Trans ) {
726 $TransObj->UpdateCustomFields(ARGSRef => \%args);
728 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
729 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
730 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
733 $RT::Handle->Rollback();
735 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
736 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
737 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
740 if ( $args{'DryRun'} ) {
741 $RT::Handle->Rollback();
742 return ($self->id, $TransObj, $ErrStr);
744 $RT::Handle->Commit();
745 return ( $self->Id, $TransObj->Id, $ErrStr );
751 # Not going to record a transaction
752 $RT::Handle->Commit();
753 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
754 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
755 return ( $self->Id, 0, $ErrStr );
763 # {{{ _Parse822HeadersForAttributes Content
765 =head2 _Parse822HeadersForAttributes Content
767 Takes an RFC822 style message and parses its attributes into a hash.
771 sub _Parse822HeadersForAttributes {
776 my @lines = ( split ( /\n/, $content ) );
777 while ( defined( my $line = shift @lines ) ) {
778 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
783 if ( defined( $args{$tag} ) )
784 { #if we're about to get a second value, make it an array
785 $args{$tag} = [ $args{$tag} ];
787 if ( ref( $args{$tag} ) )
788 { #If it's an array, we want to push the value
789 push @{ $args{$tag} }, $value;
791 else { #if there's nothing there, just set the value
792 $args{$tag} = $value;
794 } elsif ($line =~ /^$/) {
796 #TODO: this won't work, since "" isn't of the form "foo:value"
798 while ( defined( my $l = shift @lines ) ) {
799 push @{ $args{'content'} }, $l;
805 foreach my $date qw(due starts started resolved) {
806 my $dateobj = RT::Date->new($RT::SystemUser);
807 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
808 $dateobj->Set( Format => 'unix', Value => $args{$date} );
811 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
813 $args{$date} = $dateobj->ISO;
815 $args{'mimeobj'} = MIME::Entity->new();
816 $args{'mimeobj'}->build(
817 Type => ( $args{'contenttype'} || 'text/plain' ),
818 Data => ($args{'content'} || '')
828 =head2 Import PARAMHASH
831 Doesn\'t create a transaction.
832 Doesn\'t supply queue defaults, etc.
840 my ( $ErrStr, $QueueObj, $Owner );
844 EffectiveId => undef,
848 Owner => $RT::Nobody->Id,
849 Subject => '[no subject]',
850 InitialPriority => undef,
851 FinalPriority => undef,
862 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
863 $QueueObj = RT::Queue->new($RT::SystemUser);
864 $QueueObj->Load( $args{'Queue'} );
866 #TODO error check this and return 0 if it\'s not loading properly +++
868 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
869 $QueueObj = RT::Queue->new($RT::SystemUser);
870 $QueueObj->Load( $args{'Queue'}->Id );
874 "$self " . $args{'Queue'} . " not a recognised queue object." );
877 #Can't create a ticket without a queue.
878 unless ( defined($QueueObj) and $QueueObj->Id ) {
879 $RT::Logger->debug("$self No queue given for ticket creation.");
880 return ( 0, $self->loc('Could not create ticket. Queue not set') );
883 #Now that we have a queue, Check the ACLS
885 $self->CurrentUser->HasRight(
886 Right => 'CreateTicket',
892 $self->loc("No permission to create tickets in the queue '[_1]'"
896 # {{{ Deal with setting the owner
898 # Attempt to take user object, user name or user id.
899 # Assign to nobody if lookup fails.
900 if ( defined( $args{'Owner'} ) ) {
901 if ( ref( $args{'Owner'} ) ) {
902 $Owner = $args{'Owner'};
905 $Owner = new RT::User( $self->CurrentUser );
906 $Owner->Load( $args{'Owner'} );
907 if ( !defined( $Owner->id ) ) {
908 $Owner->Load( $RT::Nobody->id );
913 #If we have a proposed owner and they don't have the right
914 #to own a ticket, scream about it and make them not the owner
917 and ( $Owner->Id != $RT::Nobody->Id )
927 $RT::Logger->warning( "$self user "
931 . "as a ticket owner but has no rights to own "
933 . $QueueObj->Name . "'" );
938 #If we haven't been handed a valid owner, make it nobody.
939 unless ( defined($Owner) ) {
940 $Owner = new RT::User( $self->CurrentUser );
941 $Owner->Load( $RT::Nobody->UserObj->Id );
946 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
947 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
950 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
951 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
952 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
953 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
955 # If we're coming in with an id, set that now.
956 my $EffectiveId = undef;
958 $EffectiveId = $args{'id'};
962 my $id = $self->SUPER::Create(
964 EffectiveId => $EffectiveId,
965 Queue => $QueueObj->Id,
967 Subject => $args{'Subject'}, # loc
968 InitialPriority => $args{'InitialPriority'}, # loc
969 FinalPriority => $args{'FinalPriority'}, # loc
970 Priority => $args{'InitialPriority'}, # loc
971 Status => $args{'Status'}, # loc
972 TimeWorked => $args{'TimeWorked'}, # loc
973 Type => $args{'Type'}, # loc
974 Created => $args{'Created'}, # loc
975 Told => $args{'Told'}, # loc
976 LastUpdated => $args{'Updated'}, # loc
977 Resolved => $args{'Resolved'}, # loc
978 Due => $args{'Due'}, # loc
981 # If the ticket didn't have an id
982 # Set the ticket's effective ID now that we've created it.
984 $self->Load( $args{'id'} );
988 $self->__Set( Field => 'EffectiveId', Value => $id );
992 $self . "->Import couldn't set EffectiveId: $msg" );
996 my $create_groups_ret = $self->_CreateTicketGroups();
997 unless ($create_groups_ret) {
999 "Couldn't create ticket groups for ticket " . $self->Id );
1002 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1005 foreach $watcher ( @{ $args{'Cc'} } ) {
1006 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1008 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1009 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1012 foreach $watcher ( @{ $args{'Requestor'} } ) {
1013 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1017 return ( $self->Id, $ErrStr );
1022 # {{{ Routines dealing with watchers.
1024 # {{{ _CreateTicketGroups
1026 =head2 _CreateTicketGroups
1028 Create the ticket groups and links for this ticket.
1029 This routine expects to be called from Ticket->Create _inside of a transaction_
1031 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1033 It will return true on success and undef on failure.
1039 sub _CreateTicketGroups {
1042 my @types = qw(Requestor Owner Cc AdminCc);
1044 foreach my $type (@types) {
1045 my $type_obj = RT::Group->new($self->CurrentUser);
1046 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1047 Instance => $self->Id,
1050 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1051 $self->Id.": ".$msg);
1061 # {{{ sub OwnerGroup
1065 A constructor which returns an RT::Group object containing the owner of this ticket.
1071 my $owner_obj = RT::Group->new($self->CurrentUser);
1072 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1073 return ($owner_obj);
1079 # {{{ sub AddWatcher
1083 AddWatcher takes a parameter hash. The keys are as follows:
1085 Type One of Requestor, Cc, AdminCc
1087 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1089 Email The email address of the new watcher. If a user with this
1090 email address can't be found, a new nonprivileged user will be created.
1092 If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1100 PrincipalId => undef,
1105 # ModifyTicket works in any case
1106 return $self->_AddWatcher( %args )
1107 if $self->CurrentUserHasRight('ModifyTicket');
1108 if ( $args{'Email'} ) {
1109 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1110 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1113 if ( lc $self->CurrentUser->UserObj->EmailAddress
1114 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1116 $args{'PrincipalId'} = $self->CurrentUser->id;
1117 delete $args{'Email'};
1121 # If the watcher isn't the current user then the current user has no right
1123 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1124 return ( 0, $self->loc("Permission Denied") );
1127 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1128 if ( $args{'Type'} eq 'AdminCc' ) {
1129 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1130 return ( 0, $self->loc('Permission Denied') );
1134 # If it's a Requestor or Cc and they don't have 'Watch', bail
1135 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1136 unless ( $self->CurrentUserHasRight('Watch') ) {
1137 return ( 0, $self->loc('Permission Denied') );
1141 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1142 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1145 return $self->_AddWatcher( %args );
1148 #This contains the meat of AddWatcher. but can be called from a routine like
1149 # Create, which doesn't need the additional acl check
1155 PrincipalId => undef,
1161 my $principal = RT::Principal->new($self->CurrentUser);
1162 if ($args{'Email'}) {
1163 my $user = RT::User->new($RT::SystemUser);
1164 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1165 $args{'PrincipalId'} = $pid if $pid;
1167 if ($args{'PrincipalId'}) {
1168 $principal->Load($args{'PrincipalId'});
1172 # If we can't find this watcher, we need to bail.
1173 unless ($principal->Id) {
1174 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1175 return(0, $self->loc("Could not find or create that user"));
1179 my $group = RT::Group->new($self->CurrentUser);
1180 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1181 unless ($group->id) {
1182 return(0,$self->loc("Group not found"));
1185 if ( $group->HasMember( $principal)) {
1187 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1191 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1192 InsideTransaction => 1 );
1194 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1196 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1199 unless ( $args{'Silent'} ) {
1200 $self->_NewTransaction(
1201 Type => 'AddWatcher',
1202 NewValue => $principal->Id,
1203 Field => $args{'Type'}
1207 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1213 # {{{ sub DeleteWatcher
1215 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1218 Deletes a Ticket watcher. Takes two arguments:
1220 Type (one of Requestor,Cc,AdminCc)
1224 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1226 Email (the email address of an existing wathcer)
1235 my %args = ( Type => undef,
1236 PrincipalId => undef,
1240 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1241 return ( 0, $self->loc("No principal specified") );
1243 my $principal = RT::Principal->new( $self->CurrentUser );
1244 if ( $args{'PrincipalId'} ) {
1246 $principal->Load( $args{'PrincipalId'} );
1249 my $user = RT::User->new( $self->CurrentUser );
1250 $user->LoadByEmail( $args{'Email'} );
1251 $principal->Load( $user->Id );
1254 # If we can't find this watcher, we need to bail.
1255 unless ( $principal->Id ) {
1256 return ( 0, $self->loc("Could not find that principal") );
1259 my $group = RT::Group->new( $self->CurrentUser );
1260 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1261 unless ( $group->id ) {
1262 return ( 0, $self->loc("Group not found") );
1266 #If the watcher we're trying to add is for the current user
1267 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1269 # If it's an AdminCc and they don't have
1270 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1271 if ( $args{'Type'} eq 'AdminCc' ) {
1272 unless ( $self->CurrentUserHasRight('ModifyTicket')
1273 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1274 return ( 0, $self->loc('Permission Denied') );
1278 # If it's a Requestor or Cc and they don't have
1279 # 'Watch' or 'ModifyTicket', bail
1280 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1282 unless ( $self->CurrentUserHasRight('ModifyTicket')
1283 or $self->CurrentUserHasRight('Watch') ) {
1284 return ( 0, $self->loc('Permission Denied') );
1288 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1290 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1294 # If the watcher isn't the current user
1295 # and the current user doesn't have 'ModifyTicket' bail
1297 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1298 return ( 0, $self->loc("Permission Denied") );
1304 # see if this user is already a watcher.
1306 unless ( $group->HasMember($principal) ) {
1308 $self->loc( 'That principal is not a [_1] for this ticket',
1312 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1314 $RT::Logger->error( "Failed to delete "
1316 . " as a member of group "
1322 'Could not remove that principal as a [_1] for this ticket',
1326 unless ( $args{'Silent'} ) {
1327 $self->_NewTransaction( Type => 'DelWatcher',
1328 OldValue => $principal->Id,
1329 Field => $args{'Type'} );
1333 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1334 $principal->Object->Name,
1343 =head2 SquelchMailTo [EMAIL]
1345 Takes an optional email address to never email about updates to this ticket.
1348 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1356 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1360 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1365 return $self->_SquelchMailTo(@_);
1368 sub _SquelchMailTo {
1372 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1373 unless grep { $_->Content eq $attr }
1374 $self->Attributes->Named('SquelchMailTo');
1376 my @attributes = $self->Attributes->Named('SquelchMailTo');
1377 return (@attributes);
1381 =head2 UnsquelchMailTo ADDRESS
1383 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1385 Returns a tuple of (status, message)
1389 sub UnsquelchMailTo {
1392 my $address = shift;
1393 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1394 return ( 0, $self->loc("Permission Denied") );
1397 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1398 return ($val, $msg);
1402 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1404 =head2 RequestorAddresses
1406 B<Returns> String: All Ticket Requestor email addresses as a string.
1410 sub RequestorAddresses {
1413 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1417 return ( $self->Requestors->MemberEmailAddressesAsString );
1421 =head2 AdminCcAddresses
1423 returns String: All Ticket AdminCc email addresses as a string
1427 sub AdminCcAddresses {
1430 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1434 return ( $self->AdminCc->MemberEmailAddressesAsString )
1440 returns String: All Ticket Ccs as a string of email addresses
1447 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1450 return ( $self->Cc->MemberEmailAddressesAsString);
1456 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1458 # {{{ sub Requestors
1463 Returns this ticket's Requestors as an RT::Group object
1470 my $group = RT::Group->new($self->CurrentUser);
1471 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1472 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1480 # {{{ sub _Requestors
1484 Private non-ACLed variant of Reqeustors so that we can look them up for the
1485 purposes of customer auto-association during create.
1492 my $group = RT::Group->new($RT::SystemUser);
1493 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1504 Returns an RT::Group object which contains this ticket's Ccs.
1505 If the user doesn't have "ShowTicket" permission, returns an empty group
1512 my $group = RT::Group->new($self->CurrentUser);
1513 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1514 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1527 Returns an RT::Group object which contains this ticket's AdminCcs.
1528 If the user doesn't have "ShowTicket" permission, returns an empty group
1535 my $group = RT::Group->new($self->CurrentUser);
1536 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1537 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1547 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1550 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1552 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1554 Takes a param hash with the attributes Type and either PrincipalId or Email
1556 Type is one of Requestor, Cc, AdminCc and Owner
1558 PrincipalId is an RT::Principal id, and Email is an email address.
1560 Returns true if the specified principal (or the one corresponding to the
1561 specified address) is a member of the group Type for this ticket.
1563 XX TODO: This should be Memoized.
1570 my %args = ( Type => 'Requestor',
1571 PrincipalId => undef,
1576 # Load the relevant group.
1577 my $group = RT::Group->new($self->CurrentUser);
1578 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1580 # Find the relevant principal.
1581 if (!$args{PrincipalId} && $args{Email}) {
1582 # Look up the specified user.
1583 my $user = RT::User->new($self->CurrentUser);
1584 $user->LoadByEmail($args{Email});
1586 $args{PrincipalId} = $user->PrincipalId;
1589 # A non-existent user can't be a group member.
1594 # Ask if it has the member in question
1595 return $group->HasMember( $args{'PrincipalId'} );
1600 # {{{ sub IsRequestor
1602 =head2 IsRequestor PRINCIPAL_ID
1604 Takes an L<RT::Principal> id.
1606 Returns true if the principal is a requestor of the current ticket.
1614 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1622 =head2 IsCc PRINCIPAL_ID
1624 Takes an RT::Principal id.
1625 Returns true if the principal is a Cc of the current ticket.
1634 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1642 =head2 IsAdminCc PRINCIPAL_ID
1644 Takes an RT::Principal id.
1645 Returns true if the principal is an AdminCc of the current ticket.
1653 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1663 Takes an RT::User object. Returns true if that user is this ticket's owner.
1664 returns undef otherwise
1672 # no ACL check since this is used in acl decisions
1673 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1677 #Tickets won't yet have owners when they're being created.
1678 unless ( $self->OwnerObj->id ) {
1682 if ( $person->id == $self->OwnerObj->id ) {
1697 =head2 TransactionAddresses
1699 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for all this ticket's Create, Comment or Correspond transactions.
1700 The keys are C<To>, C<Cc> and C<Bcc>. The values are lists of C<Email::Address> objects.
1702 NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
1707 sub TransactionAddresses {
1709 my $txns = $self->Transactions;
1712 foreach my $type (qw(Create Comment Correspond)) {
1713 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1716 while (my $txn = $txns->Next) {
1717 my $txnaddrs = $txn->Addresses;
1718 foreach my $addrlist ( values %$txnaddrs ) {
1719 foreach my $addr (@$addrlist) {
1720 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1721 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1722 # skips "comment-only" addresses
1723 next unless ($addr->address);
1724 $addresses{$addr->address} = $addr;
1736 # {{{ Routines dealing with queues
1738 # {{{ sub ValidateQueue
1745 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1749 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1750 my $id = $QueueObj->Load($Value);
1766 my $NewQueue = shift;
1768 #Redundant. ACL gets checked in _Set;
1769 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1770 return ( 0, $self->loc("Permission Denied") );
1773 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1774 $NewQueueObj->Load($NewQueue);
1776 unless ( $NewQueueObj->Id() ) {
1777 return ( 0, $self->loc("That queue does not exist") );
1780 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1781 return ( 0, $self->loc('That is the same value') );
1784 $self->CurrentUser->HasRight(
1785 Right => 'CreateTicket',
1786 Object => $NewQueueObj
1790 return ( 0, $self->loc("You may not create requests in that queue.") );
1794 $self->OwnerObj->HasRight(
1795 Right => 'OwnTicket',
1796 Object => $NewQueueObj
1800 my $clone = RT::Ticket->new( $RT::SystemUser );
1801 $clone->Load( $self->Id );
1802 unless ( $clone->Id ) {
1803 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1805 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1806 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1809 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1812 # On queue change, change queue for reminders too
1813 my $reminder_collection = $self->Reminders->Collection;
1814 while ( my $reminder = $reminder_collection->Next ) {
1815 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1816 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1820 return ($status, $msg);
1829 Takes nothing. returns this ticket's queue object
1836 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1838 #We call __Value so that we can avoid the ACL decision and some deep recursion
1839 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1840 return ($queue_obj);
1847 # {{{ Date printing routines
1853 Returns an RT::Date object containing this ticket's due date
1860 my $time = new RT::Date( $self->CurrentUser );
1862 # -1 is RT::Date slang for never
1863 if ( my $due = $self->Due ) {
1864 $time->Set( Format => 'sql', Value => $due );
1867 $time->Set( Format => 'unix', Value => -1 );
1875 # {{{ sub DueAsString
1879 Returns this ticket's due date as a human readable string
1885 return $self->DueObj->AsString();
1890 # {{{ sub ResolvedObj
1894 Returns an RT::Date object of this ticket's 'resolved' time.
1901 my $time = new RT::Date( $self->CurrentUser );
1902 $time->Set( Format => 'sql', Value => $self->Resolved );
1908 # {{{ sub SetStarted
1912 Takes a date in ISO format or undef
1913 Returns a transaction id and a message
1914 The client calls "Start" to note that the project was started on the date in $date.
1915 A null date means "now"
1921 my $time = shift || 0;
1923 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1924 return ( 0, $self->loc("Permission Denied") );
1927 #We create a date object to catch date weirdness
1928 my $time_obj = new RT::Date( $self->CurrentUser() );
1930 $time_obj->Set( Format => 'ISO', Value => $time );
1933 $time_obj->SetToNow();
1936 #Now that we're starting, open this ticket
1937 #TODO do we really want to force this as policy? it should be a scrip
1939 #We need $TicketAsSystem, in case the current user doesn't have
1942 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1943 $TicketAsSystem->Load( $self->Id );
1944 if ( $TicketAsSystem->Status eq 'new' ) {
1945 $TicketAsSystem->Open();
1948 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1954 # {{{ sub StartedObj
1958 Returns an RT::Date object which contains this ticket's
1966 my $time = new RT::Date( $self->CurrentUser );
1967 $time->Set( Format => 'sql', Value => $self->Started );
1977 Returns an RT::Date object which contains this ticket's
1985 my $time = new RT::Date( $self->CurrentUser );
1986 $time->Set( Format => 'sql', Value => $self->Starts );
1996 Returns an RT::Date object which contains this ticket's
2004 my $time = new RT::Date( $self->CurrentUser );
2005 $time->Set( Format => 'sql', Value => $self->Told );
2011 # {{{ sub ToldAsString
2015 A convenience method that returns ToldObj->AsString
2017 TODO: This should be deprecated
2023 if ( $self->Told ) {
2024 return $self->ToldObj->AsString();
2033 # {{{ sub TimeWorkedAsString
2035 =head2 TimeWorkedAsString
2037 Returns the amount of time worked on this ticket as a Text String
2041 sub TimeWorkedAsString {
2043 my $value = $self->TimeWorked;
2045 # return the # of minutes worked turned into seconds and written as
2046 # a simple text string, this is not really a date object, but if we
2047 # diff a number of seconds vs the epoch, we'll get a nice description
2049 return "" unless $value;
2050 return RT::Date->new( $self->CurrentUser )
2051 ->DurationAsString( $value * 60 );
2056 # {{{ sub TimeLeftAsString
2058 =head2 TimeLeftAsString
2060 Returns the amount of time left on this ticket as a Text String
2064 sub TimeLeftAsString {
2066 my $value = $self->TimeLeft;
2067 return "" unless $value;
2068 return RT::Date->new( $self->CurrentUser )
2069 ->DurationAsString( $value * 60 );
2074 # {{{ Routines dealing with correspondence/comments
2080 Comment on this ticket.
2081 Takes a hash with the following attributes:
2082 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2085 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2087 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2088 They will, however, be prepared and you'll be able to access them through the TransactionObj
2090 Returns: Transaction id, Error Message, Transaction Object
2091 (note the different order from Create()!)
2098 my %args = ( CcMessageTo => undef,
2099 BccMessageTo => undef,
2106 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2107 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2108 return ( 0, $self->loc("Permission Denied"), undef );
2110 $args{'NoteType'} = 'Comment';
2112 if ($args{'DryRun'}) {
2113 $RT::Handle->BeginTransaction();
2114 $args{'CommitScrips'} = 0;
2117 my @results = $self->_RecordNote(%args);
2118 if ($args{'DryRun'}) {
2119 $RT::Handle->Rollback();
2126 # {{{ sub Correspond
2130 Correspond on this ticket.
2131 Takes a hashref with the following attributes:
2134 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2136 if there's no MIMEObj, Content is used to build a MIME::Entity object
2138 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2139 They will, however, be prepared and you'll be able to access them through the TransactionObj
2141 Returns: Transaction id, Error Message, Transaction Object
2142 (note the different order from Create()!)
2149 my %args = ( CcMessageTo => undef,
2150 BccMessageTo => undef,
2156 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2157 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2158 return ( 0, $self->loc("Permission Denied"), undef );
2161 $args{'NoteType'} = 'Correspond';
2162 if ($args{'DryRun'}) {
2163 $RT::Handle->BeginTransaction();
2164 $args{'CommitScrips'} = 0;
2167 my @results = $self->_RecordNote(%args);
2169 #Set the last told date to now if this isn't mail from the requestor.
2170 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2171 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2173 if ($args{'DryRun'}) {
2174 $RT::Handle->Rollback();
2183 # {{{ sub _RecordNote
2187 the meat of both comment and correspond.
2189 Performs no access control checks. hence, dangerous.
2196 CcMessageTo => undef,
2197 BccMessageTo => undef,
2202 NoteType => 'Correspond',
2208 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2209 return ( 0, $self->loc("No message attached"), undef );
2212 unless ( $args{'MIMEObj'} ) {
2213 $args{'MIMEObj'} = MIME::Entity->build(
2214 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2218 # convert text parts into utf-8
2219 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2221 # If we've been passed in CcMessageTo and BccMessageTo fields,
2222 # add them to the mime object for passing on to the transaction handler
2223 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2224 # RT-Send-Bcc: headers
2227 foreach my $type (qw/Cc Bcc/) {
2228 if ( defined $args{ $type . 'MessageTo' } ) {
2230 my $addresses = join ', ', (
2231 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2232 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2233 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2237 foreach my $argument (qw(Encrypt Sign)) {
2238 $args{'MIMEObj'}->head->add(
2239 "X-RT-$argument" => $args{ $argument }
2240 ) if defined $args{ $argument };
2243 # If this is from an external source, we need to come up with its
2244 # internal Message-ID now, so all emails sent because of this
2245 # message have a common Message-ID
2246 my $org = RT->Config->Get('Organization');
2247 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2248 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2249 $args{'MIMEObj'}->head->set(
2250 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2254 #Record the correspondence (write the transaction)
2255 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2256 Type => $args{'NoteType'},
2257 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2258 TimeTaken => $args{'TimeTaken'},
2259 MIMEObj => $args{'MIMEObj'},
2260 CommitScrips => $args{'CommitScrips'},
2264 $RT::Logger->err("$self couldn't init a transaction $msg");
2265 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2268 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2280 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2283 my $type = shift || "";
2285 unless ( $self->{"$field$type"} ) {
2286 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2288 #not sure what this ACL was supposed to do... but returning the
2289 # bare (unlimited) RT::Links certainly seems wrong, it causes the
2290 # $Ticket->Customers method during creation to return results for every
2292 #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2294 # Maybe this ticket is a merged ticket
2295 my $Tickets = new RT::Tickets( $self->CurrentUser );
2296 # at least to myself
2297 $self->{"$field$type"}->Limit( FIELD => $field,
2298 VALUE => $self->URI,
2299 ENTRYAGGREGATOR => 'OR' );
2300 $Tickets->Limit( FIELD => 'EffectiveId',
2301 VALUE => $self->EffectiveId );
2302 while (my $Ticket = $Tickets->Next) {
2303 $self->{"$field$type"}->Limit( FIELD => $field,
2304 VALUE => $Ticket->URI,
2305 ENTRYAGGREGATOR => 'OR' );
2307 $self->{"$field$type"}->Limit( FIELD => 'Type',
2312 return ( $self->{"$field$type"} );
2317 # {{{ sub DeleteLink
2321 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2322 SilentBase and SilentTarget. Either Base or Target must be null.
2323 The null value will be replaced with this ticket\'s id.
2325 If Silent is true then no transaction would be recorded, in other
2326 case you can control creation of transactions on both base and
2327 target with SilentBase and SilentTarget respectively. By default
2328 both transactions are created.
2339 SilentBase => undef,
2340 SilentTarget => undef,
2344 unless ( $args{'Target'} || $args{'Base'} ) {
2345 $RT::Logger->error("Base or Target must be specified");
2346 return ( 0, $self->loc('Either base or target must be specified') );
2351 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2352 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2353 return ( 0, $self->loc("Permission Denied") );
2356 # If the other URI is an RT::Ticket, we want to make sure the user
2357 # can modify it too...
2358 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2359 return (0, $msg) unless $status;
2360 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2363 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2364 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2366 return ( 0, $self->loc("Permission Denied") );
2369 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2370 return ( 0, $Msg ) unless $val;
2372 return ( $val, $Msg ) if $args{'Silent'};
2374 my ($direction, $remote_link);
2376 if ( $args{'Base'} ) {
2377 $remote_link = $args{'Base'};
2378 $direction = 'Target';
2380 elsif ( $args{'Target'} ) {
2381 $remote_link = $args{'Target'};
2382 $direction = 'Base';
2385 my $remote_uri = RT::URI->new( $self->CurrentUser );
2386 $remote_uri->FromURI( $remote_link );
2388 unless ( $args{ 'Silent'. $direction } ) {
2389 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2390 Type => 'DeleteLink',
2391 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2392 OldValue => $remote_uri->URI || $remote_link,
2395 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2398 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2399 my $OtherObj = $remote_uri->Object;
2400 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2401 Type => 'DeleteLink',
2402 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2403 : $LINKDIRMAP{$args{'Type'}}->{Target},
2404 OldValue => $self->URI,
2405 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2408 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2411 return ( $val, $Msg );
2420 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2422 If Silent is true then no transaction would be recorded, in other
2423 case you can control creation of transactions on both base and
2424 target with SilentBase and SilentTarget respectively. By default
2425 both transactions are created.
2431 my %args = ( Target => '',
2435 SilentBase => undef,
2436 SilentTarget => undef,
2439 unless ( $args{'Target'} || $args{'Base'} ) {
2440 $RT::Logger->error("Base or Target must be specified");
2441 return ( 0, $self->loc('Either base or target must be specified') );
2445 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2446 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2447 return ( 0, $self->loc("Permission Denied") );
2450 # If the other URI is an RT::Ticket, we want to make sure the user
2451 # can modify it too...
2452 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2453 return (0, $msg) unless $status;
2454 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2457 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2458 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2460 return ( 0, $self->loc("Permission Denied") );
2463 return $self->_AddLink(%args);
2466 sub __GetTicketFromURI {
2468 my %args = ( URI => '', @_ );
2470 # If the other URI is an RT::Ticket, we want to make sure the user
2471 # can modify it too...
2472 my $uri_obj = RT::URI->new( $self->CurrentUser );
2473 $uri_obj->FromURI( $args{'URI'} );
2475 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2476 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2477 $RT::Logger->warning( $msg );
2480 my $obj = $uri_obj->Resolver->Object;
2481 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2482 return (1, 'Found not a ticket', undef);
2484 return (1, 'Found ticket', $obj);
2489 Private non-acled variant of AddLink so that links can be added during create.
2495 my %args = ( Target => '',
2499 SilentBase => undef,
2500 SilentTarget => undef,
2503 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2504 return ($val, $msg) if !$val || $exist;
2505 return ($val, $msg) if $args{'Silent'};
2507 my ($direction, $remote_link);
2508 if ( $args{'Target'} ) {
2509 $remote_link = $args{'Target'};
2510 $direction = 'Base';
2511 } elsif ( $args{'Base'} ) {
2512 $remote_link = $args{'Base'};
2513 $direction = 'Target';
2516 my $remote_uri = RT::URI->new( $self->CurrentUser );
2517 $remote_uri->FromURI( $remote_link );
2519 unless ( $args{ 'Silent'. $direction } ) {
2520 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2522 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2523 NewValue => $remote_uri->URI || $remote_link,
2526 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2529 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2530 my $OtherObj = $remote_uri->Object;
2531 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2533 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2534 : $LINKDIRMAP{$args{'Type'}}->{Target},
2535 NewValue => $self->URI,
2536 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2539 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2542 return ( $val, $msg );
2552 MergeInto take the id of the ticket to merge this ticket into.
2560 my $ticket_id = shift;
2562 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2563 return ( 0, $self->loc("Permission Denied") );
2566 # Load up the new ticket.
2567 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2568 $MergeInto->Load($ticket_id);
2570 # make sure it exists.
2571 unless ( $MergeInto->Id ) {
2572 return ( 0, $self->loc("New ticket doesn't exist") );
2575 # Make sure the current user can modify the new ticket.
2576 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2577 return ( 0, $self->loc("Permission Denied") );
2580 $RT::Handle->BeginTransaction();
2582 # We use EffectiveId here even though it duplicates information from
2583 # the links table becasue of the massive performance hit we'd take
2584 # by trying to do a separate database query for merge info everytime
2587 #update this ticket's effective id to the new ticket's id.
2588 my ( $id_val, $id_msg ) = $self->__Set(
2589 Field => 'EffectiveId',
2590 Value => $MergeInto->Id()
2594 $RT::Handle->Rollback();
2595 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2599 if ( $self->__Value('Status') ne 'resolved' ) {
2601 my ( $status_val, $status_msg )
2602 = $self->__Set( Field => 'Status', Value => 'resolved' );
2604 unless ($status_val) {
2605 $RT::Handle->Rollback();
2608 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2612 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2616 # update all the links that point to that old ticket
2617 my $old_links_to = RT::Links->new($self->CurrentUser);
2618 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2621 while (my $link = $old_links_to->Next) {
2622 if (exists $old_seen{$link->Base."-".$link->Type}) {
2625 elsif ($link->Base eq $MergeInto->URI) {
2628 # First, make sure the link doesn't already exist. then move it over.
2629 my $tmp = RT::Link->new($RT::SystemUser);
2630 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2634 $link->SetTarget($MergeInto->URI);
2635 $link->SetLocalTarget($MergeInto->id);
2637 $old_seen{$link->Base."-".$link->Type} =1;
2642 my $old_links_from = RT::Links->new($self->CurrentUser);
2643 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2645 while (my $link = $old_links_from->Next) {
2646 if (exists $old_seen{$link->Type."-".$link->Target}) {
2649 if ($link->Target eq $MergeInto->URI) {
2652 # First, make sure the link doesn't already exist. then move it over.
2653 my $tmp = RT::Link->new($RT::SystemUser);
2654 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2658 $link->SetBase($MergeInto->URI);
2659 $link->SetLocalBase($MergeInto->id);
2660 $old_seen{$link->Type."-".$link->Target} =1;
2666 # Update time fields
2667 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2669 my $mutator = "Set$type";
2670 $MergeInto->$mutator(
2671 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2674 #add all of this ticket's watchers to that ticket.
2675 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2677 my $people = $self->$watcher_type->MembersObj;
2678 my $addwatcher_type = $watcher_type;
2679 $addwatcher_type =~ s/s$//;
2681 while ( my $watcher = $people->Next ) {
2683 my ($val, $msg) = $MergeInto->_AddWatcher(
2684 Type => $addwatcher_type,
2686 PrincipalId => $watcher->MemberId
2689 $RT::Logger->warning($msg);
2695 #find all of the tickets that were merged into this ticket.
2696 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2697 $old_mergees->Limit(
2698 FIELD => 'EffectiveId',
2703 # update their EffectiveId fields to the new ticket's id
2704 while ( my $ticket = $old_mergees->Next() ) {
2705 my ( $val, $msg ) = $ticket->__Set(
2706 Field => 'EffectiveId',
2707 Value => $MergeInto->Id()
2711 #make a new link: this ticket is merged into that other ticket.
2712 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2714 $MergeInto->_SetLastUpdated;
2716 $RT::Handle->Commit();
2717 return ( 1, $self->loc("Merge Successful") );
2722 Returns list of tickets' ids that's been merged into this ticket.
2729 my $mergees = new RT::Tickets( $self->CurrentUser );
2731 FIELD => 'EffectiveId',
2740 return map $_->id, @{ $mergees->ItemsArrayRef || [] };
2747 # {{{ Routines dealing with ownership
2753 Takes nothing and returns an RT::User object of
2761 #If this gets ACLed, we lose on a rights check in User.pm and
2762 #get deep recursion. if we need ACLs here, we need
2763 #an equiv without ACLs
2765 my $owner = new RT::User( $self->CurrentUser );
2766 $owner->Load( $self->__Value('Owner') );
2768 #Return the owner object
2774 # {{{ sub OwnerAsString
2776 =head2 OwnerAsString
2778 Returns the owner's email address
2784 return ( $self->OwnerObj->EmailAddress );
2794 Takes two arguments:
2795 the Id or Name of the owner
2796 and (optionally) the type of the SetOwner Transaction. It defaults
2797 to 'Give'. 'Steal' is also a valid option.
2804 my $NewOwner = shift;
2805 my $Type = shift || "Give";
2807 $RT::Handle->BeginTransaction();
2809 $self->_SetLastUpdated(); # lock the ticket
2810 $self->Load( $self->id ); # in case $self changed while waiting for lock
2812 my $OldOwnerObj = $self->OwnerObj;
2814 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2815 $NewOwnerObj->Load( $NewOwner );
2816 unless ( $NewOwnerObj->Id ) {
2817 $RT::Handle->Rollback();
2818 return ( 0, $self->loc("That user does not exist") );
2822 # must have ModifyTicket rights
2823 # or TakeTicket/StealTicket and $NewOwner is self
2824 # see if it's a take
2825 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2826 unless ( $self->CurrentUserHasRight('ModifyTicket')
2827 || $self->CurrentUserHasRight('TakeTicket') ) {
2828 $RT::Handle->Rollback();
2829 return ( 0, $self->loc("Permission Denied") );
2833 # see if it's a steal
2834 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2835 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2837 unless ( $self->CurrentUserHasRight('ModifyTicket')
2838 || $self->CurrentUserHasRight('StealTicket') ) {
2839 $RT::Handle->Rollback();
2840 return ( 0, $self->loc("Permission Denied") );
2844 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2845 $RT::Handle->Rollback();
2846 return ( 0, $self->loc("Permission Denied") );
2850 # If we're not stealing and the ticket has an owner and it's not
2852 if ( $Type ne 'Steal' and $Type ne 'Force'
2853 and $OldOwnerObj->Id != $RT::Nobody->Id
2854 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2856 $RT::Handle->Rollback();
2857 return ( 0, $self->loc("You can only take tickets that are unowned") )
2858 if $NewOwnerObj->id == $self->CurrentUser->id;
2861 $self->loc("You can only reassign tickets that you own or that are unowned" )
2865 #If we've specified a new owner and that user can't modify the ticket
2866 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2867 $RT::Handle->Rollback();
2868 return ( 0, $self->loc("That user may not own tickets in that queue") );
2871 # If the ticket has an owner and it's the new owner, we don't need
2873 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2874 $RT::Handle->Rollback();
2875 return ( 0, $self->loc("That user already owns that ticket") );
2878 # Delete the owner in the owner group, then add a new one
2879 # TODO: is this safe? it's not how we really want the API to work
2880 # for most things, but it's fast.
2881 my ( $del_id, $del_msg );
2882 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2883 ($del_id, $del_msg) = $owner->Delete();
2884 last unless ($del_id);
2888 $RT::Handle->Rollback();
2889 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2892 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2893 PrincipalId => $NewOwnerObj->PrincipalId,
2894 InsideTransaction => 1 );
2896 $RT::Handle->Rollback();
2897 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2900 # We call set twice with slightly different arguments, so
2901 # as to not have an SQL transaction span two RT transactions
2903 my ( $val, $msg ) = $self->_Set(
2905 RecordTransaction => 0,
2906 Value => $NewOwnerObj->Id,
2908 TransactionType => $Type,
2909 CheckACL => 0, # don't check acl
2913 $RT::Handle->Rollback;
2914 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2917 ($val, $msg) = $self->_NewTransaction(
2920 NewValue => $NewOwnerObj->Id,
2921 OldValue => $OldOwnerObj->Id,
2926 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2927 $OldOwnerObj->Name, $NewOwnerObj->Name );
2930 $RT::Handle->Rollback();
2934 $RT::Handle->Commit();
2936 return ( $val, $msg );
2945 A convenince method to set the ticket's owner to the current user
2951 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2960 Convenience method to set the owner to 'nobody' if the current user is the owner.
2966 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2975 A convenience method to change the owner of the current ticket to the
2976 current user. Even if it's owned by another user.
2983 if ( $self->IsOwner( $self->CurrentUser ) ) {
2984 return ( 0, $self->loc("You already own this ticket") );
2987 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2997 # {{{ Routines dealing with status
2999 # {{{ sub ValidateStatus
3001 =head2 ValidateStatus STATUS
3003 Takes a string. Returns true if that status is a valid status for this ticket.
3004 Returns false otherwise.
3008 sub ValidateStatus {
3012 #Make sure the status passed in is valid
3013 unless ( $self->QueueObj->IsValidStatus($status) ) {
3025 =head2 SetStatus STATUS
3027 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3029 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE). If FORCE is true, ignore unresolved dependencies and force a status change.
3040 $args{Status} = shift;
3047 if ( $args{Status} eq 'deleted') {
3048 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3049 return ( 0, $self->loc('Permission Denied') );
3052 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3053 return ( 0, $self->loc('Permission Denied') );
3057 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3058 return (0, $self->loc('That ticket has unresolved dependencies'));
3061 my $now = RT::Date->new( $self->CurrentUser );
3064 #If we're changing the status from new, record that we've started
3065 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3067 #Set the Started time to "now"
3068 $self->_Set( Field => 'Started',
3070 RecordTransaction => 0 );
3073 #When we close a ticket, set the 'Resolved' attribute to now.
3074 # It's misnamed, but that's just historical.
3075 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3076 $self->_Set( Field => 'Resolved',
3078 RecordTransaction => 0 );
3081 #Actually update the status
3082 my ($val, $msg)= $self->_Set( Field => 'Status',
3083 Value => $args{Status},
3086 TransactionType => 'Status' );
3097 Takes no arguments. Marks this ticket for garbage collection
3103 return ( $self->SetStatus('deleted') );
3105 # TODO: garbage collection
3114 Sets this ticket's status to stalled
3120 return ( $self->SetStatus('stalled') );
3129 Sets this ticket's status to rejected
3135 return ( $self->SetStatus('rejected') );
3144 Sets this ticket\'s status to Open
3150 return ( $self->SetStatus('open') );
3159 Sets this ticket\'s status to Resolved
3165 return ( $self->SetStatus('resolved') );
3173 # {{{ Actions + Routines dealing with transactions
3175 # {{{ sub SetTold and _SetTold
3177 =head2 SetTold ISO [TIMETAKEN]
3179 Updates the told and records a transaction
3186 $told = shift if (@_);
3187 my $timetaken = shift || 0;
3189 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3190 return ( 0, $self->loc("Permission Denied") );
3193 my $datetold = new RT::Date( $self->CurrentUser );
3195 $datetold->Set( Format => 'iso',
3199 $datetold->SetToNow();
3202 return ( $self->_Set( Field => 'Told',
3203 Value => $datetold->ISO,
3204 TimeTaken => $timetaken,
3205 TransactionType => 'Told' ) );
3210 Updates the told without a transaction or acl check. Useful when we're sending replies.
3217 my $now = new RT::Date( $self->CurrentUser );
3220 #use __Set to get no ACLs ;)
3221 return ( $self->__Set( Field => 'Told',
3222 Value => $now->ISO ) );
3232 my $uid = $self->CurrentUser->id;
3233 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3234 return if $attr && $attr->Content gt $self->LastUpdated;
3236 my $txns = $self->Transactions;
3237 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3238 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3239 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3243 VALUE => $attr->Content
3245 $txns->RowsPerPage(1);
3246 return $txns->First;
3251 =head2 TransactionBatch
3253 Returns an array reference of all transactions created on this ticket during
3254 this ticket object's lifetime or since last application of a batch, or undef
3257 Only works when the C<UseTransactionBatch> config option is set to true.
3261 sub TransactionBatch {
3263 return $self->{_TransactionBatch};
3266 =head2 ApplyTransactionBatch
3268 Applies scrips on the current batch of transactions and shinks it. Usually
3269 batch is applied when object is destroyed, but in some cases it's too late.
3273 sub ApplyTransactionBatch {
3276 my $batch = $self->TransactionBatch;
3277 return unless $batch && @$batch;
3279 $self->_ApplyTransactionBatch;
3281 $self->{_TransactionBatch} = [];
3284 sub _ApplyTransactionBatch {
3286 my $batch = $self->TransactionBatch;
3289 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3292 RT::Scrips->new($RT::SystemUser)->Apply(
3293 Stage => 'TransactionBatch',
3295 TransactionObj => $batch->[0],
3299 # Entry point of the rule system
3300 my $rules = RT::Ruleset->FindAllRules(
3301 Stage => 'TransactionBatch',
3303 TransactionObj => $batch->[0],
3306 RT::Ruleset->CommitRules($rules);
3312 # DESTROY methods need to localize $@, or it may unset it. This
3313 # causes $m->abort to not bubble all of the way up. See perlbug
3314 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3317 # The following line eliminates reentrancy.
3318 # It protects against the fact that perl doesn't deal gracefully
3319 # when an object's refcount is changed in its destructor.
3320 return if $self->{_Destroyed}++;
3322 my $batch = $self->TransactionBatch;
3323 return unless $batch && @$batch;
3325 return $self->_ApplyTransactionBatch;
3330 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3332 # {{{ sub _OverlayAccessible
3334 sub _OverlayAccessible {
3336 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3337 Queue => { 'read' => 1, 'write' => 1 },
3338 Requestors => { 'read' => 1, 'write' => 1 },
3339 Owner => { 'read' => 1, 'write' => 1 },
3340 Subject => { 'read' => 1, 'write' => 1 },
3341 InitialPriority => { 'read' => 1, 'write' => 1 },
3342 FinalPriority => { 'read' => 1, 'write' => 1 },
3343 Priority => { 'read' => 1, 'write' => 1 },
3344 Status => { 'read' => 1, 'write' => 1 },
3345 TimeEstimated => { 'read' => 1, 'write' => 1 },
3346 TimeWorked => { 'read' => 1, 'write' => 1 },
3347 TimeLeft => { 'read' => 1, 'write' => 1 },
3348 Told => { 'read' => 1, 'write' => 1 },
3349 Resolved => { 'read' => 1 },
3350 Type => { 'read' => 1 },
3351 Starts => { 'read' => 1, 'write' => 1 },
3352 Started => { 'read' => 1, 'write' => 1 },
3353 Due => { 'read' => 1, 'write' => 1 },
3354 Creator => { 'read' => 1, 'auto' => 1 },
3355 Created => { 'read' => 1, 'auto' => 1 },
3356 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3357 LastUpdated => { 'read' => 1, 'auto' => 1 }
3369 my %args = ( Field => undef,
3372 RecordTransaction => 1,
3375 TransactionType => 'Set',
3378 if ($args{'CheckACL'}) {
3379 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3380 return ( 0, $self->loc("Permission Denied"));
3384 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3385 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3386 return(0, $self->loc("Internal Error"));
3389 #if the user is trying to modify the record
3391 #Take care of the old value we really don't want to get in an ACL loop.
3392 # so ask the super::_Value
3393 my $Old = $self->SUPER::_Value("$args{'Field'}");
3396 if ( $args{'UpdateTicket'} ) {
3399 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3400 Value => $args{'Value'} );
3402 #If we can't actually set the field to the value, don't record
3403 # a transaction. instead, get out of here.
3404 return ( 0, $msg ) unless $ret;
3407 if ( $args{'RecordTransaction'} == 1 ) {
3409 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3410 Type => $args{'TransactionType'},
3411 Field => $args{'Field'},
3412 NewValue => $args{'Value'},
3414 TimeTaken => $args{'TimeTaken'},
3416 return ( $Trans, scalar $TransObj->BriefDescription );
3419 return ( $ret, $msg );
3429 Takes the name of a table column.
3430 Returns its value as a string, if the user passes an ACL check
3439 #if the field is public, return it.
3440 if ( $self->_Accessible( $field, 'public' ) ) {
3442 #$RT::Logger->debug("Skipping ACL check for $field");
3443 return ( $self->SUPER::_Value($field) );
3447 #If the current user doesn't have ACLs, don't let em at it.
3449 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3452 return ( $self->SUPER::_Value($field) );
3458 # {{{ sub _UpdateTimeTaken
3460 =head2 _UpdateTimeTaken
3462 This routine will increment the timeworked counter. it should
3463 only be called from _NewTransaction
3467 sub _UpdateTimeTaken {
3469 my $Minutes = shift;
3472 $Total = $self->SUPER::_Value("TimeWorked");
3473 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3475 Field => "TimeWorked",
3486 # {{{ Routines dealing with ACCESS CONTROL
3488 # {{{ sub CurrentUserHasRight
3490 =head2 CurrentUserHasRight
3492 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3493 1 if the user has that right. It returns 0 if the user doesn't have that right.
3497 sub CurrentUserHasRight {
3501 return $self->CurrentUser->PrincipalObj->HasRight(
3513 Takes a paramhash with the attributes 'Right' and 'Principal'
3514 'Right' is a ticket-scoped textual right from RT::ACE
3515 'Principal' is an RT::User object
3517 Returns 1 if the principal has the right. Returns undef if not.
3529 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3531 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3532 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3537 $args{'Principal'}->HasRight(
3539 Right => $args{'Right'}
3550 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3551 It isn't acutally a searchbuilder collection itself.
3558 unless ($self->{'__reminders'}) {
3559 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3560 $self->{'__reminders'}->Ticket($self->id);
3562 return $self->{'__reminders'};
3568 # {{{ sub Transactions
3572 Returns an RT::Transactions object of all transactions on this ticket
3579 my $transactions = RT::Transactions->new( $self->CurrentUser );
3581 #If the user has no rights, return an empty object
3582 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3583 $transactions->LimitToTicket($self->id);
3585 # if the user may not see comments do not return them
3586 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3587 $transactions->Limit(
3593 $transactions->Limit(
3597 VALUE => "CommentEmailRecord",
3598 ENTRYAGGREGATOR => 'AND'
3603 $transactions->Limit(
3607 ENTRYAGGREGATOR => 'AND'
3611 return ($transactions);
3617 # {{{ TransactionCustomFields
3619 =head2 TransactionCustomFields
3621 Returns the custom fields that transactions on tickets will have.
3625 sub TransactionCustomFields {
3627 return $self->QueueObj->TicketTransactionCustomFields;
3632 # {{{ sub CustomFieldValues
3634 =head2 CustomFieldValues
3636 # Do name => id mapping (if needed) before falling back to
3637 # RT::Record's CustomFieldValues
3643 sub CustomFieldValues {
3647 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3649 my $cf = RT::CustomField->new( $self->CurrentUser );
3650 $cf->SetContextObject( $self );
3651 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3652 unless ( $cf->id ) {
3653 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3656 # If we didn't find a valid cfid, give up.
3657 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3659 return $self->SUPER::CustomFieldValues( $cf->id );
3664 # {{{ sub CustomFieldLookupType
3666 =head2 CustomFieldLookupType
3668 Returns the RT::Ticket lookup type, which can be passed to
3669 RT::CustomField->Create() via the 'LookupType' hash key.
3675 sub CustomFieldLookupType {
3676 "RT::Queue-RT::Ticket";
3679 =head2 ACLEquivalenceObjects
3681 This method returns a list of objects for which a user's rights also apply
3682 to this ticket. Generally, this is only the ticket's queue, but some RT
3683 extensions may make other objects available too.
3685 This method is called from L<RT::Principal/HasRight>.
3689 sub ACLEquivalenceObjects {
3691 return $self->QueueObj;
3700 Jesse Vincent, jesse@bestpractical.com