1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 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 }}}
52 my $ticket = RT::Ticket->new($CurrentUser);
53 $ticket->Load($ticket_id);
57 This module lets you manipulate RT's ticket object.
81 use RT::URI::fsck_com_rt;
83 use RT::URI::freeside;
85 use Devel::GlobalDestruction;
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',
116 # A helper table for links mapping to make it easier
117 # to build and parse links between tickets
120 MemberOf => { Base => 'MemberOf',
121 Target => 'HasMember', },
122 RefersTo => { Base => 'RefersTo',
123 Target => 'ReferredToBy', },
124 DependsOn => { Base => 'DependsOn',
125 Target => 'DependedOnBy', },
126 MergedInto => { Base => 'MergedInto',
127 Target => 'MergedInto', },
132 sub LINKTYPEMAP { return \%LINKTYPEMAP }
133 sub LINKDIRMAP { return \%LINKDIRMAP }
143 Takes a single argument. This can be a ticket id, ticket alias or
144 local ticket uri. If the ticket can't be loaded, returns undef.
145 Otherwise, returns the ticket id.
152 $id = '' unless defined $id;
154 # TODO: modify this routine to look at EffectiveId and
155 # do the recursive load thing. be careful to cache all
156 # the interim tickets we try so we don't loop forever.
158 unless ( $id =~ /^\d+$/ ) {
159 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
163 $id = $MERGE_CACHE{'effective'}{ $id }
164 if $MERGE_CACHE{'effective'}{ $id };
166 my ($ticketid, $msg) = $self->LoadById( $id );
167 unless ( $self->Id ) {
168 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
172 #If we're merged, resolve the merge.
173 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
175 "We found a merged ticket. "
176 . $self->id ."/". $self->EffectiveId
178 my $real_id = $self->Load( $self->EffectiveId );
179 $MERGE_CACHE{'effective'}{ $id } = $real_id;
183 #Ok. we're loaded. lets get outa here.
191 Arguments: ARGS is a hash of named parameters. Valid parameters are:
194 Queue - Either a Queue object or a Queue Name
195 Requestor - A reference to a list of email addresses or RT user Names
196 Cc - A reference to a list of email addresses or Names
197 AdminCc - A reference to a list of email addresses or Names
198 SquelchMailTo - A reference to a list of email addresses -
199 who should this ticket not mail
200 Type -- The ticket's type. ignore this for now
201 Owner -- This ticket's owner. either an RT::User object or this user's id
202 Subject -- A string describing the subject of the ticket
203 Priority -- an integer from 0 to 99
204 InitialPriority -- an integer from 0 to 99
205 FinalPriority -- an integer from 0 to 99
206 Status -- any valid status for Queue's Lifecycle, otherwises uses on_create from Lifecycle default
207 TimeEstimated -- an integer. estimated time for this task in minutes
208 TimeWorked -- an integer. time worked so far in minutes
209 TimeLeft -- an integer. time remaining in minutes
210 Starts -- an ISO date describing the ticket's start date and time in GMT
211 Due -- an ISO date describing the ticket's due date and time in GMT
212 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
213 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
215 Ticket links can be set up during create by passing the link type as a hask key and
216 the ticket id to be linked to as a value (or a URI when linking to other objects).
217 Multiple links of the same type can be created by passing an array ref. For example:
220 DependsOn => [ 15, 22 ],
221 RefersTo => 'http://www.bestpractical.com',
223 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
224 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
225 C<Members> and C<Children> are aliases for C<HasMember>.
227 Returns: TICKETID, Transaction Object, Error Message
237 EffectiveId => undef,
242 SquelchMailTo => undef,
243 TransSquelchMailTo => undef,
247 InitialPriority => undef,
248 FinalPriority => undef,
258 WillResolve => undef,
260 _RecordTransaction => 1,
265 my ($ErrStr, @non_fatal_errors);
267 my $QueueObj = RT::Queue->new( RT->SystemUser );
268 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
269 $QueueObj->Load( $args{'Queue'}->Id );
271 elsif ( $args{'Queue'} ) {
272 $QueueObj->Load( $args{'Queue'} );
275 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
278 #Can't create a ticket without a queue.
279 unless ( $QueueObj->Id ) {
280 $RT::Logger->debug("$self No queue given for ticket creation.");
281 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
285 #Now that we have a queue, Check the ACLS
287 $self->CurrentUser->HasRight(
288 Right => 'CreateTicket',
295 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
298 my $cycle = $QueueObj->Lifecycle;
299 unless ( defined $args{'Status'} && length $args{'Status'} ) {
300 $args{'Status'} = $cycle->DefaultOnCreate;
303 $args{'Status'} = lc $args{'Status'};
304 unless ( $cycle->IsValid( $args{'Status'} ) ) {
306 $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
307 $self->loc($args{'Status'}))
311 unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
313 $self->loc("New tickets can not have status '[_1]' in this queue.",
314 $self->loc($args{'Status'}))
320 #Since we have a queue, we can set queue defaults
323 # If there's no queue default initial priority and it's not set, set it to 0
324 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
325 unless defined $args{'InitialPriority'};
328 # If there's no queue default final priority and it's not set, set it to 0
329 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
330 unless defined $args{'FinalPriority'};
332 # Priority may have changed from InitialPriority, for the case
333 # where we're importing tickets (eg, from an older RT version.)
334 $args{'Priority'} = $args{'InitialPriority'}
335 unless defined $args{'Priority'};
338 #TODO we should see what sort of due date we're getting, rather +
339 # than assuming it's in ISO format.
341 #Set the due date. if we didn't get fed one, use the queue default due in
342 my $Due = RT::Date->new( $self->CurrentUser );
343 if ( defined $args{'Due'} ) {
344 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
346 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
348 $Due->AddDays( $due_in );
351 my $Starts = RT::Date->new( $self->CurrentUser );
352 if ( defined $args{'Starts'} ) {
353 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
356 my $Started = RT::Date->new( $self->CurrentUser );
357 if ( defined $args{'Started'} ) {
358 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
361 my $WillResolve = RT::Date->new($self->CurrentUser );
362 if ( defined $args{'WillResolve'} ) {
363 $WillResolve->Set( Format => 'ISO', Value => $args{'WillResolve'} );
366 # If the status is not an initial status, set the started date
367 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
371 my $Resolved = RT::Date->new( $self->CurrentUser );
372 if ( defined $args{'Resolved'} ) {
373 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
376 #If the status is an inactive status, set the resolved date
377 elsif ( $cycle->IsInactive( $args{'Status'} ) )
379 $RT::Logger->debug( "Got a ". $args{'Status'}
380 ."(inactive) ticket with undefined resolved date. Setting to now."
387 # Dealing with time fields
389 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
390 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
391 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
395 # Deal with setting the owner
398 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
399 if ( $args{'Owner'}->id ) {
400 $Owner = $args{'Owner'};
402 $RT::Logger->error('Passed an empty RT::User for owner');
403 push @non_fatal_errors,
404 $self->loc("Owner could not be set.") . " ".
405 $self->loc("Invalid value for [_1]",loc('owner'));
410 #If we've been handed something else, try to load the user.
411 elsif ( $args{'Owner'} ) {
412 $Owner = RT::User->new( $self->CurrentUser );
413 $Owner->Load( $args{'Owner'} );
415 $Owner->LoadByEmail( $args{'Owner'} )
417 unless ( $Owner->Id ) {
418 push @non_fatal_errors,
419 $self->loc("Owner could not be set.") . " "
420 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
425 #If we have a proposed owner and they don't have the right
426 #to own a ticket, scream about it and make them not the owner
429 if ( $Owner && $Owner->Id != RT->Nobody->Id
430 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
432 $DeferOwner = $Owner;
434 $RT::Logger->debug('going to deffer setting owner');
438 #If we haven't been handed a valid owner, make it nobody.
439 unless ( defined($Owner) && $Owner->Id ) {
440 $Owner = RT::User->new( $self->CurrentUser );
441 $Owner->Load( RT->Nobody->Id );
446 # We attempt to load or create each of the people who might have a role for this ticket
447 # _outside_ the transaction, so we don't get into ticket creation races
448 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
449 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
450 foreach my $watcher ( splice @{ $args{$type} } ) {
451 next unless $watcher;
452 if ( $watcher =~ /^\d+$/ ) {
453 push @{ $args{$type} }, $watcher;
455 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
456 foreach my $address( @addresses ) {
457 my $user = RT::User->new( RT->SystemUser );
458 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
460 push @non_fatal_errors,
461 $self->loc("Couldn't load or create user: [_1]", $msg);
463 push @{ $args{$type} }, $user->id;
470 $args{'Type'} = lc $args{'Type'}
471 if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
473 $args{'Subject'} =~ s/\n//g;
475 $RT::Handle->BeginTransaction();
478 Queue => $QueueObj->Id,
480 Subject => $args{'Subject'},
481 InitialPriority => $args{'InitialPriority'},
482 FinalPriority => $args{'FinalPriority'},
483 Priority => $args{'Priority'},
484 Status => $args{'Status'},
485 TimeWorked => $args{'TimeWorked'},
486 TimeEstimated => $args{'TimeEstimated'},
487 TimeLeft => $args{'TimeLeft'},
488 Type => $args{'Type'},
489 Starts => $Starts->ISO,
490 Started => $Started->ISO,
491 Resolved => $Resolved->ISO,
492 WillResolve => $WillResolve->ISO,
496 # Parameters passed in during an import that we probably don't want to touch, otherwise
497 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
498 $params{$attr} = $args{$attr} if $args{$attr};
501 # Delete null integer parameters
503 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
505 delete $params{$attr}
506 unless ( exists $params{$attr} && $params{$attr} );
509 # Delete the time worked if we're counting it in the transaction
510 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
512 my ($id,$ticket_message) = $self->SUPER::Create( %params );
514 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
515 $RT::Handle->Rollback();
517 $self->loc("Ticket could not be created due to an internal error")
521 #Set the ticket's effective ID now that we've created it.
522 my ( $val, $msg ) = $self->__Set(
523 Field => 'EffectiveId',
524 Value => ( $args{'EffectiveId'} || $id )
527 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
528 $RT::Handle->Rollback;
530 $self->loc("Ticket could not be created due to an internal error")
534 my $create_groups_ret = $self->_CreateTicketGroups();
535 unless ($create_groups_ret) {
536 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
538 . ". aborting Ticket creation." );
539 $RT::Handle->Rollback();
541 $self->loc("Ticket could not be created due to an internal error")
545 # Set the owner in the Groups table
546 # We denormalize it into the Ticket table too because doing otherwise would
547 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
548 $self->OwnerGroup->_AddMember(
549 PrincipalId => $Owner->PrincipalId,
550 InsideTransaction => 1
551 ) unless $DeferOwner;
555 # Deal with setting up watchers
557 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
558 # we know it's an array ref
559 foreach my $watcher ( @{ $args{$type} } ) {
561 # Note that we're using AddWatcher, rather than _AddWatcher, as we
562 # actually _want_ that ACL check. Otherwise, random ticket creators
563 # could make themselves adminccs and maybe get ticket rights. that would
565 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
567 my ($val, $msg) = $self->$method(
569 PrincipalId => $watcher,
572 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
577 if ($args{'SquelchMailTo'}) {
578 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
579 : $args{'SquelchMailTo'};
580 $self->_SquelchMailTo( @squelch );
586 # Add all the custom fields
588 foreach my $arg ( keys %args ) {
589 next unless $arg =~ /^CustomField-(\d+)$/i;
593 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
595 next unless defined $value && length $value;
597 # Allow passing in uploaded LargeContent etc by hash reference
598 my ($status, $msg) = $self->_AddCustomFieldValue(
599 (UNIVERSAL::isa( $value => 'HASH' )
604 RecordTransaction => 0,
606 push @non_fatal_errors, $msg unless $status;
612 # Deal with setting up links
614 # TODO: Adding link may fire scrips on other end and those scrips
615 # could create transactions on this ticket before 'Create' transaction.
617 # We should implement different lifecycle: record 'Create' transaction,
618 # create links and only then fire create transaction's scrips.
620 # Ideal variant: add all links without firing scrips, record create
621 # transaction and only then fire scrips on the other ends of links.
625 foreach my $type ( keys %LINKTYPEMAP ) {
626 next unless ( defined $args{$type} );
628 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
630 my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
632 push @non_fatal_errors, $msg;
636 # Check rights on the other end of the link if we must
637 # then run _AddLink that doesn't check for ACLs
638 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
639 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
640 push @non_fatal_errors, $self->loc('Linking. Permission denied');
645 if ( $obj && lc $obj->Status eq 'deleted' ) {
646 push @non_fatal_errors,
647 $self->loc("Linking. Can't link to a deleted ticket");
651 my ( $wval, $wmsg ) = $self->_AddLink(
652 Type => $LINKTYPEMAP{$type}->{'Type'},
653 $LINKTYPEMAP{$type}->{'Mode'} => $link,
654 Silent => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
655 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
659 push @non_fatal_errors, $wmsg unless ($wval);
665 # {{{ Deal with auto-customer association
667 #unless we already have (a) customer(s)...
668 unless ( $self->Customers->Count ) {
670 #first find any requestors with emails but *without* customer targets
671 my @NoCust_Requestors =
672 grep { $_->EmailAddress && ! $_->Customers->Count }
673 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
675 for my $Requestor (@NoCust_Requestors) {
677 #perhaps the stuff in here should be in a User method??
679 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
681 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
683 ## false laziness w/RT/Interface/Web_Vendor.pm
684 my @link = ( 'Type' => 'MemberOf',
685 'Target' => "freeside://freeside/cust_main/$custnum",
688 my( $val, $msg ) = $Requestor->_AddLink(@link);
689 #XXX should do something with $msg# push @non_fatal_errors, $msg;
695 #find any requestors with customer targets
697 my %cust_target = ();
700 grep { $_->Customers->Count }
701 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
703 foreach my $Requestor ( @Requestors ) {
704 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
705 $cust_target{ $cust_link->Target } = 1;
709 #and then auto-associate this ticket with those customers
711 foreach my $cust_target ( keys %cust_target ) {
713 my @link = ( 'Type' => 'MemberOf',
714 #'Target' => "freeside://freeside/cust_main/$custnum",
715 'Target' => $cust_target,
718 my( $val, $msg ) = $self->_AddLink(@link);
719 push @non_fatal_errors, $msg;
727 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
728 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
730 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
732 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
733 . ") was proposed as a ticket owner but has no rights to own "
734 . "tickets in " . $QueueObj->Name );
735 push @non_fatal_errors, $self->loc(
736 "Owner '[_1]' does not have rights to own this ticket.",
740 $Owner = $DeferOwner;
741 $self->__Set(Field => 'Owner', Value => $Owner->id);
744 $self->OwnerGroup->_AddMember(
745 PrincipalId => $Owner->PrincipalId,
746 InsideTransaction => 1
750 #don't make a transaction or fire off any scrips for reminders either
751 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
753 # Add a transaction for the create
754 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
756 TimeTaken => $args{'TimeWorked'},
757 MIMEObj => $args{'MIMEObj'},
758 CommitScrips => !$args{'DryRun'},
759 SquelchMailTo => $args{'TransSquelchMailTo'},
762 if ( $self->Id && $Trans ) {
764 #$TransObj->UpdateCustomFields(ARGSRef => \%args);
765 $TransObj->UpdateCustomFields(%args);
767 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
768 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
769 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
772 $RT::Handle->Rollback();
774 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
775 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
776 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
779 if ( $args{'DryRun'} ) {
780 $RT::Handle->Rollback();
781 return ($self->id, $TransObj, $ErrStr);
783 $RT::Handle->Commit();
784 return ( $self->Id, $TransObj->Id, $ErrStr );
790 # Not going to record a transaction
791 $RT::Handle->Commit();
792 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
793 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
794 return ( $self->Id, 0, $ErrStr );
803 # Force lowercase on internal RT types
805 if $value =~ /^(ticket|approval|reminder)$/i;
806 return $self->_Set(Field => 'Type', Value => $value, @_);
811 =head2 _Parse822HeadersForAttributes Content
813 Takes an RFC822 style message and parses its attributes into a hash.
817 sub _Parse822HeadersForAttributes {
822 my @lines = ( split ( /\n/, $content ) );
823 while ( defined( my $line = shift @lines ) ) {
824 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
829 if ( defined( $args{$tag} ) )
830 { #if we're about to get a second value, make it an array
831 $args{$tag} = [ $args{$tag} ];
833 if ( ref( $args{$tag} ) )
834 { #If it's an array, we want to push the value
835 push @{ $args{$tag} }, $value;
837 else { #if there's nothing there, just set the value
838 $args{$tag} = $value;
840 } elsif ($line =~ /^$/) {
842 #TODO: this won't work, since "" isn't of the form "foo:value"
844 while ( defined( my $l = shift @lines ) ) {
845 push @{ $args{'content'} }, $l;
851 foreach my $date (qw(due starts started resolved)) {
852 my $dateobj = RT::Date->new(RT->SystemUser);
853 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
854 $dateobj->Set( Format => 'unix', Value => $args{$date} );
857 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
859 $args{$date} = $dateobj->ISO;
861 $args{'mimeobj'} = MIME::Entity->build(
862 Type => ( $args{'contenttype'} || 'text/plain' ),
864 Data => Encode::encode("UTF-8", ($args{'content'} || ''))
872 =head2 Import PARAMHASH
875 Doesn't create a transaction.
876 Doesn't supply queue defaults, etc.
884 my ( $ErrStr, $QueueObj, $Owner );
888 EffectiveId => undef,
892 Owner => RT->Nobody->Id,
893 Subject => '[no subject]',
894 InitialPriority => undef,
895 FinalPriority => undef,
906 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
907 $QueueObj = RT::Queue->new(RT->SystemUser);
908 $QueueObj->Load( $args{'Queue'} );
910 #TODO error check this and return 0 if it's not loading properly +++
912 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
913 $QueueObj = RT::Queue->new(RT->SystemUser);
914 $QueueObj->Load( $args{'Queue'}->Id );
918 "$self " . $args{'Queue'} . " not a recognised queue object." );
921 #Can't create a ticket without a queue.
922 unless ( defined($QueueObj) and $QueueObj->Id ) {
923 $RT::Logger->debug("$self No queue given for ticket creation.");
924 return ( 0, $self->loc('Could not create ticket. Queue not set') );
927 #Now that we have a queue, Check the ACLS
929 $self->CurrentUser->HasRight(
930 Right => 'CreateTicket',
936 $self->loc("No permission to create tickets in the queue '[_1]'"
940 # Deal with setting the owner
942 # Attempt to take user object, user name or user id.
943 # Assign to nobody if lookup fails.
944 if ( defined( $args{'Owner'} ) ) {
945 if ( ref( $args{'Owner'} ) ) {
946 $Owner = $args{'Owner'};
949 $Owner = RT::User->new( $self->CurrentUser );
950 $Owner->Load( $args{'Owner'} );
951 if ( !defined( $Owner->id ) ) {
952 $Owner->Load( RT->Nobody->id );
957 #If we have a proposed owner and they don't have the right
958 #to own a ticket, scream about it and make them not the owner
961 and ( $Owner->Id != RT->Nobody->Id )
971 $RT::Logger->warning( "$self user "
975 . "as a ticket owner but has no rights to own "
977 . $QueueObj->Name . "'" );
982 #If we haven't been handed a valid owner, make it nobody.
983 unless ( defined($Owner) ) {
984 $Owner = RT::User->new( $self->CurrentUser );
985 $Owner->Load( RT->Nobody->UserObj->Id );
990 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
991 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
994 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
995 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
996 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
997 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
999 # If we're coming in with an id, set that now.
1000 my $EffectiveId = undef;
1001 if ( $args{'id'} ) {
1002 $EffectiveId = $args{'id'};
1006 my $id = $self->SUPER::Create(
1008 EffectiveId => $EffectiveId,
1009 Queue => $QueueObj->Id,
1010 Owner => $Owner->Id,
1011 Subject => $args{'Subject'}, # loc
1012 InitialPriority => $args{'InitialPriority'}, # loc
1013 FinalPriority => $args{'FinalPriority'}, # loc
1014 Priority => $args{'InitialPriority'}, # loc
1015 Status => $args{'Status'}, # loc
1016 TimeWorked => $args{'TimeWorked'}, # loc
1017 Type => $args{'Type'}, # loc
1018 Created => $args{'Created'}, # loc
1019 Told => $args{'Told'}, # loc
1020 LastUpdated => $args{'Updated'}, # loc
1021 Resolved => $args{'Resolved'}, # loc
1022 Due => $args{'Due'}, # loc
1025 # If the ticket didn't have an id
1026 # Set the ticket's effective ID now that we've created it.
1027 if ( $args{'id'} ) {
1028 $self->Load( $args{'id'} );
1032 $self->__Set( Field => 'EffectiveId', Value => $id );
1036 $self . "->Import couldn't set EffectiveId: $msg" );
1040 my $create_groups_ret = $self->_CreateTicketGroups();
1041 unless ($create_groups_ret) {
1043 "Couldn't create ticket groups for ticket " . $self->Id );
1046 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1048 foreach my $watcher ( @{ $args{'Cc'} } ) {
1049 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1051 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
1052 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1055 foreach my $watcher ( @{ $args{'Requestor'} } ) {
1056 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1060 return ( $self->Id, $ErrStr );
1066 =head2 _CreateTicketGroups
1068 Create the ticket groups and links for this ticket.
1069 This routine expects to be called from Ticket->Create _inside of a transaction_
1071 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1073 It will return true on success and undef on failure.
1079 sub _CreateTicketGroups {
1082 my @types = (qw(Requestor Owner Cc AdminCc));
1084 foreach my $type (@types) {
1085 my $type_obj = RT::Group->new($self->CurrentUser);
1086 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1087 Instance => $self->Id,
1090 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1091 $self->Id.": ".$msg);
1103 A constructor which returns an RT::Group object containing the owner of this ticket.
1109 my $owner_obj = RT::Group->new($self->CurrentUser);
1110 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1111 return ($owner_obj);
1119 AddWatcher takes a parameter hash. The keys are as follows:
1121 Type One of Requestor, Cc, AdminCc
1123 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1125 Email The email address of the new watcher. If a user with this
1126 email address can't be found, a new nonprivileged user will be created.
1128 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.
1136 PrincipalId => undef,
1141 # ModifyTicket works in any case
1142 return $self->_AddWatcher( %args )
1143 if $self->CurrentUserHasRight('ModifyTicket');
1144 if ( $args{'Email'} ) {
1145 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1146 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1149 if ( lc $self->CurrentUser->EmailAddress
1150 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1152 $args{'PrincipalId'} = $self->CurrentUser->id;
1153 delete $args{'Email'};
1157 # If the watcher isn't the current user then the current user has no right
1159 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1160 return ( 0, $self->loc("Permission Denied") );
1163 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1164 if ( $args{'Type'} eq 'AdminCc' ) {
1165 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1166 return ( 0, $self->loc('Permission Denied') );
1170 # If it's a Requestor or Cc and they don't have 'Watch', bail
1171 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1172 unless ( $self->CurrentUserHasRight('Watch') ) {
1173 return ( 0, $self->loc('Permission Denied') );
1177 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1178 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1181 return $self->_AddWatcher( %args );
1184 #This contains the meat of AddWatcher. but can be called from a routine like
1185 # Create, which doesn't need the additional acl check
1191 PrincipalId => undef,
1197 my $principal = RT::Principal->new($self->CurrentUser);
1198 if ($args{'Email'}) {
1199 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1200 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
1202 my $user = RT::User->new(RT->SystemUser);
1203 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1204 $args{'PrincipalId'} = $pid if $pid;
1206 if ($args{'PrincipalId'}) {
1207 $principal->Load($args{'PrincipalId'});
1208 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1209 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
1210 if RT::EmailParser->IsRTAddress( $email );
1216 # If we can't find this watcher, we need to bail.
1217 unless ($principal->Id) {
1218 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1219 return(0, $self->loc("Could not find or create that user"));
1223 my $group = RT::Group->new($self->CurrentUser);
1224 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1225 unless ($group->id) {
1226 return(0,$self->loc("Group not found"));
1229 if ( $group->HasMember( $principal)) {
1231 return ( 0, $self->loc('[_1] is already a [_2] for this ticket',
1232 $principal->Object->Name, $self->loc($args{'Type'})) );
1236 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1237 InsideTransaction => 1 );
1239 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1241 return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
1242 $principal->Object->Name, $self->loc($args{'Type'})) );
1245 unless ( $args{'Silent'} ) {
1246 $self->_NewTransaction(
1247 Type => 'AddWatcher',
1248 NewValue => $principal->Id,
1249 Field => $args{'Type'}
1253 return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
1254 $principal->Object->Name, $self->loc($args{'Type'})) );
1260 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1263 Deletes a Ticket watcher. Takes two arguments:
1265 Type (one of Requestor,Cc,AdminCc)
1269 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1271 Email (the email address of an existing wathcer)
1280 my %args = ( Type => undef,
1281 PrincipalId => undef,
1285 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1286 return ( 0, $self->loc("No principal specified") );
1288 my $principal = RT::Principal->new( $self->CurrentUser );
1289 if ( $args{'PrincipalId'} ) {
1291 $principal->Load( $args{'PrincipalId'} );
1294 my $user = RT::User->new( $self->CurrentUser );
1295 $user->LoadByEmail( $args{'Email'} );
1296 $principal->Load( $user->Id );
1299 # If we can't find this watcher, we need to bail.
1300 unless ( $principal->Id ) {
1301 return ( 0, $self->loc("Could not find that principal") );
1304 my $group = RT::Group->new( $self->CurrentUser );
1305 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1306 unless ( $group->id ) {
1307 return ( 0, $self->loc("Group not found") );
1311 #If the watcher we're trying to add is for the current user
1312 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1314 # If it's an AdminCc and they don't have
1315 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1316 if ( $args{'Type'} eq 'AdminCc' ) {
1317 unless ( $self->CurrentUserHasRight('ModifyTicket')
1318 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1319 return ( 0, $self->loc('Permission Denied') );
1323 # If it's a Requestor or Cc and they don't have
1324 # 'Watch' or 'ModifyTicket', bail
1325 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1327 unless ( $self->CurrentUserHasRight('ModifyTicket')
1328 or $self->CurrentUserHasRight('Watch') ) {
1329 return ( 0, $self->loc('Permission Denied') );
1333 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1335 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1339 # If the watcher isn't the current user
1340 # and the current user doesn't have 'ModifyTicket' bail
1342 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1343 return ( 0, $self->loc("Permission Denied") );
1349 # see if this user is already a watcher.
1351 unless ( $group->HasMember($principal) ) {
1353 $self->loc( '[_1] is not a [_2] for this ticket',
1354 $principal->Object->Name, $args{'Type'} ) );
1357 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1359 $RT::Logger->error( "Failed to delete "
1361 . " as a member of group "
1367 'Could not remove [_1] as a [_2] for this ticket',
1368 $principal->Object->Name, $args{'Type'} ) );
1371 unless ( $args{'Silent'} ) {
1372 $self->_NewTransaction( Type => 'DelWatcher',
1373 OldValue => $principal->Id,
1374 Field => $args{'Type'} );
1378 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1379 $principal->Object->Name,
1387 =head2 SquelchMailTo [EMAIL]
1389 Takes an optional email address to never email about updates to this ticket.
1392 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1400 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1404 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1409 return $self->_SquelchMailTo(@_);
1412 sub _SquelchMailTo {
1416 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1417 unless grep { $_->Content eq $attr }
1418 $self->Attributes->Named('SquelchMailTo');
1420 my @attributes = $self->Attributes->Named('SquelchMailTo');
1421 return (@attributes);
1425 =head2 UnsquelchMailTo ADDRESS
1427 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1429 Returns a tuple of (status, message)
1433 sub UnsquelchMailTo {
1436 my $address = shift;
1437 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1438 return ( 0, $self->loc("Permission Denied") );
1441 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1442 return ($val, $msg);
1447 =head2 RequestorAddresses
1449 B<Returns> String: All Ticket Requestor email addresses as a string.
1453 sub RequestorAddresses {
1456 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1460 return ( $self->Requestors->MemberEmailAddressesAsString );
1464 =head2 AdminCcAddresses
1466 returns String: All Ticket AdminCc email addresses as a string
1470 sub AdminCcAddresses {
1473 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1477 return ( $self->AdminCc->MemberEmailAddressesAsString )
1483 returns String: All Ticket Ccs as a string of email addresses
1490 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1493 return ( $self->Cc->MemberEmailAddressesAsString);
1503 Returns this ticket's Requestors as an RT::Group object
1510 my $group = RT::Group->new($self->CurrentUser);
1511 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1512 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1520 Private non-ACLed variant of Reqeustors so that we can look them up for the
1521 purposes of customer auto-association during create.
1528 my $group = RT::Group->new($RT::SystemUser);
1529 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1536 Returns an RT::Group object which contains this ticket's Ccs.
1537 If the user doesn't have "ShowTicket" permission, returns an empty group
1544 my $group = RT::Group->new($self->CurrentUser);
1545 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1546 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1557 Returns an RT::Group object which contains this ticket's AdminCcs.
1558 If the user doesn't have "ShowTicket" permission, returns an empty group
1565 my $group = RT::Group->new($self->CurrentUser);
1566 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1567 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1576 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1578 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1580 Takes a param hash with the attributes Type and either PrincipalId or Email
1582 Type is one of Requestor, Cc, AdminCc and Owner
1584 PrincipalId is an RT::Principal id, and Email is an email address.
1586 Returns true if the specified principal (or the one corresponding to the
1587 specified address) is a member of the group Type for this ticket.
1589 XX TODO: This should be Memoized.
1596 my %args = ( Type => 'Requestor',
1597 PrincipalId => undef,
1602 # Load the relevant group.
1603 my $group = RT::Group->new($self->CurrentUser);
1604 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1606 # Find the relevant principal.
1607 if (!$args{PrincipalId} && $args{Email}) {
1608 # Look up the specified user.
1609 my $user = RT::User->new($self->CurrentUser);
1610 $user->LoadByEmail($args{Email});
1612 $args{PrincipalId} = $user->PrincipalId;
1615 # A non-existent user can't be a group member.
1620 # Ask if it has the member in question
1621 return $group->HasMember( $args{'PrincipalId'} );
1626 =head2 IsRequestor PRINCIPAL_ID
1628 Takes an L<RT::Principal> id.
1630 Returns true if the principal is a requestor of the current ticket.
1638 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1644 =head2 IsCc PRINCIPAL_ID
1646 Takes an RT::Principal id.
1647 Returns true if the principal is a Cc of the current ticket.
1656 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1662 =head2 IsAdminCc PRINCIPAL_ID
1664 Takes an RT::Principal id.
1665 Returns true if the principal is an AdminCc of the current ticket.
1673 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1681 Takes an RT::User object. Returns true if that user is this ticket's owner.
1682 returns undef otherwise
1690 # no ACL check since this is used in acl decisions
1691 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1695 #Tickets won't yet have owners when they're being created.
1696 unless ( $self->OwnerObj->id ) {
1700 if ( $person->id == $self->OwnerObj->id ) {
1712 =head2 TransactionAddresses
1714 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1715 all this ticket's Create, Comment or Correspond transactions. The keys are
1716 stringified email addresses. Each value is an L<Email::Address> object.
1718 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.
1723 sub TransactionAddresses {
1725 my $txns = $self->Transactions;
1729 my $attachments = RT::Attachments->new( $self->CurrentUser );
1730 $attachments->LimitByTicket( $self->id );
1731 $attachments->Columns( qw( id Headers TransactionId));
1734 foreach my $type (qw(Create Comment Correspond)) {
1735 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1739 ENTRYAGGREGATOR => 'OR',
1744 while ( my $att = $attachments->Next ) {
1745 foreach my $addrlist ( values %{$att->Addresses } ) {
1746 foreach my $addr (@$addrlist) {
1748 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1750 if ( $addresses{ $addr->address }
1751 && $addresses{ $addr->address }->phrase
1752 && not $addr->phrase );
1754 # skips "comment-only" addresses
1755 next unless ( $addr->address );
1756 $addresses{ $addr->address } = $addr;
1775 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1779 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1780 my $id = $QueueObj->Load($Value);
1794 my $NewQueue = shift;
1796 #Redundant. ACL gets checked in _Set;
1797 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1798 return ( 0, $self->loc("Permission Denied") );
1801 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1802 $NewQueueObj->Load($NewQueue);
1804 unless ( $NewQueueObj->Id() ) {
1805 return ( 0, $self->loc("That queue does not exist") );
1808 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1809 return ( 0, $self->loc('That is the same value') );
1811 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1812 return ( 0, $self->loc("You may not create requests in that queue.") );
1816 my $old_lifecycle = $self->QueueObj->Lifecycle;
1817 my $new_lifecycle = $NewQueueObj->Lifecycle;
1818 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1819 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1820 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1822 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status };
1823 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1827 if ( $new_status ) {
1828 my $clone = RT::Ticket->new( RT->SystemUser );
1829 $clone->Load( $self->Id );
1830 unless ( $clone->Id ) {
1831 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1834 my $now = RT::Date->new( $self->CurrentUser );
1837 my $old_status = $clone->Status;
1839 #If we're changing the status from initial in old to not intial in new,
1840 # record that we've started
1841 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1842 #Set the Started time to "now"
1846 RecordTransaction => 0
1850 #When we close a ticket, set the 'Resolved' attribute to now.
1851 # It's misnamed, but that's just historical.
1852 if ( $new_lifecycle->IsInactive($new_status) ) {
1854 Field => 'Resolved',
1856 RecordTransaction => 0,
1860 #Actually update the status
1861 my ($val, $msg)= $clone->_Set(
1863 Value => $new_status,
1864 RecordTransaction => 0,
1866 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1870 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1873 # Clear the queue object cache;
1874 $self->{_queue_obj} = undef;
1876 # Untake the ticket if we have no permissions in the new queue
1877 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1878 my $clone = RT::Ticket->new( RT->SystemUser );
1879 $clone->Load( $self->Id );
1880 unless ( $clone->Id ) {
1881 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1883 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1884 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1887 # On queue change, change queue for reminders too
1888 my $reminder_collection = $self->Reminders->Collection;
1889 while ( my $reminder = $reminder_collection->Next ) {
1890 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1891 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1895 return ($status, $msg);
1902 Takes nothing. returns this ticket's queue object
1909 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1911 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1913 #We call __Value so that we can avoid the ACL decision and some deep recursion
1914 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1916 return ($self->{_queue_obj});
1923 return $self->_Set( Field => 'Subject', Value => $value );
1928 Takes nothing. Returns SubjectTag for this ticket. Includes
1929 queue's subject tag or rtname if that is not set, ticket
1930 id and braces, for example:
1932 [support.example.com #123456]
1940 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1949 Returns an RT::Date object containing this ticket's due date
1956 my $time = RT::Date->new( $self->CurrentUser );
1958 # -1 is RT::Date slang for never
1959 if ( my $due = $self->Due ) {
1960 $time->Set( Format => 'sql', Value => $due );
1963 $time->Set( Format => 'unix', Value => -1 );
1973 Returns this ticket's due date as a human readable string
1979 return $self->DueObj->AsString();
1986 Returns an RT::Date object of this ticket's 'resolved' time.
1993 my $time = RT::Date->new( $self->CurrentUser );
1994 $time->Set( Format => 'sql', Value => $self->Resolved );
1999 =head2 FirstActiveStatus
2001 Returns the first active status that the ticket could transition to,
2002 according to its current Queue's lifecycle. May return undef if there
2003 is no such possible status to transition to, or we are already in it.
2004 This is used in L<RT::Action::AutoOpen>, for instance.
2008 sub FirstActiveStatus {
2011 my $lifecycle = $self->QueueObj->Lifecycle;
2012 my $status = $self->Status;
2013 my @active = $lifecycle->Active;
2014 # no change if no active statuses in the lifecycle
2015 return undef unless @active;
2017 # no change if the ticket is already has first status from the list of active
2018 return undef if lc $status eq lc $active[0];
2020 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
2024 =head2 FirstInactiveStatus
2026 Returns the first inactive status that the ticket could transition to,
2027 according to its current Queue's lifecycle. May return undef if there
2028 is no such possible status to transition to, or we are already in it.
2029 This is used in resolve action in UnsafeEmailCommands, for instance.
2033 sub FirstInactiveStatus {
2036 my $lifecycle = $self->QueueObj->Lifecycle;
2037 my $status = $self->Status;
2038 my @inactive = $lifecycle->Inactive;
2039 # no change if no inactive statuses in the lifecycle
2040 return undef unless @inactive;
2042 # no change if the ticket is already has first status from the list of inactive
2043 return undef if lc $status eq lc $inactive[0];
2045 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
2051 Takes a date in ISO format or undef
2052 Returns a transaction id and a message
2053 The client calls "Start" to note that the project was started on the date in $date.
2054 A null date means "now"
2060 my $time = shift || 0;
2062 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2063 return ( 0, $self->loc("Permission Denied") );
2066 #We create a date object to catch date weirdness
2067 my $time_obj = RT::Date->new( $self->CurrentUser() );
2069 $time_obj->Set( Format => 'ISO', Value => $time );
2072 $time_obj->SetToNow();
2075 # We need $TicketAsSystem, in case the current user doesn't have
2077 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
2078 $TicketAsSystem->Load( $self->Id );
2079 # Now that we're starting, open this ticket
2080 # TODO: do we really want to force this as policy? it should be a scrip
2081 my $next = $TicketAsSystem->FirstActiveStatus;
2083 $self->SetStatus( $next ) if defined $next;
2085 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2093 Returns an RT::Date object which contains this ticket's
2101 my $time = RT::Date->new( $self->CurrentUser );
2102 $time->Set( Format => 'sql', Value => $self->Started );
2110 Returns an RT::Date object which contains this ticket's
2118 my $time = RT::Date->new( $self->CurrentUser );
2119 $time->Set( Format => 'sql', Value => $self->Starts );
2127 Returns an RT::Date object which contains this ticket's
2135 my $time = RT::Date->new( $self->CurrentUser );
2136 $time->Set( Format => 'sql', Value => $self->Told );
2144 A convenience method that returns ToldObj->AsString
2146 TODO: This should be deprecated
2152 if ( $self->Told ) {
2153 return $self->ToldObj->AsString();
2162 =head2 TimeWorkedAsString
2164 Returns the amount of time worked on this ticket as a Text String
2168 sub TimeWorkedAsString {
2170 my $value = $self->TimeWorked;
2172 # return the # of minutes worked turned into seconds and written as
2173 # a simple text string, this is not really a date object, but if we
2174 # diff a number of seconds vs the epoch, we'll get a nice description
2176 return "" unless $value;
2177 return RT::Date->new( $self->CurrentUser )
2178 ->DurationAsString( $value * 60 );
2183 =head2 TimeLeftAsString
2185 Returns the amount of time left on this ticket as a Text String
2189 sub TimeLeftAsString {
2191 my $value = $self->TimeLeft;
2192 return "" unless $value;
2193 return RT::Date->new( $self->CurrentUser )
2194 ->DurationAsString( $value * 60 );
2202 Comment on this ticket.
2203 Takes a hash with the following attributes:
2204 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2207 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2209 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2210 They will, however, be prepared and you'll be able to access them through the TransactionObj
2212 Returns: Transaction id, Error Message, Transaction Object
2213 (note the different order from Create()!)
2220 my %args = ( CcMessageTo => undef,
2221 BccMessageTo => undef,
2228 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2229 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2230 return ( 0, $self->loc("Permission Denied"), undef );
2232 $args{'NoteType'} = 'Comment';
2234 $RT::Handle->BeginTransaction();
2235 if ($args{'DryRun'}) {
2236 $args{'CommitScrips'} = 0;
2239 my @results = $self->_RecordNote(%args);
2240 if ($args{'DryRun'}) {
2241 $RT::Handle->Rollback();
2243 $RT::Handle->Commit();
2252 Correspond on this ticket.
2253 Takes a hashref with the following attributes:
2256 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2258 if there's no MIMEObj, Content is used to build a MIME::Entity object
2260 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2261 They will, however, be prepared and you'll be able to access them through the TransactionObj
2263 Returns: Transaction id, Error Message, Transaction Object
2264 (note the different order from Create()!)
2271 my %args = ( CcMessageTo => undef,
2272 BccMessageTo => undef,
2278 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2279 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2280 return ( 0, $self->loc("Permission Denied"), undef );
2282 $args{'NoteType'} = 'Correspond';
2284 $RT::Handle->BeginTransaction();
2285 if ($args{'DryRun'}) {
2286 $args{'CommitScrips'} = 0;
2289 my @results = $self->_RecordNote(%args);
2291 unless ( $results[0] ) {
2292 $RT::Handle->Rollback();
2296 #Set the last told date to now if this isn't mail from the requestor.
2297 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2298 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2300 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2302 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2305 if ($args{'DryRun'}) {
2306 $RT::Handle->Rollback();
2308 $RT::Handle->Commit();
2319 the meat of both comment and correspond.
2321 Performs no access control checks. hence, dangerous.
2328 CcMessageTo => undef,
2329 BccMessageTo => undef,
2334 NoteType => 'Correspond',
2337 SquelchMailTo => undef,
2342 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2343 return ( 0, $self->loc("No message attached"), undef );
2346 unless ( $args{'MIMEObj'} ) {
2347 my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
2348 $args{'MIMEObj'} = MIME::Entity->build(
2349 Type => "text/plain",
2351 Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
2355 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
2356 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
2358 # convert text parts into utf-8
2359 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2361 # If we've been passed in CcMessageTo and BccMessageTo fields,
2362 # add them to the mime object for passing on to the transaction handler
2363 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2364 # RT-Send-Bcc: headers
2367 foreach my $type (qw/Cc Bcc/) {
2368 if ( defined $args{ $type . 'MessageTo' } ) {
2370 my $addresses = join ', ', (
2371 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2372 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2373 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
2377 foreach my $argument (qw(Encrypt Sign)) {
2378 $args{'MIMEObj'}->head->replace(
2379 "X-RT-$argument" => Encode::encode( "UTF-8", $args{ $argument } )
2380 ) if defined $args{ $argument };
2383 # If this is from an external source, we need to come up with its
2384 # internal Message-ID now, so all emails sent because of this
2385 # message have a common Message-ID
2386 my $org = RT->Config->Get('Organization');
2387 my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
2388 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2389 $args{'MIMEObj'}->head->set(
2390 'RT-Message-ID' => Encode::encode( "UTF-8",
2391 RT::Interface::Email::GenMessageId( Ticket => $self )
2396 #Record the correspondence (write the transaction)
2397 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2398 Type => $args{'NoteType'},
2399 Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
2400 TimeTaken => $args{'TimeTaken'},
2401 MIMEObj => $args{'MIMEObj'},
2402 CommitScrips => $args{'CommitScrips'},
2403 SquelchMailTo => $args{'SquelchMailTo'},
2404 CustomFields => $args{'CustomFields'},
2408 $RT::Logger->err("$self couldn't init a transaction $msg");
2409 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2412 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2418 Builds a MIME object from the given C<UpdateSubject> and
2419 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2420 C<< DryRun => 1 >>, and returns the transaction so produced.
2428 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2429 $action = 'Correspond';
2431 $action = 'Comment';
2434 my $Message = MIME::Entity->build(
2435 Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
2436 Type => 'text/plain',
2438 Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
2441 my ( $Transaction, $Description, $Object ) = $self->$action(
2442 CcMessageTo => $args{'UpdateCc'},
2443 BccMessageTo => $args{'UpdateBcc'},
2444 MIMEObj => $Message,
2445 TimeTaken => $args{'UpdateTimeWorked'},
2448 unless ( $Transaction ) {
2449 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2457 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2458 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2459 the resulting L<RT::Transaction>.
2466 my $Message = MIME::Entity->build(
2467 Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
2468 (defined $args{'Cc'} ?
2469 ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
2470 Type => 'text/plain',
2472 Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
2475 my ( $Transaction, $Object, $Description ) = $self->Create(
2476 Type => $args{'Type'} || 'ticket',
2477 Queue => $args{'Queue'},
2478 Owner => $args{'Owner'},
2479 Requestor => $args{'Requestors'},
2481 AdminCc => $args{'AdminCc'},
2482 InitialPriority => $args{'InitialPriority'},
2483 FinalPriority => $args{'FinalPriority'},
2484 TimeLeft => $args{'TimeLeft'},
2485 TimeEstimated => $args{'TimeEstimated'},
2486 TimeWorked => $args{'TimeWorked'},
2487 Subject => $args{'Subject'},
2488 Status => $args{'Status'},
2489 MIMEObj => $Message,
2492 unless ( $Transaction ) {
2493 $RT::Logger->error("Couldn't fire Create action: $Description");
2504 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2507 my $type = shift || "";
2509 my $cache_key = "$field$type";
2510 return $self->{ $cache_key } if $self->{ $cache_key };
2512 my $links = $self->{ $cache_key }
2513 = RT::Links->new( $self->CurrentUser );
2514 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2515 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2519 # Maybe this ticket is a merge ticket
2520 #my $limit_on = 'Local'. $field;
2521 # at least to myself
2523 FIELD => $field, #$limit_on,
2524 OPERATOR => 'MATCHES',
2525 VALUE => 'fsck.com-rt://%/ticket/'. $self->id,
2526 ENTRYAGGREGATOR => 'OR',
2529 FIELD => $field, #$limit_on,
2530 OPERATOR => 'MATCHES',
2531 VALUE => 'fsck.com-rt://%/ticket/'. $_,
2532 ENTRYAGGREGATOR => 'OR',
2533 ) foreach $self->Merged;
2546 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2547 SilentBase and SilentTarget. Either Base or Target must be null.
2548 The null value will be replaced with this ticket's id.
2550 If Silent is true then no transaction would be recorded, in other
2551 case you can control creation of transactions on both base and
2552 target with SilentBase and SilentTarget respectively. By default
2553 both transactions are created.
2564 SilentBase => undef,
2565 SilentTarget => undef,
2569 unless ( $args{'Target'} || $args{'Base'} ) {
2570 $RT::Logger->error("Base or Target must be specified");
2571 return ( 0, $self->loc('Either base or target must be specified') );
2576 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2577 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2578 return ( 0, $self->loc("Permission Denied") );
2581 # If the other URI is an RT::Ticket, we want to make sure the user
2582 # can modify it too...
2583 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2584 return (0, $msg) unless $status;
2585 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2588 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2589 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2591 return ( 0, $self->loc("Permission Denied") );
2594 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2595 return ( 0, $Msg ) unless $val;
2597 return ( $val, $Msg ) if $args{'Silent'};
2599 my ($direction, $remote_link);
2601 if ( $args{'Base'} ) {
2602 $remote_link = $args{'Base'};
2603 $direction = 'Target';
2605 elsif ( $args{'Target'} ) {
2606 $remote_link = $args{'Target'};
2607 $direction = 'Base';
2610 my $remote_uri = RT::URI->new( $self->CurrentUser );
2611 $remote_uri->FromURI( $remote_link );
2613 unless ( $args{ 'Silent'. $direction } ) {
2614 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2615 Type => 'DeleteLink',
2616 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2617 OldValue => $remote_uri->URI || $remote_link,
2620 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2623 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2624 my $OtherObj = $remote_uri->Object;
2625 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2626 Type => 'DeleteLink',
2627 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2628 : $LINKDIRMAP{$args{'Type'}}->{Target},
2629 OldValue => $self->URI,
2630 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2633 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2636 return ( $val, $Msg );
2643 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2645 If Silent is true then no transaction would be recorded, in other
2646 case you can control creation of transactions on both base and
2647 target with SilentBase and SilentTarget respectively. By default
2648 both transactions are created.
2654 my %args = ( Target => '',
2658 SilentBase => undef,
2659 SilentTarget => undef,
2662 unless ( $args{'Target'} || $args{'Base'} ) {
2663 $RT::Logger->error("Base or Target must be specified");
2664 return ( 0, $self->loc('Either base or target must be specified') );
2668 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2669 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2670 return ( 0, $self->loc("Permission Denied") );
2673 # If the other URI is an RT::Ticket, we want to make sure the user
2674 # can modify it too...
2675 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2676 return (0, $msg) unless $status;
2677 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2680 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2681 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2683 return ( 0, $self->loc("Permission Denied") );
2686 return ( 0, "Can't link to a deleted ticket" )
2687 if $other_ticket && lc $other_ticket->Status eq 'deleted';
2689 return $self->_AddLink(%args);
2692 sub __GetTicketFromURI {
2694 my %args = ( URI => '', @_ );
2696 # If the other URI is an RT::Ticket, we want to make sure the user
2697 # can modify it too...
2698 my $uri_obj = RT::URI->new( $self->CurrentUser );
2699 unless ($uri_obj->FromURI( $args{'URI'} )) {
2700 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2701 $RT::Logger->warning( $msg );
2704 my $obj = $uri_obj->Resolver->Object;
2705 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2706 return (1, 'Found not a ticket', undef);
2708 return (1, 'Found ticket', $obj);
2713 Private non-acled variant of AddLink so that links can be added during create.
2719 my %args = ( Target => '',
2723 SilentBase => undef,
2724 SilentTarget => undef,
2727 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2728 return ($val, $msg) if !$val || $exist;
2729 return ($val, $msg) if $args{'Silent'};
2731 my ($direction, $remote_link);
2732 if ( $args{'Target'} ) {
2733 $remote_link = $args{'Target'};
2734 $direction = 'Base';
2735 } elsif ( $args{'Base'} ) {
2736 $remote_link = $args{'Base'};
2737 $direction = 'Target';
2740 my $remote_uri = RT::URI->new( $self->CurrentUser );
2741 $remote_uri->FromURI( $remote_link );
2743 unless ( $args{ 'Silent'. $direction } ) {
2744 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2746 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2747 NewValue => $remote_uri->URI || $remote_link,
2750 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2753 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2754 my $OtherObj = $remote_uri->Object;
2755 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2757 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2758 : $LINKDIRMAP{$args{'Type'}}->{Target},
2759 NewValue => $self->URI,
2760 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2763 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2766 return ( $val, $msg );
2774 MergeInto take the id of the ticket to merge this ticket into.
2780 my $ticket_id = shift;
2782 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2783 return ( 0, $self->loc("Permission Denied") );
2786 # Load up the new ticket.
2787 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2788 $MergeInto->Load($ticket_id);
2790 # make sure it exists.
2791 unless ( $MergeInto->Id ) {
2792 return ( 0, $self->loc("New ticket doesn't exist") );
2795 # Make sure the current user can modify the new ticket.
2796 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2797 return ( 0, $self->loc("Permission Denied") );
2800 delete $MERGE_CACHE{'effective'}{ $self->id };
2801 delete @{ $MERGE_CACHE{'merged'} }{
2802 $ticket_id, $MergeInto->id, $self->id
2805 $RT::Handle->BeginTransaction();
2807 $self->_MergeInto( $MergeInto );
2809 $RT::Handle->Commit();
2811 return ( 1, $self->loc("Merge Successful") );
2816 my $MergeInto = shift;
2819 # We use EffectiveId here even though it duplicates information from
2820 # the links table becasue of the massive performance hit we'd take
2821 # by trying to do a separate database query for merge info everytime
2824 #update this ticket's effective id to the new ticket's id.
2825 my ( $id_val, $id_msg ) = $self->__Set(
2826 Field => 'EffectiveId',
2827 Value => $MergeInto->Id()
2831 $RT::Handle->Rollback();
2832 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2836 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2837 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2838 my ( $status_val, $status_msg )
2839 = $self->__Set( Field => 'Status', Value => $force_status );
2841 unless ($status_val) {
2842 $RT::Handle->Rollback();
2844 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2846 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2850 # update all the links that point to that old ticket
2851 my $old_links_to = RT::Links->new($self->CurrentUser);
2852 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2855 while (my $link = $old_links_to->Next) {
2856 if (exists $old_seen{$link->Base."-".$link->Type}) {
2859 elsif ($link->Base eq $MergeInto->URI) {
2862 # First, make sure the link doesn't already exist. then move it over.
2863 my $tmp = RT::Link->new(RT->SystemUser);
2864 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2868 $link->SetTarget($MergeInto->URI);
2869 $link->SetLocalTarget($MergeInto->id);
2871 $old_seen{$link->Base."-".$link->Type} =1;
2876 my $old_links_from = RT::Links->new($self->CurrentUser);
2877 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2879 while (my $link = $old_links_from->Next) {
2880 if (exists $old_seen{$link->Type."-".$link->Target}) {
2883 if ($link->Target eq $MergeInto->URI) {
2886 # First, make sure the link doesn't already exist. then move it over.
2887 my $tmp = RT::Link->new(RT->SystemUser);
2888 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2892 $link->SetBase($MergeInto->URI);
2893 $link->SetLocalBase($MergeInto->id);
2894 $old_seen{$link->Type."-".$link->Target} =1;
2900 # Update time fields
2901 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2903 my $mutator = "Set$type";
2904 $MergeInto->$mutator(
2905 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2908 #add all of this ticket's watchers to that ticket.
2909 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2911 my $people = $self->$watcher_type->MembersObj;
2912 my $addwatcher_type = $watcher_type;
2913 $addwatcher_type =~ s/s$//;
2915 while ( my $watcher = $people->Next ) {
2917 my ($val, $msg) = $MergeInto->_AddWatcher(
2918 Type => $addwatcher_type,
2920 PrincipalId => $watcher->MemberId
2923 $RT::Logger->debug($msg);
2929 #find all of the tickets that were merged into this ticket.
2930 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2931 $old_mergees->Limit(
2932 FIELD => 'EffectiveId',
2937 # update their EffectiveId fields to the new ticket's id
2938 while ( my $ticket = $old_mergees->Next() ) {
2939 my ( $val, $msg ) = $ticket->__Set(
2940 Field => 'EffectiveId',
2941 Value => $MergeInto->Id()
2945 #make a new link: this ticket is merged into that other ticket.
2946 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2948 $MergeInto->_SetLastUpdated;
2953 Returns list of tickets' ids that's been merged into this ticket.
2961 return @{ $MERGE_CACHE{'merged'}{ $id } }
2962 if $MERGE_CACHE{'merged'}{ $id };
2964 my $mergees = RT::Tickets->new( $self->CurrentUser );
2966 FIELD => 'EffectiveId',
2974 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2975 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2984 Takes nothing and returns an RT::User object of
2992 #If this gets ACLed, we lose on a rights check in User.pm and
2993 #get deep recursion. if we need ACLs here, we need
2994 #an equiv without ACLs
2996 my $owner = RT::User->new( $self->CurrentUser );
2997 $owner->Load( $self->__Value('Owner') );
2999 #Return the owner object
3005 =head2 OwnerAsString
3007 Returns the owner's email address
3013 return ( $self->OwnerObj->EmailAddress );
3021 Takes two arguments:
3022 the Id or Name of the owner
3023 and (optionally) the type of the SetOwner Transaction. It defaults
3024 to 'Set'. 'Steal' is also a valid option.
3031 my $NewOwner = shift;
3032 my $Type = shift || "Set";
3034 $RT::Handle->BeginTransaction();
3036 $self->_SetLastUpdated(); # lock the ticket
3037 $self->Load( $self->id ); # in case $self changed while waiting for lock
3039 my $OldOwnerObj = $self->OwnerObj;
3041 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3042 $NewOwnerObj->Load( $NewOwner );
3043 unless ( $NewOwnerObj->Id ) {
3044 $RT::Handle->Rollback();
3045 return ( 0, $self->loc("That user does not exist") );
3049 # must have ModifyTicket rights
3050 # or TakeTicket/StealTicket and $NewOwner is self
3051 # see if it's a take
3052 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
3053 unless ( $self->CurrentUserHasRight('ModifyTicket')
3054 || $self->CurrentUserHasRight('TakeTicket') ) {
3055 $RT::Handle->Rollback();
3056 return ( 0, $self->loc("Permission Denied") );
3060 # see if it's a steal
3061 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
3062 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3064 unless ( $self->CurrentUserHasRight('ModifyTicket')
3065 || $self->CurrentUserHasRight('StealTicket') ) {
3066 $RT::Handle->Rollback();
3067 return ( 0, $self->loc("Permission Denied") );
3071 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3072 $RT::Handle->Rollback();
3073 return ( 0, $self->loc("Permission Denied") );
3077 # If we're not stealing and the ticket has an owner and it's not
3079 if ( $Type ne 'Steal' and $Type ne 'Force'
3080 and $OldOwnerObj->Id != RT->Nobody->Id
3081 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3083 $RT::Handle->Rollback();
3084 return ( 0, $self->loc("You can only take tickets that are unowned") )
3085 if $NewOwnerObj->id == $self->CurrentUser->id;
3088 $self->loc("You can only reassign tickets that you own or that are unowned" )
3092 #If we've specified a new owner and that user can't modify the ticket
3093 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3094 $RT::Handle->Rollback();
3095 return ( 0, $self->loc("That user may not own tickets in that queue") );
3098 # If the ticket has an owner and it's the new owner, we don't need
3100 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3101 $RT::Handle->Rollback();
3102 return ( 0, $self->loc("That user already owns that ticket") );
3105 # Delete the owner in the owner group, then add a new one
3106 # TODO: is this safe? it's not how we really want the API to work
3107 # for most things, but it's fast.
3108 my ( $del_id, $del_msg );
3109 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3110 ($del_id, $del_msg) = $owner->Delete();
3111 last unless ($del_id);
3115 $RT::Handle->Rollback();
3116 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3119 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3120 PrincipalId => $NewOwnerObj->PrincipalId,
3121 InsideTransaction => 1 );
3123 $RT::Handle->Rollback();
3124 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3127 # We call set twice with slightly different arguments, so
3128 # as to not have an SQL transaction span two RT transactions
3130 my ( $val, $msg ) = $self->_Set(
3132 RecordTransaction => 0,
3133 Value => $NewOwnerObj->Id,
3135 TransactionType => 'Set',
3136 CheckACL => 0, # don't check acl
3140 $RT::Handle->Rollback;
3141 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3144 ($val, $msg) = $self->_NewTransaction(
3147 NewValue => $NewOwnerObj->Id,
3148 OldValue => $OldOwnerObj->Id,
3153 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3154 $OldOwnerObj->Name, $NewOwnerObj->Name );
3157 $RT::Handle->Rollback();
3161 $RT::Handle->Commit();
3163 return ( $val, $msg );
3170 A convenince method to set the ticket's owner to the current user
3176 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3183 Convenience method to set the owner to 'nobody' if the current user is the owner.
3189 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3196 A convenience method to change the owner of the current ticket to the
3197 current user. Even if it's owned by another user.
3204 if ( $self->IsOwner( $self->CurrentUser ) ) {
3205 return ( 0, $self->loc("You already own this ticket") );
3208 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3218 =head2 ValidateStatus STATUS
3220 Takes a string. Returns true if that status is a valid status for this ticket.
3221 Returns false otherwise.
3225 sub ValidateStatus {
3229 #Make sure the status passed in is valid
3230 return 1 if $self->QueueObj->IsValidStatus($status);
3233 while ( my $caller = (caller($i++))[3] ) {
3234 return 1 if $caller eq 'RT::Ticket::SetQueue';
3242 my $value = $self->_Value( 'Status' );
3243 return $value unless $self->QueueObj;
3244 return $self->QueueObj->Lifecycle->CanonicalCase( $value );
3247 =head2 SetStatus STATUS
3249 Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3251 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3252 If FORCE is true, ignore unresolved dependencies and force a status change.
3253 if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3254 is not set and the status is changed from initial to not initial.
3262 $args{Status} = shift;
3268 # this only allows us to SetStarted, not we must SetStarted.
3269 # this option was added for rtir initially
3270 $args{SetStarted} = 1 unless exists $args{SetStarted};
3273 my $lifecycle = $self->QueueObj->Lifecycle;
3275 my $new = lc $args{'Status'};
3276 unless ( $lifecycle->IsValid( $new ) ) {
3277 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3280 my $old = $self->__Value('Status');
3281 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3282 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3285 my $check_right = $lifecycle->CheckRight( $old => $new );
3286 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3287 return ( 0, $self->loc('Permission Denied') );
3290 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3291 return (0, $self->loc('That ticket has unresolved dependencies'));
3294 my $now = RT::Date->new( $self->CurrentUser );
3297 my $raw_started = RT::Date->new(RT->SystemUser);
3298 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3300 #If we're changing the status from new, record that we've started
3301 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3302 #Set the Started time to "now"
3306 RecordTransaction => 0
3310 #When we close a ticket, set the 'Resolved' attribute to now.
3311 # It's misnamed, but that's just historical.
3312 if ( $lifecycle->IsInactive($new) ) {
3314 Field => 'Resolved',
3316 RecordTransaction => 0,
3320 #Actually update the status
3321 my ($val, $msg)= $self->_Set(
3326 TransactionType => 'Status',
3328 return ($val, $msg);
3335 Takes no arguments. Marks this ticket for garbage collection
3341 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3342 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3344 return ( $self->SetStatus('deleted') );
3348 =head2 SetTold ISO [TIMETAKEN]
3350 Updates the told and records a transaction
3357 $told = shift if (@_);
3358 my $timetaken = shift || 0;
3360 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3361 return ( 0, $self->loc("Permission Denied") );
3364 my $datetold = RT::Date->new( $self->CurrentUser );
3366 $datetold->Set( Format => 'iso',
3370 $datetold->SetToNow();
3373 return ( $self->_Set( Field => 'Told',
3374 Value => $datetold->ISO,
3375 TimeTaken => $timetaken,
3376 TransactionType => 'Told' ) );
3381 Updates the told without a transaction or acl check. Useful when we're sending replies.
3388 my $now = RT::Date->new( $self->CurrentUser );
3391 #use __Set to get no ACLs ;)
3392 return ( $self->__Set( Field => 'Told',
3393 Value => $now->ISO ) );
3403 my $uid = $self->CurrentUser->id;
3404 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3405 return if $attr && $attr->Content gt $self->LastUpdated;
3407 my $txns = $self->Transactions;
3408 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3409 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3410 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3414 VALUE => $attr->Content
3416 $txns->RowsPerPage(1);
3417 return $txns->First;
3420 =head2 RanTransactionBatch
3422 Acts as a guard around running TransactionBatch scrips.
3424 Should be false until you enter the code that runs TransactionBatch scrips
3426 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3430 sub RanTransactionBatch {
3434 if ( defined $val ) {
3435 return $self->{_RanTransactionBatch} = $val;
3437 return $self->{_RanTransactionBatch};
3443 =head2 TransactionBatch
3445 Returns an array reference of all transactions created on this ticket during
3446 this ticket object's lifetime or since last application of a batch, or undef
3449 Only works when the C<UseTransactionBatch> config option is set to true.
3453 sub TransactionBatch {
3455 return $self->{_TransactionBatch};
3458 =head2 ApplyTransactionBatch
3460 Applies scrips on the current batch of transactions and shinks it. Usually
3461 batch is applied when object is destroyed, but in some cases it's too late.
3465 sub ApplyTransactionBatch {
3468 my $batch = $self->TransactionBatch;
3469 return unless $batch && @$batch;
3471 $self->_ApplyTransactionBatch;
3473 $self->{_TransactionBatch} = [];
3476 sub _ApplyTransactionBatch {
3479 return if $self->RanTransactionBatch;
3480 $self->RanTransactionBatch(1);
3482 my $still_exists = RT::Ticket->new( RT->SystemUser );
3483 $still_exists->Load( $self->Id );
3484 if (not $still_exists->Id) {
3485 # The ticket has been removed from the database, but we still
3486 # have pending TransactionBatch txns for it. Unfortunately,
3487 # because it isn't in the DB anymore, attempting to run scrips
3488 # on it may produce unpredictable results; simply drop the
3489 # batched transactions.
3490 $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips! Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
3494 my $batch = $self->TransactionBatch;
3497 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3500 RT::Scrips->new(RT->SystemUser)->Apply(
3501 Stage => 'TransactionBatch',
3503 TransactionObj => $batch->[0],
3507 # Entry point of the rule system
3508 my $rules = RT::Ruleset->FindAllRules(
3509 Stage => 'TransactionBatch',
3511 TransactionObj => $batch->[0],
3514 RT::Ruleset->CommitRules($rules);
3520 # DESTROY methods need to localize $@, or it may unset it. This
3521 # causes $m->abort to not bubble all of the way up. See perlbug
3522 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3525 # The following line eliminates reentrancy.
3526 # It protects against the fact that perl doesn't deal gracefully
3527 # when an object's refcount is changed in its destructor.
3528 return if $self->{_Destroyed}++;
3530 if (in_global_destruction()) {
3531 unless ($ENV{'HARNESS_ACTIVE'}) {
3532 warn "Too late to safely run transaction-batch scrips!"
3533 ." This is typically caused by using ticket objects"
3534 ." at the top-level of a script which uses the RT API."
3535 ." Be sure to explicitly undef such ticket objects,"
3536 ." or put them inside of a lexical scope.";
3541 return $self->ApplyTransactionBatch;
3547 sub _OverlayAccessible {
3549 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3550 Queue => { 'read' => 1, 'write' => 1 },
3551 Requestors => { 'read' => 1, 'write' => 1 },
3552 Owner => { 'read' => 1, 'write' => 1 },
3553 Subject => { 'read' => 1, 'write' => 1 },
3554 InitialPriority => { 'read' => 1, 'write' => 1 },
3555 FinalPriority => { 'read' => 1, 'write' => 1 },
3556 Priority => { 'read' => 1, 'write' => 1 },
3557 Status => { 'read' => 1, 'write' => 1 },
3558 TimeEstimated => { 'read' => 1, 'write' => 1 },
3559 TimeWorked => { 'read' => 1, 'write' => 1 },
3560 TimeLeft => { 'read' => 1, 'write' => 1 },
3561 Told => { 'read' => 1, 'write' => 1 },
3562 Resolved => { 'read' => 1 },
3563 Type => { 'read' => 1 },
3564 Starts => { 'read' => 1, 'write' => 1 },
3565 Started => { 'read' => 1, 'write' => 1 },
3566 Due => { 'read' => 1, 'write' => 1 },
3567 Creator => { 'read' => 1, 'auto' => 1 },
3568 Created => { 'read' => 1, 'auto' => 1 },
3569 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3570 LastUpdated => { 'read' => 1, 'auto' => 1 }
3580 my %args = ( Field => undef,
3583 RecordTransaction => 1,
3586 TransactionType => 'Set',
3589 if ($args{'CheckACL'}) {
3590 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3591 return ( 0, $self->loc("Permission Denied"));
3595 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3596 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3597 return(0, $self->loc("Internal Error"));
3600 #if the user is trying to modify the record
3602 #Take care of the old value we really don't want to get in an ACL loop.
3603 # so ask the super::_Value
3604 my $Old = $self->SUPER::_Value("$args{'Field'}");
3607 if ( $args{'UpdateTicket'} ) {
3610 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3611 Value => $args{'Value'} );
3613 #If we can't actually set the field to the value, don't record
3614 # a transaction. instead, get out of here.
3615 return ( 0, $msg ) unless $ret;
3618 if ( $args{'RecordTransaction'} == 1 ) {
3620 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3621 Type => $args{'TransactionType'},
3622 Field => $args{'Field'},
3623 NewValue => $args{'Value'},
3625 TimeTaken => $args{'TimeTaken'},
3627 # Ensure that we can read the transaction, even if the change
3628 # just made the ticket unreadable to us
3629 $TransObj->{ _object_is_readable } = 1;
3630 return ( $Trans, scalar $TransObj->BriefDescription );
3633 return ( $ret, $msg );
3641 Takes the name of a table column.
3642 Returns its value as a string, if the user passes an ACL check
3651 #if the field is public, return it.
3652 if ( $self->_Accessible( $field, 'public' ) ) {
3654 #$RT::Logger->debug("Skipping ACL check for $field");
3655 return ( $self->SUPER::_Value($field) );
3659 #If the current user doesn't have ACLs, don't let em at it.
3661 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3664 return ( $self->SUPER::_Value($field) );
3670 =head2 _UpdateTimeTaken
3672 This routine will increment the timeworked counter. it should
3673 only be called from _NewTransaction
3677 sub _UpdateTimeTaken {
3679 my $Minutes = shift;
3682 $Total = $self->SUPER::_Value("TimeWorked");
3683 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3685 Field => "TimeWorked",
3696 =head2 CurrentUserHasRight
3698 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3699 1 if the user has that right. It returns 0 if the user doesn't have that right.
3703 sub CurrentUserHasRight {
3707 return $self->CurrentUser->PrincipalObj->HasRight(
3714 =head2 CurrentUserCanSee
3716 Returns true if the current user can see the ticket, using ShowTicket
3720 sub CurrentUserCanSee {
3722 return $self->CurrentUserHasRight('ShowTicket');
3727 Takes a paramhash with the attributes 'Right' and 'Principal'
3728 'Right' is a ticket-scoped textual right from RT::ACE
3729 'Principal' is an RT::User object
3731 Returns 1 if the principal has the right. Returns undef if not.
3743 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3745 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3746 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3751 $args{'Principal'}->HasRight(
3753 Right => $args{'Right'}
3762 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3763 It isn't acutally a searchbuilder collection itself.
3770 unless ($self->{'__reminders'}) {
3771 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3772 $self->{'__reminders'}->Ticket($self->id);
3774 return $self->{'__reminders'};
3783 Returns an RT::Transactions object of all transactions on this ticket
3790 my $transactions = RT::Transactions->new( $self->CurrentUser );
3792 #If the user has no rights, return an empty object
3793 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3794 $transactions->LimitToTicket($self->id);
3796 # if the user may not see comments do not return them
3797 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3798 $transactions->Limit(
3804 $transactions->Limit(
3808 VALUE => "CommentEmailRecord",
3809 ENTRYAGGREGATOR => 'AND'
3814 $transactions->Limit(
3818 ENTRYAGGREGATOR => 'AND'
3822 return ($transactions);
3828 =head2 TransactionCustomFields
3830 Returns the custom fields that transactions on tickets will have.
3834 sub TransactionCustomFields {
3836 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3837 $cfs->SetContextObject( $self );
3842 =head2 LoadCustomFieldByIdentifier
3844 Finds and returns the custom field of the given name for the ticket,
3845 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3846 queue-specific CFs before global ones.
3850 sub LoadCustomFieldByIdentifier {
3854 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3855 if ref $field or $field =~ /^\d+$/;
3857 my $cf = RT::CustomField->new( $self->CurrentUser );
3858 $cf->SetContextObject( $self );
3859 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3860 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
3865 =head2 CustomFieldLookupType
3867 Returns the RT::Ticket lookup type, which can be passed to
3868 RT::CustomField->Create() via the 'LookupType' hash key.
3873 sub CustomFieldLookupType {
3874 "RT::Queue-RT::Ticket";
3877 =head2 ACLEquivalenceObjects
3879 This method returns a list of objects for which a user's rights also apply
3880 to this ticket. Generally, this is only the ticket's queue, but some RT
3881 extensions may make other objects available too.
3883 This method is called from L<RT::Principal/HasRight>.
3887 sub ACLEquivalenceObjects {
3889 return $self->QueueObj;
3898 Jesse Vincent, jesse@bestpractical.com
3908 use base 'RT::Record';
3910 sub Table {'Tickets'}
3919 Returns the current value of id.
3920 (In the database, id is stored as int(11).)
3928 Returns the current value of EffectiveId.
3929 (In the database, EffectiveId is stored as int(11).)
3933 =head2 SetEffectiveId VALUE
3936 Set EffectiveId to VALUE.
3937 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3938 (In the database, EffectiveId will be stored as a int(11).)
3946 Returns the current value of Queue.
3947 (In the database, Queue is stored as int(11).)
3951 =head2 SetQueue VALUE
3955 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3956 (In the database, Queue will be stored as a int(11).)
3964 Returns the current value of Type.
3965 (In the database, Type is stored as varchar(16).)
3969 =head2 SetType VALUE
3973 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3974 (In the database, Type will be stored as a varchar(16).)
3980 =head2 IssueStatement
3982 Returns the current value of IssueStatement.
3983 (In the database, IssueStatement is stored as int(11).)
3987 =head2 SetIssueStatement VALUE
3990 Set IssueStatement to VALUE.
3991 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3992 (In the database, IssueStatement will be stored as a int(11).)
4000 Returns the current value of Resolution.
4001 (In the database, Resolution is stored as int(11).)
4005 =head2 SetResolution VALUE
4008 Set Resolution to VALUE.
4009 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4010 (In the database, Resolution will be stored as a int(11).)
4018 Returns the current value of Owner.
4019 (In the database, Owner is stored as int(11).)
4023 =head2 SetOwner VALUE
4027 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4028 (In the database, Owner will be stored as a int(11).)
4036 Returns the current value of Subject.
4037 (In the database, Subject is stored as varchar(200).)
4041 =head2 SetSubject VALUE
4044 Set Subject to VALUE.
4045 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4046 (In the database, Subject will be stored as a varchar(200).)
4052 =head2 InitialPriority
4054 Returns the current value of InitialPriority.
4055 (In the database, InitialPriority is stored as int(11).)
4059 =head2 SetInitialPriority VALUE
4062 Set InitialPriority to VALUE.
4063 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4064 (In the database, InitialPriority will be stored as a int(11).)
4070 =head2 FinalPriority
4072 Returns the current value of FinalPriority.
4073 (In the database, FinalPriority is stored as int(11).)
4077 =head2 SetFinalPriority VALUE
4080 Set FinalPriority to VALUE.
4081 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4082 (In the database, FinalPriority will be stored as a int(11).)
4090 Returns the current value of Priority.
4091 (In the database, Priority is stored as int(11).)
4095 =head2 SetPriority VALUE
4098 Set Priority to VALUE.
4099 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4100 (In the database, Priority will be stored as a int(11).)
4106 =head2 TimeEstimated
4108 Returns the current value of TimeEstimated.
4109 (In the database, TimeEstimated is stored as int(11).)
4113 =head2 SetTimeEstimated VALUE
4116 Set TimeEstimated to VALUE.
4117 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4118 (In the database, TimeEstimated will be stored as a int(11).)
4126 Returns the current value of TimeWorked.
4127 (In the database, TimeWorked is stored as int(11).)
4131 =head2 SetTimeWorked VALUE
4134 Set TimeWorked to VALUE.
4135 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4136 (In the database, TimeWorked will be stored as a int(11).)
4144 Returns the current value of Status.
4145 (In the database, Status is stored as varchar(64).)
4149 =head2 SetStatus VALUE
4152 Set Status to VALUE.
4153 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4154 (In the database, Status will be stored as a varchar(64).)
4162 Returns the current value of TimeLeft.
4163 (In the database, TimeLeft is stored as int(11).)
4167 =head2 SetTimeLeft VALUE
4170 Set TimeLeft to VALUE.
4171 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4172 (In the database, TimeLeft will be stored as a int(11).)
4180 Returns the current value of Told.
4181 (In the database, Told is stored as datetime.)
4185 =head2 SetTold VALUE
4189 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4190 (In the database, Told will be stored as a datetime.)
4198 Returns the current value of Starts.
4199 (In the database, Starts is stored as datetime.)
4203 =head2 SetStarts VALUE
4206 Set Starts to VALUE.
4207 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4208 (In the database, Starts will be stored as a datetime.)
4216 Returns the current value of Started.
4217 (In the database, Started is stored as datetime.)
4221 =head2 SetStarted VALUE
4224 Set Started to VALUE.
4225 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4226 (In the database, Started will be stored as a datetime.)
4234 Returns the current value of Due.
4235 (In the database, Due is stored as datetime.)
4243 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4244 (In the database, Due will be stored as a datetime.)
4252 Returns the current value of Resolved.
4253 (In the database, Resolved is stored as datetime.)
4257 =head2 SetResolved VALUE
4260 Set Resolved to VALUE.
4261 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4262 (In the database, Resolved will be stored as a datetime.)
4268 =head2 LastUpdatedBy
4270 Returns the current value of LastUpdatedBy.
4271 (In the database, LastUpdatedBy is stored as int(11).)
4279 Returns the current value of LastUpdated.
4280 (In the database, LastUpdated is stored as datetime.)
4288 Returns the current value of Creator.
4289 (In the database, Creator is stored as int(11).)
4297 Returns the current value of Created.
4298 (In the database, Created is stored as datetime.)
4306 Returns the current value of Disabled.
4307 (In the database, Disabled is stored as smallint(6).)
4311 =head2 SetDisabled VALUE
4314 Set Disabled to VALUE.
4315 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4316 (In the database, Disabled will be stored as a smallint(6).)
4323 sub _CoreAccessible {
4327 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
4329 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4331 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4333 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
4335 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4337 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4339 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4341 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
4343 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4345 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4347 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4349 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4351 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4353 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
4355 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4357 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4359 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4361 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4363 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4365 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4367 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4369 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4371 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4373 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4375 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
4380 RT::Base->_ImportOverlays();