1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
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 (Defined in RT::Queue)
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,
259 _RecordTransaction => 1,
264 my ($ErrStr, @non_fatal_errors);
266 my $QueueObj = RT::Queue->new( RT->SystemUser );
267 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
268 $QueueObj->Load( $args{'Queue'}->Id );
270 elsif ( $args{'Queue'} ) {
271 $QueueObj->Load( $args{'Queue'} );
274 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
277 #Can't create a ticket without a queue.
278 unless ( $QueueObj->Id ) {
279 $RT::Logger->debug("$self No queue given for ticket creation.");
280 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
284 #Now that we have a queue, Check the ACLS
286 $self->CurrentUser->HasRight(
287 Right => 'CreateTicket',
294 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
297 my $cycle = $QueueObj->Lifecycle;
298 unless ( defined $args{'Status'} && length $args{'Status'} ) {
299 $args{'Status'} = $cycle->DefaultOnCreate;
302 $args{'Status'} = lc $args{'Status'};
303 unless ( $cycle->IsValid( $args{'Status'} ) ) {
305 $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
306 $self->loc($args{'Status'}))
310 unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
312 $self->loc("New tickets can not have status '[_1]' in this queue.",
313 $self->loc($args{'Status'}))
319 #Since we have a queue, we can set queue defaults
322 # If there's no queue default initial priority and it's not set, set it to 0
323 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
324 unless defined $args{'InitialPriority'};
327 # If there's no queue default final priority and it's not set, set it to 0
328 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
329 unless defined $args{'FinalPriority'};
331 # Priority may have changed from InitialPriority, for the case
332 # where we're importing tickets (eg, from an older RT version.)
333 $args{'Priority'} = $args{'InitialPriority'}
334 unless defined $args{'Priority'};
337 #TODO we should see what sort of due date we're getting, rather +
338 # than assuming it's in ISO format.
340 #Set the due date. if we didn't get fed one, use the queue default due in
341 my $Due = RT::Date->new( $self->CurrentUser );
342 if ( defined $args{'Due'} ) {
343 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
345 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
347 $Due->AddDays( $due_in );
350 my $Starts = RT::Date->new( $self->CurrentUser );
351 if ( defined $args{'Starts'} ) {
352 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
355 my $Started = RT::Date->new( $self->CurrentUser );
356 if ( defined $args{'Started'} ) {
357 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
360 # If the status is not an initial status, set the started date
361 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
365 my $Resolved = RT::Date->new( $self->CurrentUser );
366 if ( defined $args{'Resolved'} ) {
367 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
370 #If the status is an inactive status, set the resolved date
371 elsif ( $cycle->IsInactive( $args{'Status'} ) )
373 $RT::Logger->debug( "Got a ". $args{'Status'}
374 ."(inactive) ticket with undefined resolved date. Setting to now."
381 # Dealing with time fields
383 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
384 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
385 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
389 # Deal with setting the owner
392 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
393 if ( $args{'Owner'}->id ) {
394 $Owner = $args{'Owner'};
396 $RT::Logger->error('Passed an empty RT::User for owner');
397 push @non_fatal_errors,
398 $self->loc("Owner could not be set.") . " ".
399 $self->loc("Invalid value for [_1]",loc('owner'));
404 #If we've been handed something else, try to load the user.
405 elsif ( $args{'Owner'} ) {
406 $Owner = RT::User->new( $self->CurrentUser );
407 $Owner->Load( $args{'Owner'} );
409 $Owner->LoadByEmail( $args{'Owner'} )
411 unless ( $Owner->Id ) {
412 push @non_fatal_errors,
413 $self->loc("Owner could not be set.") . " "
414 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
419 #If we have a proposed owner and they don't have the right
420 #to own a ticket, scream about it and make them not the owner
423 if ( $Owner && $Owner->Id != RT->Nobody->Id
424 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
426 $DeferOwner = $Owner;
428 $RT::Logger->debug('going to deffer setting owner');
432 #If we haven't been handed a valid owner, make it nobody.
433 unless ( defined($Owner) && $Owner->Id ) {
434 $Owner = RT::User->new( $self->CurrentUser );
435 $Owner->Load( RT->Nobody->Id );
440 # We attempt to load or create each of the people who might have a role for this ticket
441 # _outside_ the transaction, so we don't get into ticket creation races
442 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
443 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
444 foreach my $watcher ( splice @{ $args{$type} } ) {
445 next unless $watcher;
446 if ( $watcher =~ /^\d+$/ ) {
447 push @{ $args{$type} }, $watcher;
449 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
450 foreach my $address( @addresses ) {
451 my $user = RT::User->new( RT->SystemUser );
452 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
454 push @non_fatal_errors,
455 $self->loc("Couldn't load or create user: [_1]", $msg);
457 push @{ $args{$type} }, $user->id;
464 $args{'Type'} = lc $args{'Type'}
465 if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
467 $args{'Subject'} =~ s/\n//g;
469 $RT::Handle->BeginTransaction();
472 Queue => $QueueObj->Id,
474 Subject => $args{'Subject'},
475 InitialPriority => $args{'InitialPriority'},
476 FinalPriority => $args{'FinalPriority'},
477 Priority => $args{'Priority'},
478 Status => $args{'Status'},
479 TimeWorked => $args{'TimeWorked'},
480 TimeEstimated => $args{'TimeEstimated'},
481 TimeLeft => $args{'TimeLeft'},
482 Type => $args{'Type'},
483 Starts => $Starts->ISO,
484 Started => $Started->ISO,
485 Resolved => $Resolved->ISO,
489 # Parameters passed in during an import that we probably don't want to touch, otherwise
490 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
491 $params{$attr} = $args{$attr} if $args{$attr};
494 # Delete null integer parameters
496 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
498 delete $params{$attr}
499 unless ( exists $params{$attr} && $params{$attr} );
502 # Delete the time worked if we're counting it in the transaction
503 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
505 my ($id,$ticket_message) = $self->SUPER::Create( %params );
507 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
508 $RT::Handle->Rollback();
510 $self->loc("Ticket could not be created due to an internal error")
514 #Set the ticket's effective ID now that we've created it.
515 my ( $val, $msg ) = $self->__Set(
516 Field => 'EffectiveId',
517 Value => ( $args{'EffectiveId'} || $id )
520 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
521 $RT::Handle->Rollback;
523 $self->loc("Ticket could not be created due to an internal error")
527 my $create_groups_ret = $self->_CreateTicketGroups();
528 unless ($create_groups_ret) {
529 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
531 . ". aborting Ticket creation." );
532 $RT::Handle->Rollback();
534 $self->loc("Ticket could not be created due to an internal error")
538 # Set the owner in the Groups table
539 # We denormalize it into the Ticket table too because doing otherwise would
540 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
541 $self->OwnerGroup->_AddMember(
542 PrincipalId => $Owner->PrincipalId,
543 InsideTransaction => 1
544 ) unless $DeferOwner;
548 # Deal with setting up watchers
550 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
551 # we know it's an array ref
552 foreach my $watcher ( @{ $args{$type} } ) {
554 # Note that we're using AddWatcher, rather than _AddWatcher, as we
555 # actually _want_ that ACL check. Otherwise, random ticket creators
556 # could make themselves adminccs and maybe get ticket rights. that would
558 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
560 my ($val, $msg) = $self->$method(
562 PrincipalId => $watcher,
565 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
570 if ($args{'SquelchMailTo'}) {
571 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
572 : $args{'SquelchMailTo'};
573 $self->_SquelchMailTo( @squelch );
579 # Add all the custom fields
581 foreach my $arg ( keys %args ) {
582 next unless $arg =~ /^CustomField-(\d+)$/i;
586 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
588 next unless defined $value && length $value;
590 # Allow passing in uploaded LargeContent etc by hash reference
591 my ($status, $msg) = $self->_AddCustomFieldValue(
592 (UNIVERSAL::isa( $value => 'HASH' )
597 RecordTransaction => 0,
599 push @non_fatal_errors, $msg unless $status;
605 # Deal with setting up links
607 # TODO: Adding link may fire scrips on other end and those scrips
608 # could create transactions on this ticket before 'Create' transaction.
610 # We should implement different lifecycle: record 'Create' transaction,
611 # create links and only then fire create transaction's scrips.
613 # Ideal variant: add all links without firing scrips, record create
614 # transaction and only then fire scrips on the other ends of links.
618 foreach my $type ( keys %LINKTYPEMAP ) {
619 next unless ( defined $args{$type} );
621 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
623 my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
625 push @non_fatal_errors, $msg;
629 # Check rights on the other end of the link if we must
630 # then run _AddLink that doesn't check for ACLs
631 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
632 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
633 push @non_fatal_errors, $self->loc('Linking. Permission denied');
638 if ( $obj && $obj->Status eq 'deleted' ) {
639 push @non_fatal_errors,
640 $self->loc("Linking. Can't link to a deleted ticket");
644 my ( $wval, $wmsg ) = $self->_AddLink(
645 Type => $LINKTYPEMAP{$type}->{'Type'},
646 $LINKTYPEMAP{$type}->{'Mode'} => $link,
647 Silent => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
648 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
652 push @non_fatal_errors, $wmsg unless ($wval);
658 # {{{ Deal with auto-customer association
660 #unless we already have (a) customer(s)...
661 unless ( $self->Customers->Count ) {
663 #first find any requestors with emails but *without* customer targets
664 my @NoCust_Requestors =
665 grep { $_->EmailAddress && ! $_->Customers->Count }
666 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
668 for my $Requestor (@NoCust_Requestors) {
670 #perhaps the stuff in here should be in a User method??
672 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
674 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
676 ## false laziness w/RT/Interface/Web_Vendor.pm
677 my @link = ( 'Type' => 'MemberOf',
678 'Target' => "freeside://freeside/cust_main/$custnum",
681 my( $val, $msg ) = $Requestor->_AddLink(@link);
682 #XXX should do something with $msg# push @non_fatal_errors, $msg;
688 #find any requestors with customer targets
690 my %cust_target = ();
693 grep { $_->Customers->Count }
694 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
696 foreach my $Requestor ( @Requestors ) {
697 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
698 $cust_target{ $cust_link->Target } = 1;
702 #and then auto-associate this ticket with those customers
704 foreach my $cust_target ( keys %cust_target ) {
706 my @link = ( 'Type' => 'MemberOf',
707 #'Target' => "freeside://freeside/cust_main/$custnum",
708 'Target' => $cust_target,
711 my( $val, $msg ) = $self->_AddLink(@link);
712 push @non_fatal_errors, $msg;
720 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
721 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
723 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
725 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
726 . ") was proposed as a ticket owner but has no rights to own "
727 . "tickets in " . $QueueObj->Name );
728 push @non_fatal_errors, $self->loc(
729 "Owner '[_1]' does not have rights to own this ticket.",
733 $Owner = $DeferOwner;
734 $self->__Set(Field => 'Owner', Value => $Owner->id);
737 $self->OwnerGroup->_AddMember(
738 PrincipalId => $Owner->PrincipalId,
739 InsideTransaction => 1
743 #don't make a transaction or fire off any scrips for reminders either
744 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
746 # Add a transaction for the create
747 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
749 TimeTaken => $args{'TimeWorked'},
750 MIMEObj => $args{'MIMEObj'},
751 CommitScrips => !$args{'DryRun'},
752 SquelchMailTo => $args{'TransSquelchMailTo'},
755 if ( $self->Id && $Trans ) {
757 #$TransObj->UpdateCustomFields(ARGSRef => \%args);
758 $TransObj->UpdateCustomFields(%args);
760 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
761 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
762 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
765 $RT::Handle->Rollback();
767 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
768 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
769 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
772 if ( $args{'DryRun'} ) {
773 $RT::Handle->Rollback();
774 return ($self->id, $TransObj, $ErrStr);
776 $RT::Handle->Commit();
777 return ( $self->Id, $TransObj->Id, $ErrStr );
783 # Not going to record a transaction
784 $RT::Handle->Commit();
785 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
786 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
787 return ( $self->Id, 0, $ErrStr );
796 # Force lowercase on internal RT types
798 if $value =~ /^(ticket|approval|reminder)$/i;
799 return $self->_Set(Field => 'Type', Value => $value, @_);
804 =head2 _Parse822HeadersForAttributes Content
806 Takes an RFC822 style message and parses its attributes into a hash.
810 sub _Parse822HeadersForAttributes {
815 my @lines = ( split ( /\n/, $content ) );
816 while ( defined( my $line = shift @lines ) ) {
817 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
822 if ( defined( $args{$tag} ) )
823 { #if we're about to get a second value, make it an array
824 $args{$tag} = [ $args{$tag} ];
826 if ( ref( $args{$tag} ) )
827 { #If it's an array, we want to push the value
828 push @{ $args{$tag} }, $value;
830 else { #if there's nothing there, just set the value
831 $args{$tag} = $value;
833 } elsif ($line =~ /^$/) {
835 #TODO: this won't work, since "" isn't of the form "foo:value"
837 while ( defined( my $l = shift @lines ) ) {
838 push @{ $args{'content'} }, $l;
844 foreach my $date (qw(due starts started resolved)) {
845 my $dateobj = RT::Date->new(RT->SystemUser);
846 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
847 $dateobj->Set( Format => 'unix', Value => $args{$date} );
850 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
852 $args{$date} = $dateobj->ISO;
854 $args{'mimeobj'} = MIME::Entity->new();
855 $args{'mimeobj'}->build(
856 Type => ( $args{'contenttype'} || 'text/plain' ),
857 Data => ($args{'content'} || '')
865 =head2 Import PARAMHASH
868 Doesn't create a transaction.
869 Doesn't supply queue defaults, etc.
877 my ( $ErrStr, $QueueObj, $Owner );
881 EffectiveId => undef,
885 Owner => RT->Nobody->Id,
886 Subject => '[no subject]',
887 InitialPriority => undef,
888 FinalPriority => undef,
899 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
900 $QueueObj = RT::Queue->new(RT->SystemUser);
901 $QueueObj->Load( $args{'Queue'} );
903 #TODO error check this and return 0 if it's not loading properly +++
905 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
906 $QueueObj = RT::Queue->new(RT->SystemUser);
907 $QueueObj->Load( $args{'Queue'}->Id );
911 "$self " . $args{'Queue'} . " not a recognised queue object." );
914 #Can't create a ticket without a queue.
915 unless ( defined($QueueObj) and $QueueObj->Id ) {
916 $RT::Logger->debug("$self No queue given for ticket creation.");
917 return ( 0, $self->loc('Could not create ticket. Queue not set') );
920 #Now that we have a queue, Check the ACLS
922 $self->CurrentUser->HasRight(
923 Right => 'CreateTicket',
929 $self->loc("No permission to create tickets in the queue '[_1]'"
933 # Deal with setting the owner
935 # Attempt to take user object, user name or user id.
936 # Assign to nobody if lookup fails.
937 if ( defined( $args{'Owner'} ) ) {
938 if ( ref( $args{'Owner'} ) ) {
939 $Owner = $args{'Owner'};
942 $Owner = RT::User->new( $self->CurrentUser );
943 $Owner->Load( $args{'Owner'} );
944 if ( !defined( $Owner->id ) ) {
945 $Owner->Load( RT->Nobody->id );
950 #If we have a proposed owner and they don't have the right
951 #to own a ticket, scream about it and make them not the owner
954 and ( $Owner->Id != RT->Nobody->Id )
964 $RT::Logger->warning( "$self user "
968 . "as a ticket owner but has no rights to own "
970 . $QueueObj->Name . "'" );
975 #If we haven't been handed a valid owner, make it nobody.
976 unless ( defined($Owner) ) {
977 $Owner = RT::User->new( $self->CurrentUser );
978 $Owner->Load( RT->Nobody->UserObj->Id );
983 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
984 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
987 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
988 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
989 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
990 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
992 # If we're coming in with an id, set that now.
993 my $EffectiveId = undef;
995 $EffectiveId = $args{'id'};
999 my $id = $self->SUPER::Create(
1001 EffectiveId => $EffectiveId,
1002 Queue => $QueueObj->Id,
1003 Owner => $Owner->Id,
1004 Subject => $args{'Subject'}, # loc
1005 InitialPriority => $args{'InitialPriority'}, # loc
1006 FinalPriority => $args{'FinalPriority'}, # loc
1007 Priority => $args{'InitialPriority'}, # loc
1008 Status => $args{'Status'}, # loc
1009 TimeWorked => $args{'TimeWorked'}, # loc
1010 Type => $args{'Type'}, # loc
1011 Created => $args{'Created'}, # loc
1012 Told => $args{'Told'}, # loc
1013 LastUpdated => $args{'Updated'}, # loc
1014 Resolved => $args{'Resolved'}, # loc
1015 Due => $args{'Due'}, # loc
1018 # If the ticket didn't have an id
1019 # Set the ticket's effective ID now that we've created it.
1020 if ( $args{'id'} ) {
1021 $self->Load( $args{'id'} );
1025 $self->__Set( Field => 'EffectiveId', Value => $id );
1029 $self . "->Import couldn't set EffectiveId: $msg" );
1033 my $create_groups_ret = $self->_CreateTicketGroups();
1034 unless ($create_groups_ret) {
1036 "Couldn't create ticket groups for ticket " . $self->Id );
1039 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1041 foreach my $watcher ( @{ $args{'Cc'} } ) {
1042 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1044 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
1045 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1048 foreach my $watcher ( @{ $args{'Requestor'} } ) {
1049 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1053 return ( $self->Id, $ErrStr );
1059 =head2 _CreateTicketGroups
1061 Create the ticket groups and links for this ticket.
1062 This routine expects to be called from Ticket->Create _inside of a transaction_
1064 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1066 It will return true on success and undef on failure.
1072 sub _CreateTicketGroups {
1075 my @types = (qw(Requestor Owner Cc AdminCc));
1077 foreach my $type (@types) {
1078 my $type_obj = RT::Group->new($self->CurrentUser);
1079 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1080 Instance => $self->Id,
1083 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1084 $self->Id.": ".$msg);
1096 A constructor which returns an RT::Group object containing the owner of this ticket.
1102 my $owner_obj = RT::Group->new($self->CurrentUser);
1103 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1104 return ($owner_obj);
1112 AddWatcher takes a parameter hash. The keys are as follows:
1114 Type One of Requestor, Cc, AdminCc
1116 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1118 Email The email address of the new watcher. If a user with this
1119 email address can't be found, a new nonprivileged user will be created.
1121 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.
1129 PrincipalId => undef,
1134 # ModifyTicket works in any case
1135 return $self->_AddWatcher( %args )
1136 if $self->CurrentUserHasRight('ModifyTicket');
1137 if ( $args{'Email'} ) {
1138 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1139 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1142 if ( lc $self->CurrentUser->EmailAddress
1143 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1145 $args{'PrincipalId'} = $self->CurrentUser->id;
1146 delete $args{'Email'};
1150 # If the watcher isn't the current user then the current user has no right
1152 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1153 return ( 0, $self->loc("Permission Denied") );
1156 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1157 if ( $args{'Type'} eq 'AdminCc' ) {
1158 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1159 return ( 0, $self->loc('Permission Denied') );
1163 # If it's a Requestor or Cc and they don't have 'Watch', bail
1164 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1165 unless ( $self->CurrentUserHasRight('Watch') ) {
1166 return ( 0, $self->loc('Permission Denied') );
1170 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1171 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1174 return $self->_AddWatcher( %args );
1177 #This contains the meat of AddWatcher. but can be called from a routine like
1178 # Create, which doesn't need the additional acl check
1184 PrincipalId => undef,
1190 my $principal = RT::Principal->new($self->CurrentUser);
1191 if ($args{'Email'}) {
1192 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1193 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'})));
1195 my $user = RT::User->new(RT->SystemUser);
1196 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1197 $args{'PrincipalId'} = $pid if $pid;
1199 if ($args{'PrincipalId'}) {
1200 $principal->Load($args{'PrincipalId'});
1201 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1202 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'})))
1203 if RT::EmailParser->IsRTAddress( $email );
1209 # If we can't find this watcher, we need to bail.
1210 unless ($principal->Id) {
1211 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1212 return(0, $self->loc("Could not find or create that user"));
1216 my $group = RT::Group->new($self->CurrentUser);
1217 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1218 unless ($group->id) {
1219 return(0,$self->loc("Group not found"));
1222 if ( $group->HasMember( $principal)) {
1224 return ( 0, $self->loc('[_1] is already a [_2] for this ticket',
1225 $principal->Object->Name, $self->loc($args{'Type'})) );
1229 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1230 InsideTransaction => 1 );
1232 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1234 return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
1235 $principal->Object->Name, $self->loc($args{'Type'})) );
1238 unless ( $args{'Silent'} ) {
1239 $self->_NewTransaction(
1240 Type => 'AddWatcher',
1241 NewValue => $principal->Id,
1242 Field => $args{'Type'}
1246 return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
1247 $principal->Object->Name, $self->loc($args{'Type'})) );
1253 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1256 Deletes a Ticket watcher. Takes two arguments:
1258 Type (one of Requestor,Cc,AdminCc)
1262 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1264 Email (the email address of an existing wathcer)
1273 my %args = ( Type => undef,
1274 PrincipalId => undef,
1278 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1279 return ( 0, $self->loc("No principal specified") );
1281 my $principal = RT::Principal->new( $self->CurrentUser );
1282 if ( $args{'PrincipalId'} ) {
1284 $principal->Load( $args{'PrincipalId'} );
1287 my $user = RT::User->new( $self->CurrentUser );
1288 $user->LoadByEmail( $args{'Email'} );
1289 $principal->Load( $user->Id );
1292 # If we can't find this watcher, we need to bail.
1293 unless ( $principal->Id ) {
1294 return ( 0, $self->loc("Could not find that principal") );
1297 my $group = RT::Group->new( $self->CurrentUser );
1298 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1299 unless ( $group->id ) {
1300 return ( 0, $self->loc("Group not found") );
1304 #If the watcher we're trying to add is for the current user
1305 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1307 # If it's an AdminCc and they don't have
1308 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1309 if ( $args{'Type'} eq 'AdminCc' ) {
1310 unless ( $self->CurrentUserHasRight('ModifyTicket')
1311 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1312 return ( 0, $self->loc('Permission Denied') );
1316 # If it's a Requestor or Cc and they don't have
1317 # 'Watch' or 'ModifyTicket', bail
1318 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1320 unless ( $self->CurrentUserHasRight('ModifyTicket')
1321 or $self->CurrentUserHasRight('Watch') ) {
1322 return ( 0, $self->loc('Permission Denied') );
1326 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1328 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1332 # If the watcher isn't the current user
1333 # and the current user doesn't have 'ModifyTicket' bail
1335 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1336 return ( 0, $self->loc("Permission Denied") );
1342 # see if this user is already a watcher.
1344 unless ( $group->HasMember($principal) ) {
1346 $self->loc( '[_1] is not a [_2] for this ticket',
1347 $principal->Object->Name, $args{'Type'} ) );
1350 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1352 $RT::Logger->error( "Failed to delete "
1354 . " as a member of group "
1360 'Could not remove [_1] as a [_2] for this ticket',
1361 $principal->Object->Name, $args{'Type'} ) );
1364 unless ( $args{'Silent'} ) {
1365 $self->_NewTransaction( Type => 'DelWatcher',
1366 OldValue => $principal->Id,
1367 Field => $args{'Type'} );
1371 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1372 $principal->Object->Name,
1380 =head2 SquelchMailTo [EMAIL]
1382 Takes an optional email address to never email about updates to this ticket.
1385 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1393 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1397 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1402 return $self->_SquelchMailTo(@_);
1405 sub _SquelchMailTo {
1409 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1410 unless grep { $_->Content eq $attr }
1411 $self->Attributes->Named('SquelchMailTo');
1413 my @attributes = $self->Attributes->Named('SquelchMailTo');
1414 return (@attributes);
1418 =head2 UnsquelchMailTo ADDRESS
1420 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1422 Returns a tuple of (status, message)
1426 sub UnsquelchMailTo {
1429 my $address = shift;
1430 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1431 return ( 0, $self->loc("Permission Denied") );
1434 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1435 return ($val, $msg);
1440 =head2 RequestorAddresses
1442 B<Returns> String: All Ticket Requestor email addresses as a string.
1446 sub RequestorAddresses {
1449 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1453 return ( $self->Requestors->MemberEmailAddressesAsString );
1457 =head2 AdminCcAddresses
1459 returns String: All Ticket AdminCc email addresses as a string
1463 sub AdminCcAddresses {
1466 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1470 return ( $self->AdminCc->MemberEmailAddressesAsString )
1476 returns String: All Ticket Ccs as a string of email addresses
1483 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1486 return ( $self->Cc->MemberEmailAddressesAsString);
1496 Returns this ticket's Requestors as an RT::Group object
1503 my $group = RT::Group->new($self->CurrentUser);
1504 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1505 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1513 Private non-ACLed variant of Reqeustors so that we can look them up for the
1514 purposes of customer auto-association during create.
1521 my $group = RT::Group->new($RT::SystemUser);
1522 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1529 Returns an RT::Group object which contains this ticket's Ccs.
1530 If the user doesn't have "ShowTicket" permission, returns an empty group
1537 my $group = RT::Group->new($self->CurrentUser);
1538 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1539 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1550 Returns an RT::Group object which contains this ticket's AdminCcs.
1551 If the user doesn't have "ShowTicket" permission, returns an empty group
1558 my $group = RT::Group->new($self->CurrentUser);
1559 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1560 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1569 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1571 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1573 Takes a param hash with the attributes Type and either PrincipalId or Email
1575 Type is one of Requestor, Cc, AdminCc and Owner
1577 PrincipalId is an RT::Principal id, and Email is an email address.
1579 Returns true if the specified principal (or the one corresponding to the
1580 specified address) is a member of the group Type for this ticket.
1582 XX TODO: This should be Memoized.
1589 my %args = ( Type => 'Requestor',
1590 PrincipalId => undef,
1595 # Load the relevant group.
1596 my $group = RT::Group->new($self->CurrentUser);
1597 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1599 # Find the relevant principal.
1600 if (!$args{PrincipalId} && $args{Email}) {
1601 # Look up the specified user.
1602 my $user = RT::User->new($self->CurrentUser);
1603 $user->LoadByEmail($args{Email});
1605 $args{PrincipalId} = $user->PrincipalId;
1608 # A non-existent user can't be a group member.
1613 # Ask if it has the member in question
1614 return $group->HasMember( $args{'PrincipalId'} );
1619 =head2 IsRequestor PRINCIPAL_ID
1621 Takes an L<RT::Principal> id.
1623 Returns true if the principal is a requestor of the current ticket.
1631 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1637 =head2 IsCc PRINCIPAL_ID
1639 Takes an RT::Principal id.
1640 Returns true if the principal is a Cc of the current ticket.
1649 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1655 =head2 IsAdminCc PRINCIPAL_ID
1657 Takes an RT::Principal id.
1658 Returns true if the principal is an AdminCc of the current ticket.
1666 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1674 Takes an RT::User object. Returns true if that user is this ticket's owner.
1675 returns undef otherwise
1683 # no ACL check since this is used in acl decisions
1684 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1688 #Tickets won't yet have owners when they're being created.
1689 unless ( $self->OwnerObj->id ) {
1693 if ( $person->id == $self->OwnerObj->id ) {
1705 =head2 TransactionAddresses
1707 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1708 all this ticket's Create, Comment or Correspond transactions. The keys are
1709 stringified email addresses. Each value is an L<Email::Address> object.
1711 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.
1716 sub TransactionAddresses {
1718 my $txns = $self->Transactions;
1722 my $attachments = RT::Attachments->new( $self->CurrentUser );
1723 $attachments->LimitByTicket( $self->id );
1724 $attachments->Columns( qw( id Headers TransactionId));
1727 foreach my $type (qw(Create Comment Correspond)) {
1728 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1732 ENTRYAGGREGATOR => 'OR',
1737 while ( my $att = $attachments->Next ) {
1738 foreach my $addrlist ( values %{$att->Addresses } ) {
1739 foreach my $addr (@$addrlist) {
1741 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1743 if ( $addresses{ $addr->address }
1744 && $addresses{ $addr->address }->phrase
1745 && not $addr->phrase );
1747 # skips "comment-only" addresses
1748 next unless ( $addr->address );
1749 $addresses{ $addr->address } = $addr;
1768 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1772 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1773 my $id = $QueueObj->Load($Value);
1787 my $NewQueue = shift;
1789 #Redundant. ACL gets checked in _Set;
1790 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1791 return ( 0, $self->loc("Permission Denied") );
1794 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1795 $NewQueueObj->Load($NewQueue);
1797 unless ( $NewQueueObj->Id() ) {
1798 return ( 0, $self->loc("That queue does not exist") );
1801 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1802 return ( 0, $self->loc('That is the same value') );
1804 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1805 return ( 0, $self->loc("You may not create requests in that queue.") );
1809 my $old_lifecycle = $self->QueueObj->Lifecycle;
1810 my $new_lifecycle = $NewQueueObj->Lifecycle;
1811 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1812 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1813 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1815 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status };
1816 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1820 if ( $new_status ) {
1821 my $clone = RT::Ticket->new( RT->SystemUser );
1822 $clone->Load( $self->Id );
1823 unless ( $clone->Id ) {
1824 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1827 my $now = RT::Date->new( $self->CurrentUser );
1830 my $old_status = $clone->Status;
1832 #If we're changing the status from initial in old to not intial in new,
1833 # record that we've started
1834 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1835 #Set the Started time to "now"
1839 RecordTransaction => 0
1843 #When we close a ticket, set the 'Resolved' attribute to now.
1844 # It's misnamed, but that's just historical.
1845 if ( $new_lifecycle->IsInactive($new_status) ) {
1847 Field => 'Resolved',
1849 RecordTransaction => 0,
1853 #Actually update the status
1854 my ($val, $msg)= $clone->_Set(
1856 Value => $new_status,
1857 RecordTransaction => 0,
1859 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1863 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1866 # Clear the queue object cache;
1867 $self->{_queue_obj} = undef;
1869 # Untake the ticket if we have no permissions in the new queue
1870 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1871 my $clone = RT::Ticket->new( RT->SystemUser );
1872 $clone->Load( $self->Id );
1873 unless ( $clone->Id ) {
1874 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1876 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1877 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1880 # On queue change, change queue for reminders too
1881 my $reminder_collection = $self->Reminders->Collection;
1882 while ( my $reminder = $reminder_collection->Next ) {
1883 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1884 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1888 return ($status, $msg);
1895 Takes nothing. returns this ticket's queue object
1902 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1904 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1906 #We call __Value so that we can avoid the ACL decision and some deep recursion
1907 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1909 return ($self->{_queue_obj});
1916 return $self->_Set( Field => 'Subject', Value => $value );
1921 Takes nothing. Returns SubjectTag for this ticket. Includes
1922 queue's subject tag or rtname if that is not set, ticket
1923 id and braces, for example:
1925 [support.example.com #123456]
1933 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1942 Returns an RT::Date object containing this ticket's due date
1949 my $time = RT::Date->new( $self->CurrentUser );
1951 # -1 is RT::Date slang for never
1952 if ( my $due = $self->Due ) {
1953 $time->Set( Format => 'sql', Value => $due );
1956 $time->Set( Format => 'unix', Value => -1 );
1966 Returns this ticket's due date as a human readable string
1972 return $self->DueObj->AsString();
1979 Returns an RT::Date object of this ticket's 'resolved' time.
1986 my $time = RT::Date->new( $self->CurrentUser );
1987 $time->Set( Format => 'sql', Value => $self->Resolved );
1992 =head2 FirstActiveStatus
1994 Returns the first active status that the ticket could transition to,
1995 according to its current Queue's lifecycle. May return undef if there
1996 is no such possible status to transition to, or we are already in it.
1997 This is used in L<RT::Action::AutoOpen>, for instance.
2001 sub FirstActiveStatus {
2004 my $lifecycle = $self->QueueObj->Lifecycle;
2005 my $status = $self->Status;
2006 my @active = $lifecycle->Active;
2007 # no change if no active statuses in the lifecycle
2008 return undef unless @active;
2010 # no change if the ticket is already has first status from the list of active
2011 return undef if lc $status eq lc $active[0];
2013 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
2017 =head2 FirstInactiveStatus
2019 Returns the first inactive status that the ticket could transition to,
2020 according to its current Queue's lifecycle. May return undef if there
2021 is no such possible status to transition to, or we are already in it.
2022 This is used in resolve action in UnsafeEmailCommands, for instance.
2026 sub FirstInactiveStatus {
2029 my $lifecycle = $self->QueueObj->Lifecycle;
2030 my $status = $self->Status;
2031 my @inactive = $lifecycle->Inactive;
2032 # no change if no inactive statuses in the lifecycle
2033 return undef unless @inactive;
2035 # no change if the ticket is already has first status from the list of inactive
2036 return undef if lc $status eq lc $inactive[0];
2038 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
2044 Takes a date in ISO format or undef
2045 Returns a transaction id and a message
2046 The client calls "Start" to note that the project was started on the date in $date.
2047 A null date means "now"
2053 my $time = shift || 0;
2055 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2056 return ( 0, $self->loc("Permission Denied") );
2059 #We create a date object to catch date weirdness
2060 my $time_obj = RT::Date->new( $self->CurrentUser() );
2062 $time_obj->Set( Format => 'ISO', Value => $time );
2065 $time_obj->SetToNow();
2068 # We need $TicketAsSystem, in case the current user doesn't have
2070 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
2071 $TicketAsSystem->Load( $self->Id );
2072 # Now that we're starting, open this ticket
2073 # TODO: do we really want to force this as policy? it should be a scrip
2074 my $next = $TicketAsSystem->FirstActiveStatus;
2076 $self->SetStatus( $next ) if defined $next;
2078 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2086 Returns an RT::Date object which contains this ticket's
2094 my $time = RT::Date->new( $self->CurrentUser );
2095 $time->Set( Format => 'sql', Value => $self->Started );
2103 Returns an RT::Date object which contains this ticket's
2111 my $time = RT::Date->new( $self->CurrentUser );
2112 $time->Set( Format => 'sql', Value => $self->Starts );
2120 Returns an RT::Date object which contains this ticket's
2128 my $time = RT::Date->new( $self->CurrentUser );
2129 $time->Set( Format => 'sql', Value => $self->Told );
2137 A convenience method that returns ToldObj->AsString
2139 TODO: This should be deprecated
2145 if ( $self->Told ) {
2146 return $self->ToldObj->AsString();
2155 =head2 TimeWorkedAsString
2157 Returns the amount of time worked on this ticket as a Text String
2161 sub TimeWorkedAsString {
2163 my $value = $self->TimeWorked;
2165 # return the # of minutes worked turned into seconds and written as
2166 # a simple text string, this is not really a date object, but if we
2167 # diff a number of seconds vs the epoch, we'll get a nice description
2169 return "" unless $value;
2170 return RT::Date->new( $self->CurrentUser )
2171 ->DurationAsString( $value * 60 );
2176 =head2 TimeLeftAsString
2178 Returns the amount of time left on this ticket as a Text String
2182 sub TimeLeftAsString {
2184 my $value = $self->TimeLeft;
2185 return "" unless $value;
2186 return RT::Date->new( $self->CurrentUser )
2187 ->DurationAsString( $value * 60 );
2195 Comment on this ticket.
2196 Takes a hash with the following attributes:
2197 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2200 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2202 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2203 They will, however, be prepared and you'll be able to access them through the TransactionObj
2205 Returns: Transaction id, Error Message, Transaction Object
2206 (note the different order from Create()!)
2213 my %args = ( CcMessageTo => undef,
2214 BccMessageTo => undef,
2221 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2222 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2223 return ( 0, $self->loc("Permission Denied"), undef );
2225 $args{'NoteType'} = 'Comment';
2227 $RT::Handle->BeginTransaction();
2228 if ($args{'DryRun'}) {
2229 $args{'CommitScrips'} = 0;
2232 my @results = $self->_RecordNote(%args);
2233 if ($args{'DryRun'}) {
2234 $RT::Handle->Rollback();
2236 $RT::Handle->Commit();
2245 Correspond on this ticket.
2246 Takes a hashref with the following attributes:
2249 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2251 if there's no MIMEObj, Content is used to build a MIME::Entity object
2253 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2254 They will, however, be prepared and you'll be able to access them through the TransactionObj
2256 Returns: Transaction id, Error Message, Transaction Object
2257 (note the different order from Create()!)
2264 my %args = ( CcMessageTo => undef,
2265 BccMessageTo => undef,
2271 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2272 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2273 return ( 0, $self->loc("Permission Denied"), undef );
2275 $args{'NoteType'} = 'Correspond';
2277 $RT::Handle->BeginTransaction();
2278 if ($args{'DryRun'}) {
2279 $args{'CommitScrips'} = 0;
2282 my @results = $self->_RecordNote(%args);
2284 unless ( $results[0] ) {
2285 $RT::Handle->Rollback();
2289 #Set the last told date to now if this isn't mail from the requestor.
2290 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2291 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2293 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2295 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2298 if ($args{'DryRun'}) {
2299 $RT::Handle->Rollback();
2301 $RT::Handle->Commit();
2312 the meat of both comment and correspond.
2314 Performs no access control checks. hence, dangerous.
2321 CcMessageTo => undef,
2322 BccMessageTo => undef,
2327 NoteType => 'Correspond',
2330 SquelchMailTo => undef,
2335 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2336 return ( 0, $self->loc("No message attached"), undef );
2339 unless ( $args{'MIMEObj'} ) {
2340 $args{'MIMEObj'} = MIME::Entity->build(
2341 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2345 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
2346 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
2348 # convert text parts into utf-8
2349 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2351 # If we've been passed in CcMessageTo and BccMessageTo fields,
2352 # add them to the mime object for passing on to the transaction handler
2353 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2354 # RT-Send-Bcc: headers
2357 foreach my $type (qw/Cc Bcc/) {
2358 if ( defined $args{ $type . 'MessageTo' } ) {
2360 my $addresses = join ', ', (
2361 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2362 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2363 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2367 foreach my $argument (qw(Encrypt Sign)) {
2368 $args{'MIMEObj'}->head->replace(
2369 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2370 ) if defined $args{ $argument };
2373 # If this is from an external source, we need to come up with its
2374 # internal Message-ID now, so all emails sent because of this
2375 # message have a common Message-ID
2376 my $org = RT->Config->Get('Organization');
2377 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2378 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2379 $args{'MIMEObj'}->head->set(
2380 'RT-Message-ID' => Encode::encode_utf8(
2381 RT::Interface::Email::GenMessageId( Ticket => $self )
2386 #Record the correspondence (write the transaction)
2387 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2388 Type => $args{'NoteType'},
2389 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2390 TimeTaken => $args{'TimeTaken'},
2391 MIMEObj => $args{'MIMEObj'},
2392 CommitScrips => $args{'CommitScrips'},
2393 SquelchMailTo => $args{'SquelchMailTo'},
2394 CustomFields => $args{'CustomFields'},
2398 $RT::Logger->err("$self couldn't init a transaction $msg");
2399 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2402 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2408 Builds a MIME object from the given C<UpdateSubject> and
2409 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2410 C<< DryRun => 1 >>, and returns the transaction so produced.
2418 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2419 $action = 'Correspond';
2421 $action = 'Comment';
2424 my $Message = MIME::Entity->build(
2425 Type => 'text/plain',
2426 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2428 Data => $args{'UpdateContent'} || "",
2431 my ( $Transaction, $Description, $Object ) = $self->$action(
2432 CcMessageTo => $args{'UpdateCc'},
2433 BccMessageTo => $args{'UpdateBcc'},
2434 MIMEObj => $Message,
2435 TimeTaken => $args{'UpdateTimeWorked'},
2438 unless ( $Transaction ) {
2439 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2447 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2448 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2449 the resulting L<RT::Transaction>.
2456 my $Message = MIME::Entity->build(
2457 Type => 'text/plain',
2458 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2459 (defined $args{'Cc'} ?
2460 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2462 Data => $args{'Content'} || "",
2465 my ( $Transaction, $Object, $Description ) = $self->Create(
2466 Type => $args{'Type'} || 'ticket',
2467 Queue => $args{'Queue'},
2468 Owner => $args{'Owner'},
2469 Requestor => $args{'Requestors'},
2471 AdminCc => $args{'AdminCc'},
2472 InitialPriority => $args{'InitialPriority'},
2473 FinalPriority => $args{'FinalPriority'},
2474 TimeLeft => $args{'TimeLeft'},
2475 TimeEstimated => $args{'TimeEstimated'},
2476 TimeWorked => $args{'TimeWorked'},
2477 Subject => $args{'Subject'},
2478 Status => $args{'Status'},
2479 MIMEObj => $Message,
2482 unless ( $Transaction ) {
2483 $RT::Logger->error("Couldn't fire Create action: $Description");
2494 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2497 my $type = shift || "";
2499 my $cache_key = "$field$type";
2500 return $self->{ $cache_key } if $self->{ $cache_key };
2502 my $links = $self->{ $cache_key }
2503 = RT::Links->new( $self->CurrentUser );
2504 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2505 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2509 # Maybe this ticket is a merge ticket
2510 #my $limit_on = 'Local'. $field;
2511 # at least to myself
2513 FIELD => $field, #$limit_on,
2514 OPERATOR => 'MATCHES',
2515 VALUE => 'fsck.com-rt://%/ticket/'. $self->id,
2516 ENTRYAGGREGATOR => 'OR',
2519 FIELD => $field, #$limit_on,
2520 OPERATOR => 'MATCHES',
2521 VALUE => 'fsck.com-rt://%/ticket/'. $_,
2522 ENTRYAGGREGATOR => 'OR',
2523 ) foreach $self->Merged;
2536 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2537 SilentBase and SilentTarget. Either Base or Target must be null.
2538 The null value will be replaced with this ticket's id.
2540 If Silent is true then no transaction would be recorded, in other
2541 case you can control creation of transactions on both base and
2542 target with SilentBase and SilentTarget respectively. By default
2543 both transactions are created.
2554 SilentBase => undef,
2555 SilentTarget => undef,
2559 unless ( $args{'Target'} || $args{'Base'} ) {
2560 $RT::Logger->error("Base or Target must be specified");
2561 return ( 0, $self->loc('Either base or target must be specified') );
2566 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2567 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2568 return ( 0, $self->loc("Permission Denied") );
2571 # If the other URI is an RT::Ticket, we want to make sure the user
2572 # can modify it too...
2573 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2574 return (0, $msg) unless $status;
2575 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2578 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2579 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2581 return ( 0, $self->loc("Permission Denied") );
2584 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2585 return ( 0, $Msg ) unless $val;
2587 return ( $val, $Msg ) if $args{'Silent'};
2589 my ($direction, $remote_link);
2591 if ( $args{'Base'} ) {
2592 $remote_link = $args{'Base'};
2593 $direction = 'Target';
2595 elsif ( $args{'Target'} ) {
2596 $remote_link = $args{'Target'};
2597 $direction = 'Base';
2600 my $remote_uri = RT::URI->new( $self->CurrentUser );
2601 $remote_uri->FromURI( $remote_link );
2603 unless ( $args{ 'Silent'. $direction } ) {
2604 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2605 Type => 'DeleteLink',
2606 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2607 OldValue => $remote_uri->URI || $remote_link,
2610 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2613 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2614 my $OtherObj = $remote_uri->Object;
2615 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2616 Type => 'DeleteLink',
2617 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2618 : $LINKDIRMAP{$args{'Type'}}->{Target},
2619 OldValue => $self->URI,
2620 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2623 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2626 return ( $val, $Msg );
2633 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2635 If Silent is true then no transaction would be recorded, in other
2636 case you can control creation of transactions on both base and
2637 target with SilentBase and SilentTarget respectively. By default
2638 both transactions are created.
2644 my %args = ( Target => '',
2648 SilentBase => undef,
2649 SilentTarget => undef,
2652 unless ( $args{'Target'} || $args{'Base'} ) {
2653 $RT::Logger->error("Base or Target must be specified");
2654 return ( 0, $self->loc('Either base or target must be specified') );
2658 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2659 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2660 return ( 0, $self->loc("Permission Denied") );
2663 # If the other URI is an RT::Ticket, we want to make sure the user
2664 # can modify it too...
2665 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2666 return (0, $msg) unless $status;
2667 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2670 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2671 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2673 return ( 0, $self->loc("Permission Denied") );
2676 return ( 0, "Can't link to a deleted ticket" )
2677 if $other_ticket && $other_ticket->Status eq 'deleted';
2679 return $self->_AddLink(%args);
2682 sub __GetTicketFromURI {
2684 my %args = ( URI => '', @_ );
2686 # If the other URI is an RT::Ticket, we want to make sure the user
2687 # can modify it too...
2688 my $uri_obj = RT::URI->new( $self->CurrentUser );
2689 unless ($uri_obj->FromURI( $args{'URI'} )) {
2690 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2691 $RT::Logger->warning( $msg );
2694 my $obj = $uri_obj->Resolver->Object;
2695 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2696 return (1, 'Found not a ticket', undef);
2698 return (1, 'Found ticket', $obj);
2703 Private non-acled variant of AddLink so that links can be added during create.
2709 my %args = ( Target => '',
2713 SilentBase => undef,
2714 SilentTarget => undef,
2717 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2718 return ($val, $msg) if !$val || $exist;
2719 return ($val, $msg) if $args{'Silent'};
2721 my ($direction, $remote_link);
2722 if ( $args{'Target'} ) {
2723 $remote_link = $args{'Target'};
2724 $direction = 'Base';
2725 } elsif ( $args{'Base'} ) {
2726 $remote_link = $args{'Base'};
2727 $direction = 'Target';
2730 my $remote_uri = RT::URI->new( $self->CurrentUser );
2731 $remote_uri->FromURI( $remote_link );
2733 unless ( $args{ 'Silent'. $direction } ) {
2734 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2736 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2737 NewValue => $remote_uri->URI || $remote_link,
2740 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2743 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2744 my $OtherObj = $remote_uri->Object;
2745 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2747 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2748 : $LINKDIRMAP{$args{'Type'}}->{Target},
2749 NewValue => $self->URI,
2750 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2753 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2756 return ( $val, $msg );
2764 MergeInto take the id of the ticket to merge this ticket into.
2770 my $ticket_id = shift;
2772 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2773 return ( 0, $self->loc("Permission Denied") );
2776 # Load up the new ticket.
2777 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2778 $MergeInto->Load($ticket_id);
2780 # make sure it exists.
2781 unless ( $MergeInto->Id ) {
2782 return ( 0, $self->loc("New ticket doesn't exist") );
2785 # Make sure the current user can modify the new ticket.
2786 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2787 return ( 0, $self->loc("Permission Denied") );
2790 delete $MERGE_CACHE{'effective'}{ $self->id };
2791 delete @{ $MERGE_CACHE{'merged'} }{
2792 $ticket_id, $MergeInto->id, $self->id
2795 $RT::Handle->BeginTransaction();
2797 $self->_MergeInto( $MergeInto );
2799 $RT::Handle->Commit();
2801 return ( 1, $self->loc("Merge Successful") );
2806 my $MergeInto = shift;
2809 # We use EffectiveId here even though it duplicates information from
2810 # the links table becasue of the massive performance hit we'd take
2811 # by trying to do a separate database query for merge info everytime
2814 #update this ticket's effective id to the new ticket's id.
2815 my ( $id_val, $id_msg ) = $self->__Set(
2816 Field => 'EffectiveId',
2817 Value => $MergeInto->Id()
2821 $RT::Handle->Rollback();
2822 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2826 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2827 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2828 my ( $status_val, $status_msg )
2829 = $self->__Set( Field => 'Status', Value => $force_status );
2831 unless ($status_val) {
2832 $RT::Handle->Rollback();
2834 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2836 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2840 # update all the links that point to that old ticket
2841 my $old_links_to = RT::Links->new($self->CurrentUser);
2842 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2845 while (my $link = $old_links_to->Next) {
2846 if (exists $old_seen{$link->Base."-".$link->Type}) {
2849 elsif ($link->Base eq $MergeInto->URI) {
2852 # First, make sure the link doesn't already exist. then move it over.
2853 my $tmp = RT::Link->new(RT->SystemUser);
2854 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2858 $link->SetTarget($MergeInto->URI);
2859 $link->SetLocalTarget($MergeInto->id);
2861 $old_seen{$link->Base."-".$link->Type} =1;
2866 my $old_links_from = RT::Links->new($self->CurrentUser);
2867 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2869 while (my $link = $old_links_from->Next) {
2870 if (exists $old_seen{$link->Type."-".$link->Target}) {
2873 if ($link->Target eq $MergeInto->URI) {
2876 # First, make sure the link doesn't already exist. then move it over.
2877 my $tmp = RT::Link->new(RT->SystemUser);
2878 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2882 $link->SetBase($MergeInto->URI);
2883 $link->SetLocalBase($MergeInto->id);
2884 $old_seen{$link->Type."-".$link->Target} =1;
2890 # Update time fields
2891 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2893 my $mutator = "Set$type";
2894 $MergeInto->$mutator(
2895 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2898 #add all of this ticket's watchers to that ticket.
2899 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2901 my $people = $self->$watcher_type->MembersObj;
2902 my $addwatcher_type = $watcher_type;
2903 $addwatcher_type =~ s/s$//;
2905 while ( my $watcher = $people->Next ) {
2907 my ($val, $msg) = $MergeInto->_AddWatcher(
2908 Type => $addwatcher_type,
2910 PrincipalId => $watcher->MemberId
2913 $RT::Logger->debug($msg);
2919 #find all of the tickets that were merged into this ticket.
2920 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2921 $old_mergees->Limit(
2922 FIELD => 'EffectiveId',
2927 # update their EffectiveId fields to the new ticket's id
2928 while ( my $ticket = $old_mergees->Next() ) {
2929 my ( $val, $msg ) = $ticket->__Set(
2930 Field => 'EffectiveId',
2931 Value => $MergeInto->Id()
2935 #make a new link: this ticket is merged into that other ticket.
2936 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2938 $MergeInto->_SetLastUpdated;
2943 Returns list of tickets' ids that's been merged into this ticket.
2951 return @{ $MERGE_CACHE{'merged'}{ $id } }
2952 if $MERGE_CACHE{'merged'}{ $id };
2954 my $mergees = RT::Tickets->new( $self->CurrentUser );
2956 FIELD => 'EffectiveId',
2964 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2965 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2974 Takes nothing and returns an RT::User object of
2982 #If this gets ACLed, we lose on a rights check in User.pm and
2983 #get deep recursion. if we need ACLs here, we need
2984 #an equiv without ACLs
2986 my $owner = RT::User->new( $self->CurrentUser );
2987 $owner->Load( $self->__Value('Owner') );
2989 #Return the owner object
2995 =head2 OwnerAsString
2997 Returns the owner's email address
3003 return ( $self->OwnerObj->EmailAddress );
3011 Takes two arguments:
3012 the Id or Name of the owner
3013 and (optionally) the type of the SetOwner Transaction. It defaults
3014 to 'Set'. 'Steal' is also a valid option.
3021 my $NewOwner = shift;
3022 my $Type = shift || "Set";
3024 $RT::Handle->BeginTransaction();
3026 $self->_SetLastUpdated(); # lock the ticket
3027 $self->Load( $self->id ); # in case $self changed while waiting for lock
3029 my $OldOwnerObj = $self->OwnerObj;
3031 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3032 $NewOwnerObj->Load( $NewOwner );
3033 unless ( $NewOwnerObj->Id ) {
3034 $RT::Handle->Rollback();
3035 return ( 0, $self->loc("That user does not exist") );
3039 # must have ModifyTicket rights
3040 # or TakeTicket/StealTicket and $NewOwner is self
3041 # see if it's a take
3042 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
3043 unless ( $self->CurrentUserHasRight('ModifyTicket')
3044 || $self->CurrentUserHasRight('TakeTicket') ) {
3045 $RT::Handle->Rollback();
3046 return ( 0, $self->loc("Permission Denied") );
3050 # see if it's a steal
3051 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
3052 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3054 unless ( $self->CurrentUserHasRight('ModifyTicket')
3055 || $self->CurrentUserHasRight('StealTicket') ) {
3056 $RT::Handle->Rollback();
3057 return ( 0, $self->loc("Permission Denied") );
3061 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3062 $RT::Handle->Rollback();
3063 return ( 0, $self->loc("Permission Denied") );
3067 # If we're not stealing and the ticket has an owner and it's not
3069 if ( $Type ne 'Steal' and $Type ne 'Force'
3070 and $OldOwnerObj->Id != RT->Nobody->Id
3071 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3073 $RT::Handle->Rollback();
3074 return ( 0, $self->loc("You can only take tickets that are unowned") )
3075 if $NewOwnerObj->id == $self->CurrentUser->id;
3078 $self->loc("You can only reassign tickets that you own or that are unowned" )
3082 #If we've specified a new owner and that user can't modify the ticket
3083 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3084 $RT::Handle->Rollback();
3085 return ( 0, $self->loc("That user may not own tickets in that queue") );
3088 # If the ticket has an owner and it's the new owner, we don't need
3090 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3091 $RT::Handle->Rollback();
3092 return ( 0, $self->loc("That user already owns that ticket") );
3095 # Delete the owner in the owner group, then add a new one
3096 # TODO: is this safe? it's not how we really want the API to work
3097 # for most things, but it's fast.
3098 my ( $del_id, $del_msg );
3099 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3100 ($del_id, $del_msg) = $owner->Delete();
3101 last unless ($del_id);
3105 $RT::Handle->Rollback();
3106 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3109 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3110 PrincipalId => $NewOwnerObj->PrincipalId,
3111 InsideTransaction => 1 );
3113 $RT::Handle->Rollback();
3114 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3117 # We call set twice with slightly different arguments, so
3118 # as to not have an SQL transaction span two RT transactions
3120 my ( $val, $msg ) = $self->_Set(
3122 RecordTransaction => 0,
3123 Value => $NewOwnerObj->Id,
3125 TransactionType => 'Set',
3126 CheckACL => 0, # don't check acl
3130 $RT::Handle->Rollback;
3131 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3134 ($val, $msg) = $self->_NewTransaction(
3137 NewValue => $NewOwnerObj->Id,
3138 OldValue => $OldOwnerObj->Id,
3143 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3144 $OldOwnerObj->Name, $NewOwnerObj->Name );
3147 $RT::Handle->Rollback();
3151 $RT::Handle->Commit();
3153 return ( $val, $msg );
3160 A convenince method to set the ticket's owner to the current user
3166 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3173 Convenience method to set the owner to 'nobody' if the current user is the owner.
3179 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3186 A convenience method to change the owner of the current ticket to the
3187 current user. Even if it's owned by another user.
3194 if ( $self->IsOwner( $self->CurrentUser ) ) {
3195 return ( 0, $self->loc("You already own this ticket") );
3198 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3208 =head2 ValidateStatus STATUS
3210 Takes a string. Returns true if that status is a valid status for this ticket.
3211 Returns false otherwise.
3215 sub ValidateStatus {
3219 #Make sure the status passed in is valid
3220 return 1 if $self->QueueObj->IsValidStatus($status);
3223 while ( my $caller = (caller($i++))[3] ) {
3224 return 1 if $caller eq 'RT::Ticket::SetQueue';
3232 my $value = $self->_Value( 'Status' );
3233 return $value unless $self->QueueObj;
3234 return $self->QueueObj->Lifecycle->CanonicalCase( $value );
3237 =head2 SetStatus STATUS
3239 Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3241 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3242 If FORCE is true, ignore unresolved dependencies and force a status change.
3243 if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3244 is not set and the status is changed from initial to not initial.
3252 $args{Status} = shift;
3258 # this only allows us to SetStarted, not we must SetStarted.
3259 # this option was added for rtir initially
3260 $args{SetStarted} = 1 unless exists $args{SetStarted};
3263 my $lifecycle = $self->QueueObj->Lifecycle;
3265 my $new = lc $args{'Status'};
3266 unless ( $lifecycle->IsValid( $new ) ) {
3267 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3270 my $old = $self->__Value('Status');
3271 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3272 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3275 my $check_right = $lifecycle->CheckRight( $old => $new );
3276 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3277 return ( 0, $self->loc('Permission Denied') );
3280 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3281 return (0, $self->loc('That ticket has unresolved dependencies'));
3284 my $now = RT::Date->new( $self->CurrentUser );
3287 my $raw_started = RT::Date->new(RT->SystemUser);
3288 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3290 #If we're changing the status from new, record that we've started
3291 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3292 #Set the Started time to "now"
3296 RecordTransaction => 0
3300 #When we close a ticket, set the 'Resolved' attribute to now.
3301 # It's misnamed, but that's just historical.
3302 if ( $lifecycle->IsInactive($new) ) {
3304 Field => 'Resolved',
3306 RecordTransaction => 0,
3310 #Actually update the status
3311 my ($val, $msg)= $self->_Set(
3316 TransactionType => 'Status',
3318 return ($val, $msg);
3325 Takes no arguments. Marks this ticket for garbage collection
3331 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3332 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3334 return ( $self->SetStatus('deleted') );
3338 =head2 SetTold ISO [TIMETAKEN]
3340 Updates the told and records a transaction
3347 $told = shift if (@_);
3348 my $timetaken = shift || 0;
3350 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3351 return ( 0, $self->loc("Permission Denied") );
3354 my $datetold = RT::Date->new( $self->CurrentUser );
3356 $datetold->Set( Format => 'iso',
3360 $datetold->SetToNow();
3363 return ( $self->_Set( Field => 'Told',
3364 Value => $datetold->ISO,
3365 TimeTaken => $timetaken,
3366 TransactionType => 'Told' ) );
3371 Updates the told without a transaction or acl check. Useful when we're sending replies.
3378 my $now = RT::Date->new( $self->CurrentUser );
3381 #use __Set to get no ACLs ;)
3382 return ( $self->__Set( Field => 'Told',
3383 Value => $now->ISO ) );
3393 my $uid = $self->CurrentUser->id;
3394 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3395 return if $attr && $attr->Content gt $self->LastUpdated;
3397 my $txns = $self->Transactions;
3398 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3399 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3400 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3404 VALUE => $attr->Content
3406 $txns->RowsPerPage(1);
3407 return $txns->First;
3410 =head2 RanTransactionBatch
3412 Acts as a guard around running TransactionBatch scrips.
3414 Should be false until you enter the code that runs TransactionBatch scrips
3416 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3420 sub RanTransactionBatch {
3424 if ( defined $val ) {
3425 return $self->{_RanTransactionBatch} = $val;
3427 return $self->{_RanTransactionBatch};
3433 =head2 TransactionBatch
3435 Returns an array reference of all transactions created on this ticket during
3436 this ticket object's lifetime or since last application of a batch, or undef
3439 Only works when the C<UseTransactionBatch> config option is set to true.
3443 sub TransactionBatch {
3445 return $self->{_TransactionBatch};
3448 =head2 ApplyTransactionBatch
3450 Applies scrips on the current batch of transactions and shinks it. Usually
3451 batch is applied when object is destroyed, but in some cases it's too late.
3455 sub ApplyTransactionBatch {
3458 my $batch = $self->TransactionBatch;
3459 return unless $batch && @$batch;
3461 $self->_ApplyTransactionBatch;
3463 $self->{_TransactionBatch} = [];
3466 sub _ApplyTransactionBatch {
3469 return if $self->RanTransactionBatch;
3470 $self->RanTransactionBatch(1);
3472 my $still_exists = RT::Ticket->new( RT->SystemUser );
3473 $still_exists->Load( $self->Id );
3474 if (not $still_exists->Id) {
3475 # The ticket has been removed from the database, but we still
3476 # have pending TransactionBatch txns for it. Unfortunately,
3477 # because it isn't in the DB anymore, attempting to run scrips
3478 # on it may produce unpredictable results; simply drop the
3479 # batched transactions.
3480 $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.");
3484 my $batch = $self->TransactionBatch;
3487 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3490 RT::Scrips->new(RT->SystemUser)->Apply(
3491 Stage => 'TransactionBatch',
3493 TransactionObj => $batch->[0],
3497 # Entry point of the rule system
3498 my $rules = RT::Ruleset->FindAllRules(
3499 Stage => 'TransactionBatch',
3501 TransactionObj => $batch->[0],
3504 RT::Ruleset->CommitRules($rules);
3510 # DESTROY methods need to localize $@, or it may unset it. This
3511 # causes $m->abort to not bubble all of the way up. See perlbug
3512 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3515 # The following line eliminates reentrancy.
3516 # It protects against the fact that perl doesn't deal gracefully
3517 # when an object's refcount is changed in its destructor.
3518 return if $self->{_Destroyed}++;
3520 if (in_global_destruction()) {
3521 unless ($ENV{'HARNESS_ACTIVE'}) {
3522 warn "Too late to safely run transaction-batch scrips!"
3523 ." This is typically caused by using ticket objects"
3524 ." at the top-level of a script which uses the RT API."
3525 ." Be sure to explicitly undef such ticket objects,"
3526 ." or put them inside of a lexical scope.";
3531 return $self->ApplyTransactionBatch;
3537 sub _OverlayAccessible {
3539 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3540 Queue => { 'read' => 1, 'write' => 1 },
3541 Requestors => { 'read' => 1, 'write' => 1 },
3542 Owner => { 'read' => 1, 'write' => 1 },
3543 Subject => { 'read' => 1, 'write' => 1 },
3544 InitialPriority => { 'read' => 1, 'write' => 1 },
3545 FinalPriority => { 'read' => 1, 'write' => 1 },
3546 Priority => { 'read' => 1, 'write' => 1 },
3547 Status => { 'read' => 1, 'write' => 1 },
3548 TimeEstimated => { 'read' => 1, 'write' => 1 },
3549 TimeWorked => { 'read' => 1, 'write' => 1 },
3550 TimeLeft => { 'read' => 1, 'write' => 1 },
3551 Told => { 'read' => 1, 'write' => 1 },
3552 Resolved => { 'read' => 1 },
3553 Type => { 'read' => 1 },
3554 Starts => { 'read' => 1, 'write' => 1 },
3555 Started => { 'read' => 1, 'write' => 1 },
3556 Due => { 'read' => 1, 'write' => 1 },
3557 Creator => { 'read' => 1, 'auto' => 1 },
3558 Created => { 'read' => 1, 'auto' => 1 },
3559 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3560 LastUpdated => { 'read' => 1, 'auto' => 1 }
3570 my %args = ( Field => undef,
3573 RecordTransaction => 1,
3576 TransactionType => 'Set',
3579 if ($args{'CheckACL'}) {
3580 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3581 return ( 0, $self->loc("Permission Denied"));
3585 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3586 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3587 return(0, $self->loc("Internal Error"));
3590 #if the user is trying to modify the record
3592 #Take care of the old value we really don't want to get in an ACL loop.
3593 # so ask the super::_Value
3594 my $Old = $self->SUPER::_Value("$args{'Field'}");
3597 if ( $args{'UpdateTicket'} ) {
3600 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3601 Value => $args{'Value'} );
3603 #If we can't actually set the field to the value, don't record
3604 # a transaction. instead, get out of here.
3605 return ( 0, $msg ) unless $ret;
3608 if ( $args{'RecordTransaction'} == 1 ) {
3610 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3611 Type => $args{'TransactionType'},
3612 Field => $args{'Field'},
3613 NewValue => $args{'Value'},
3615 TimeTaken => $args{'TimeTaken'},
3617 # Ensure that we can read the transaction, even if the change
3618 # just made the ticket unreadable to us
3619 $TransObj->{ _object_is_readable } = 1;
3620 return ( $Trans, scalar $TransObj->BriefDescription );
3623 return ( $ret, $msg );
3631 Takes the name of a table column.
3632 Returns its value as a string, if the user passes an ACL check
3641 #if the field is public, return it.
3642 if ( $self->_Accessible( $field, 'public' ) ) {
3644 #$RT::Logger->debug("Skipping ACL check for $field");
3645 return ( $self->SUPER::_Value($field) );
3649 #If the current user doesn't have ACLs, don't let em at it.
3651 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3654 return ( $self->SUPER::_Value($field) );
3660 =head2 _UpdateTimeTaken
3662 This routine will increment the timeworked counter. it should
3663 only be called from _NewTransaction
3667 sub _UpdateTimeTaken {
3669 my $Minutes = shift;
3672 $Total = $self->SUPER::_Value("TimeWorked");
3673 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3675 Field => "TimeWorked",
3686 =head2 CurrentUserHasRight
3688 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3689 1 if the user has that right. It returns 0 if the user doesn't have that right.
3693 sub CurrentUserHasRight {
3697 return $self->CurrentUser->PrincipalObj->HasRight(
3704 =head2 CurrentUserCanSee
3706 Returns true if the current user can see the ticket, using ShowTicket
3710 sub CurrentUserCanSee {
3712 return $self->CurrentUserHasRight('ShowTicket');
3717 Takes a paramhash with the attributes 'Right' and 'Principal'
3718 'Right' is a ticket-scoped textual right from RT::ACE
3719 'Principal' is an RT::User object
3721 Returns 1 if the principal has the right. Returns undef if not.
3733 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3735 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3736 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3741 $args{'Principal'}->HasRight(
3743 Right => $args{'Right'}
3752 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3753 It isn't acutally a searchbuilder collection itself.
3760 unless ($self->{'__reminders'}) {
3761 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3762 $self->{'__reminders'}->Ticket($self->id);
3764 return $self->{'__reminders'};
3773 Returns an RT::Transactions object of all transactions on this ticket
3780 my $transactions = RT::Transactions->new( $self->CurrentUser );
3782 #If the user has no rights, return an empty object
3783 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3784 $transactions->LimitToTicket($self->id);
3786 # if the user may not see comments do not return them
3787 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3788 $transactions->Limit(
3794 $transactions->Limit(
3798 VALUE => "CommentEmailRecord",
3799 ENTRYAGGREGATOR => 'AND'
3804 $transactions->Limit(
3808 ENTRYAGGREGATOR => 'AND'
3812 return ($transactions);
3818 =head2 TransactionCustomFields
3820 Returns the custom fields that transactions on tickets will have.
3824 sub TransactionCustomFields {
3826 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3827 $cfs->SetContextObject( $self );
3832 =head2 LoadCustomFieldByIdentifier
3834 Finds and returns the custom field of the given name for the ticket,
3835 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3836 queue-specific CFs before global ones.
3840 sub LoadCustomFieldByIdentifier {
3844 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3845 if ref $field or $field =~ /^\d+$/;
3847 my $cf = RT::CustomField->new( $self->CurrentUser );
3848 $cf->SetContextObject( $self );
3849 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3850 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
3855 =head2 CustomFieldLookupType
3857 Returns the RT::Ticket lookup type, which can be passed to
3858 RT::CustomField->Create() via the 'LookupType' hash key.
3863 sub CustomFieldLookupType {
3864 "RT::Queue-RT::Ticket";
3867 =head2 ACLEquivalenceObjects
3869 This method returns a list of objects for which a user's rights also apply
3870 to this ticket. Generally, this is only the ticket's queue, but some RT
3871 extensions may make other objects available too.
3873 This method is called from L<RT::Principal/HasRight>.
3877 sub ACLEquivalenceObjects {
3879 return $self->QueueObj;
3888 Jesse Vincent, jesse@bestpractical.com
3898 use base 'RT::Record';
3900 sub Table {'Tickets'}
3909 Returns the current value of id.
3910 (In the database, id is stored as int(11).)
3918 Returns the current value of EffectiveId.
3919 (In the database, EffectiveId is stored as int(11).)
3923 =head2 SetEffectiveId VALUE
3926 Set EffectiveId to VALUE.
3927 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3928 (In the database, EffectiveId will be stored as a int(11).)
3936 Returns the current value of Queue.
3937 (In the database, Queue is stored as int(11).)
3941 =head2 SetQueue VALUE
3945 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3946 (In the database, Queue will be stored as a int(11).)
3954 Returns the current value of Type.
3955 (In the database, Type is stored as varchar(16).)
3959 =head2 SetType VALUE
3963 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3964 (In the database, Type will be stored as a varchar(16).)
3970 =head2 IssueStatement
3972 Returns the current value of IssueStatement.
3973 (In the database, IssueStatement is stored as int(11).)
3977 =head2 SetIssueStatement VALUE
3980 Set IssueStatement to VALUE.
3981 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3982 (In the database, IssueStatement will be stored as a int(11).)
3990 Returns the current value of Resolution.
3991 (In the database, Resolution is stored as int(11).)
3995 =head2 SetResolution VALUE
3998 Set Resolution to VALUE.
3999 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4000 (In the database, Resolution will be stored as a int(11).)
4008 Returns the current value of Owner.
4009 (In the database, Owner is stored as int(11).)
4013 =head2 SetOwner VALUE
4017 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4018 (In the database, Owner will be stored as a int(11).)
4026 Returns the current value of Subject.
4027 (In the database, Subject is stored as varchar(200).)
4031 =head2 SetSubject VALUE
4034 Set Subject to VALUE.
4035 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4036 (In the database, Subject will be stored as a varchar(200).)
4042 =head2 InitialPriority
4044 Returns the current value of InitialPriority.
4045 (In the database, InitialPriority is stored as int(11).)
4049 =head2 SetInitialPriority VALUE
4052 Set InitialPriority to VALUE.
4053 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4054 (In the database, InitialPriority will be stored as a int(11).)
4060 =head2 FinalPriority
4062 Returns the current value of FinalPriority.
4063 (In the database, FinalPriority is stored as int(11).)
4067 =head2 SetFinalPriority VALUE
4070 Set FinalPriority to VALUE.
4071 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4072 (In the database, FinalPriority will be stored as a int(11).)
4080 Returns the current value of Priority.
4081 (In the database, Priority is stored as int(11).)
4085 =head2 SetPriority VALUE
4088 Set Priority to VALUE.
4089 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4090 (In the database, Priority will be stored as a int(11).)
4096 =head2 TimeEstimated
4098 Returns the current value of TimeEstimated.
4099 (In the database, TimeEstimated is stored as int(11).)
4103 =head2 SetTimeEstimated VALUE
4106 Set TimeEstimated to VALUE.
4107 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4108 (In the database, TimeEstimated will be stored as a int(11).)
4116 Returns the current value of TimeWorked.
4117 (In the database, TimeWorked is stored as int(11).)
4121 =head2 SetTimeWorked VALUE
4124 Set TimeWorked to VALUE.
4125 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4126 (In the database, TimeWorked will be stored as a int(11).)
4134 Returns the current value of Status.
4135 (In the database, Status is stored as varchar(64).)
4139 =head2 SetStatus VALUE
4142 Set Status to VALUE.
4143 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4144 (In the database, Status will be stored as a varchar(64).)
4152 Returns the current value of TimeLeft.
4153 (In the database, TimeLeft is stored as int(11).)
4157 =head2 SetTimeLeft VALUE
4160 Set TimeLeft to VALUE.
4161 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4162 (In the database, TimeLeft will be stored as a int(11).)
4170 Returns the current value of Told.
4171 (In the database, Told is stored as datetime.)
4175 =head2 SetTold VALUE
4179 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4180 (In the database, Told will be stored as a datetime.)
4188 Returns the current value of Starts.
4189 (In the database, Starts is stored as datetime.)
4193 =head2 SetStarts VALUE
4196 Set Starts to VALUE.
4197 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4198 (In the database, Starts will be stored as a datetime.)
4206 Returns the current value of Started.
4207 (In the database, Started is stored as datetime.)
4211 =head2 SetStarted VALUE
4214 Set Started to VALUE.
4215 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4216 (In the database, Started will be stored as a datetime.)
4224 Returns the current value of Due.
4225 (In the database, Due is stored as datetime.)
4233 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4234 (In the database, Due will be stored as a datetime.)
4242 Returns the current value of Resolved.
4243 (In the database, Resolved is stored as datetime.)
4247 =head2 SetResolved VALUE
4250 Set Resolved to VALUE.
4251 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4252 (In the database, Resolved will be stored as a datetime.)
4258 =head2 LastUpdatedBy
4260 Returns the current value of LastUpdatedBy.
4261 (In the database, LastUpdatedBy is stored as int(11).)
4269 Returns the current value of LastUpdated.
4270 (In the database, LastUpdated is stored as datetime.)
4278 Returns the current value of Creator.
4279 (In the database, Creator is stored as int(11).)
4287 Returns the current value of Created.
4288 (In the database, Created is stored as datetime.)
4296 Returns the current value of Disabled.
4297 (In the database, Disabled is stored as smallint(6).)
4301 =head2 SetDisabled VALUE
4304 Set Disabled to VALUE.
4305 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4306 (In the database, Disabled will be stored as a smallint(6).)
4313 sub _CoreAccessible {
4317 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
4319 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4321 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4323 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
4325 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4327 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
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 => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
4333 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4343 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
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 => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4349 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4351 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4353 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4355 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4357 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4359 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4361 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4363 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4365 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
4370 RT::Base->_ImportOverlays();