1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
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 unless ( $cycle->IsValid( $args{'Status'} ) ) {
304 $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
305 $self->loc($args{'Status'}))
309 unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
311 $self->loc("New tickets can not have status '[_1]' in this queue.",
312 $self->loc($args{'Status'}))
318 #Since we have a queue, we can set queue defaults
321 # If there's no queue default initial priority and it's not set, set it to 0
322 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
323 unless defined $args{'InitialPriority'};
326 # If there's no queue default final priority and it's not set, set it to 0
327 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
328 unless defined $args{'FinalPriority'};
330 # Priority may have changed from InitialPriority, for the case
331 # where we're importing tickets (eg, from an older RT version.)
332 $args{'Priority'} = $args{'InitialPriority'}
333 unless defined $args{'Priority'};
336 #TODO we should see what sort of due date we're getting, rather +
337 # than assuming it's in ISO format.
339 #Set the due date. if we didn't get fed one, use the queue default due in
340 my $Due = RT::Date->new( $self->CurrentUser );
341 if ( defined $args{'Due'} ) {
342 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
344 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
346 $Due->AddDays( $due_in );
349 my $Starts = RT::Date->new( $self->CurrentUser );
350 if ( defined $args{'Starts'} ) {
351 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
354 my $Started = RT::Date->new( $self->CurrentUser );
355 if ( defined $args{'Started'} ) {
356 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
359 # If the status is not an initial status, set the started date
360 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
364 my $Resolved = RT::Date->new( $self->CurrentUser );
365 if ( defined $args{'Resolved'} ) {
366 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
369 #If the status is an inactive status, set the resolved date
370 elsif ( $cycle->IsInactive( $args{'Status'} ) )
372 $RT::Logger->debug( "Got a ". $args{'Status'}
373 ."(inactive) ticket with undefined resolved date. Setting to now."
380 # Dealing with time fields
382 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
383 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
384 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
388 # Deal with setting the owner
391 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
392 if ( $args{'Owner'}->id ) {
393 $Owner = $args{'Owner'};
395 $RT::Logger->error('Passed an empty RT::User for owner');
396 push @non_fatal_errors,
397 $self->loc("Owner could not be set.") . " ".
398 $self->loc("Invalid value for [_1]",loc('owner'));
403 #If we've been handed something else, try to load the user.
404 elsif ( $args{'Owner'} ) {
405 $Owner = RT::User->new( $self->CurrentUser );
406 $Owner->Load( $args{'Owner'} );
408 $Owner->LoadByEmail( $args{'Owner'} )
410 unless ( $Owner->Id ) {
411 push @non_fatal_errors,
412 $self->loc("Owner could not be set.") . " "
413 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
418 #If we have a proposed owner and they don't have the right
419 #to own a ticket, scream about it and make them not the owner
422 if ( $Owner && $Owner->Id != RT->Nobody->Id
423 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
425 $DeferOwner = $Owner;
427 $RT::Logger->debug('going to deffer setting owner');
431 #If we haven't been handed a valid owner, make it nobody.
432 unless ( defined($Owner) && $Owner->Id ) {
433 $Owner = RT::User->new( $self->CurrentUser );
434 $Owner->Load( RT->Nobody->Id );
439 # We attempt to load or create each of the people who might have a role for this ticket
440 # _outside_ the transaction, so we don't get into ticket creation races
441 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
442 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
443 foreach my $watcher ( splice @{ $args{$type} } ) {
444 next unless $watcher;
445 if ( $watcher =~ /^\d+$/ ) {
446 push @{ $args{$type} }, $watcher;
448 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
449 foreach my $address( @addresses ) {
450 my $user = RT::User->new( RT->SystemUser );
451 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
453 push @non_fatal_errors,
454 $self->loc("Couldn't load or create user: [_1]", $msg);
456 push @{ $args{$type} }, $user->id;
463 $RT::Handle->BeginTransaction();
466 Queue => $QueueObj->Id,
468 Subject => $args{'Subject'},
469 InitialPriority => $args{'InitialPriority'},
470 FinalPriority => $args{'FinalPriority'},
471 Priority => $args{'Priority'},
472 Status => $args{'Status'},
473 TimeWorked => $args{'TimeWorked'},
474 TimeEstimated => $args{'TimeEstimated'},
475 TimeLeft => $args{'TimeLeft'},
476 Type => $args{'Type'},
477 Starts => $Starts->ISO,
478 Started => $Started->ISO,
479 Resolved => $Resolved->ISO,
483 # Parameters passed in during an import that we probably don't want to touch, otherwise
484 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
485 $params{$attr} = $args{$attr} if $args{$attr};
488 # Delete null integer parameters
490 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
492 delete $params{$attr}
493 unless ( exists $params{$attr} && $params{$attr} );
496 # Delete the time worked if we're counting it in the transaction
497 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
499 my ($id,$ticket_message) = $self->SUPER::Create( %params );
501 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
502 $RT::Handle->Rollback();
504 $self->loc("Ticket could not be created due to an internal error")
508 #Set the ticket's effective ID now that we've created it.
509 my ( $val, $msg ) = $self->__Set(
510 Field => 'EffectiveId',
511 Value => ( $args{'EffectiveId'} || $id )
514 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
515 $RT::Handle->Rollback;
517 $self->loc("Ticket could not be created due to an internal error")
521 my $create_groups_ret = $self->_CreateTicketGroups();
522 unless ($create_groups_ret) {
523 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
525 . ". aborting Ticket creation." );
526 $RT::Handle->Rollback();
528 $self->loc("Ticket could not be created due to an internal error")
532 # Set the owner in the Groups table
533 # We denormalize it into the Ticket table too because doing otherwise would
534 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
535 $self->OwnerGroup->_AddMember(
536 PrincipalId => $Owner->PrincipalId,
537 InsideTransaction => 1
538 ) unless $DeferOwner;
542 # Deal with setting up watchers
544 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
545 # we know it's an array ref
546 foreach my $watcher ( @{ $args{$type} } ) {
548 # Note that we're using AddWatcher, rather than _AddWatcher, as we
549 # actually _want_ that ACL check. Otherwise, random ticket creators
550 # could make themselves adminccs and maybe get ticket rights. that would
552 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
554 my ($val, $msg) = $self->$method(
556 PrincipalId => $watcher,
559 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
564 if ($args{'SquelchMailTo'}) {
565 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
566 : $args{'SquelchMailTo'};
567 $self->_SquelchMailTo( @squelch );
573 # Add all the custom fields
575 foreach my $arg ( keys %args ) {
576 next unless $arg =~ /^CustomField-(\d+)$/i;
580 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
582 next unless defined $value && length $value;
584 # Allow passing in uploaded LargeContent etc by hash reference
585 my ($status, $msg) = $self->_AddCustomFieldValue(
586 (UNIVERSAL::isa( $value => 'HASH' )
591 RecordTransaction => 0,
593 push @non_fatal_errors, $msg unless $status;
599 # Deal with setting up links
601 # TODO: Adding link may fire scrips on other end and those scrips
602 # could create transactions on this ticket before 'Create' transaction.
604 # We should implement different lifecycle: record 'Create' transaction,
605 # create links and only then fire create transaction's scrips.
607 # Ideal variant: add all links without firing scrips, record create
608 # transaction and only then fire scrips on the other ends of links.
612 foreach my $type ( keys %LINKTYPEMAP ) {
613 next unless ( defined $args{$type} );
615 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
617 my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
619 push @non_fatal_errors, $msg;
623 # Check rights on the other end of the link if we must
624 # then run _AddLink that doesn't check for ACLs
625 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
626 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
627 push @non_fatal_errors, $self->loc('Linking. Permission denied');
632 if ( $obj && $obj->Status eq 'deleted' ) {
633 push @non_fatal_errors,
634 $self->loc("Linking. Can't link to a deleted ticket");
638 my ( $wval, $wmsg ) = $self->_AddLink(
639 Type => $LINKTYPEMAP{$type}->{'Type'},
640 $LINKTYPEMAP{$type}->{'Mode'} => $link,
641 Silent => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
642 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
646 push @non_fatal_errors, $wmsg unless ($wval);
652 # {{{ Deal with auto-customer association
654 #unless we already have (a) customer(s)...
655 unless ( $self->Customers->Count ) {
657 #first find any requestors with emails but *without* customer targets
658 my @NoCust_Requestors =
659 grep { $_->EmailAddress && ! $_->Customers->Count }
660 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
662 for my $Requestor (@NoCust_Requestors) {
664 #perhaps the stuff in here should be in a User method??
666 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
668 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
670 ## false laziness w/RT/Interface/Web_Vendor.pm
671 my @link = ( 'Type' => 'MemberOf',
672 'Target' => "freeside://freeside/cust_main/$custnum",
675 my( $val, $msg ) = $Requestor->_AddLink(@link);
676 #XXX should do something with $msg# push @non_fatal_errors, $msg;
682 #find any requestors with customer targets
684 my %cust_target = ();
687 grep { $_->Customers->Count }
688 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
690 foreach my $Requestor ( @Requestors ) {
691 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
692 $cust_target{ $cust_link->Target } = 1;
696 #and then auto-associate this ticket with those customers
698 foreach my $cust_target ( keys %cust_target ) {
700 my @link = ( 'Type' => 'MemberOf',
701 #'Target' => "freeside://freeside/cust_main/$custnum",
702 'Target' => $cust_target,
705 my( $val, $msg ) = $self->_AddLink(@link);
706 push @non_fatal_errors, $msg;
714 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
715 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
717 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
719 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
720 . ") was proposed as a ticket owner but has no rights to own "
721 . "tickets in " . $QueueObj->Name );
722 push @non_fatal_errors, $self->loc(
723 "Owner '[_1]' does not have rights to own this ticket.",
727 $Owner = $DeferOwner;
728 $self->__Set(Field => 'Owner', Value => $Owner->id);
731 $self->OwnerGroup->_AddMember(
732 PrincipalId => $Owner->PrincipalId,
733 InsideTransaction => 1
737 #don't make a transaction or fire off any scrips for reminders either
738 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
740 # Add a transaction for the create
741 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
743 TimeTaken => $args{'TimeWorked'},
744 MIMEObj => $args{'MIMEObj'},
745 CommitScrips => !$args{'DryRun'},
746 SquelchMailTo => $args{'TransSquelchMailTo'},
749 if ( $self->Id && $Trans ) {
751 #$TransObj->UpdateCustomFields(ARGSRef => \%args);
752 $TransObj->UpdateCustomFields(%args);
754 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
755 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
756 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
759 $RT::Handle->Rollback();
761 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
762 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
763 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
766 if ( $args{'DryRun'} ) {
767 $RT::Handle->Rollback();
768 return ($self->id, $TransObj, $ErrStr);
770 $RT::Handle->Commit();
771 return ( $self->Id, $TransObj->Id, $ErrStr );
777 # Not going to record a transaction
778 $RT::Handle->Commit();
779 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
780 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
781 return ( $self->Id, 0, $ErrStr );
789 =head2 _Parse822HeadersForAttributes Content
791 Takes an RFC822 style message and parses its attributes into a hash.
795 sub _Parse822HeadersForAttributes {
800 my @lines = ( split ( /\n/, $content ) );
801 while ( defined( my $line = shift @lines ) ) {
802 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
807 if ( defined( $args{$tag} ) )
808 { #if we're about to get a second value, make it an array
809 $args{$tag} = [ $args{$tag} ];
811 if ( ref( $args{$tag} ) )
812 { #If it's an array, we want to push the value
813 push @{ $args{$tag} }, $value;
815 else { #if there's nothing there, just set the value
816 $args{$tag} = $value;
818 } elsif ($line =~ /^$/) {
820 #TODO: this won't work, since "" isn't of the form "foo:value"
822 while ( defined( my $l = shift @lines ) ) {
823 push @{ $args{'content'} }, $l;
829 foreach my $date (qw(due starts started resolved)) {
830 my $dateobj = RT::Date->new(RT->SystemUser);
831 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
832 $dateobj->Set( Format => 'unix', Value => $args{$date} );
835 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
837 $args{$date} = $dateobj->ISO;
839 $args{'mimeobj'} = MIME::Entity->new();
840 $args{'mimeobj'}->build(
841 Type => ( $args{'contenttype'} || 'text/plain' ),
842 Data => ($args{'content'} || '')
850 =head2 Import PARAMHASH
853 Doesn\'t create a transaction.
854 Doesn\'t supply queue defaults, etc.
862 my ( $ErrStr, $QueueObj, $Owner );
866 EffectiveId => undef,
870 Owner => RT->Nobody->Id,
871 Subject => '[no subject]',
872 InitialPriority => undef,
873 FinalPriority => undef,
884 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
885 $QueueObj = RT::Queue->new(RT->SystemUser);
886 $QueueObj->Load( $args{'Queue'} );
888 #TODO error check this and return 0 if it\'s not loading properly +++
890 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
891 $QueueObj = RT::Queue->new(RT->SystemUser);
892 $QueueObj->Load( $args{'Queue'}->Id );
896 "$self " . $args{'Queue'} . " not a recognised queue object." );
899 #Can't create a ticket without a queue.
900 unless ( defined($QueueObj) and $QueueObj->Id ) {
901 $RT::Logger->debug("$self No queue given for ticket creation.");
902 return ( 0, $self->loc('Could not create ticket. Queue not set') );
905 #Now that we have a queue, Check the ACLS
907 $self->CurrentUser->HasRight(
908 Right => 'CreateTicket',
914 $self->loc("No permission to create tickets in the queue '[_1]'"
918 # Deal with setting the owner
920 # Attempt to take user object, user name or user id.
921 # Assign to nobody if lookup fails.
922 if ( defined( $args{'Owner'} ) ) {
923 if ( ref( $args{'Owner'} ) ) {
924 $Owner = $args{'Owner'};
927 $Owner = RT::User->new( $self->CurrentUser );
928 $Owner->Load( $args{'Owner'} );
929 if ( !defined( $Owner->id ) ) {
930 $Owner->Load( RT->Nobody->id );
935 #If we have a proposed owner and they don't have the right
936 #to own a ticket, scream about it and make them not the owner
939 and ( $Owner->Id != RT->Nobody->Id )
949 $RT::Logger->warning( "$self user "
953 . "as a ticket owner but has no rights to own "
955 . $QueueObj->Name . "'" );
960 #If we haven't been handed a valid owner, make it nobody.
961 unless ( defined($Owner) ) {
962 $Owner = RT::User->new( $self->CurrentUser );
963 $Owner->Load( RT->Nobody->UserObj->Id );
968 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
969 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
972 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
973 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
974 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
975 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
977 # If we're coming in with an id, set that now.
978 my $EffectiveId = undef;
980 $EffectiveId = $args{'id'};
984 my $id = $self->SUPER::Create(
986 EffectiveId => $EffectiveId,
987 Queue => $QueueObj->Id,
989 Subject => $args{'Subject'}, # loc
990 InitialPriority => $args{'InitialPriority'}, # loc
991 FinalPriority => $args{'FinalPriority'}, # loc
992 Priority => $args{'InitialPriority'}, # loc
993 Status => $args{'Status'}, # loc
994 TimeWorked => $args{'TimeWorked'}, # loc
995 Type => $args{'Type'}, # loc
996 Created => $args{'Created'}, # loc
997 Told => $args{'Told'}, # loc
998 LastUpdated => $args{'Updated'}, # loc
999 Resolved => $args{'Resolved'}, # loc
1000 Due => $args{'Due'}, # loc
1003 # If the ticket didn't have an id
1004 # Set the ticket's effective ID now that we've created it.
1005 if ( $args{'id'} ) {
1006 $self->Load( $args{'id'} );
1010 $self->__Set( Field => 'EffectiveId', Value => $id );
1014 $self . "->Import couldn't set EffectiveId: $msg" );
1018 my $create_groups_ret = $self->_CreateTicketGroups();
1019 unless ($create_groups_ret) {
1021 "Couldn't create ticket groups for ticket " . $self->Id );
1024 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1026 foreach my $watcher ( @{ $args{'Cc'} } ) {
1027 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1029 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
1030 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1033 foreach my $watcher ( @{ $args{'Requestor'} } ) {
1034 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1038 return ( $self->Id, $ErrStr );
1044 =head2 _CreateTicketGroups
1046 Create the ticket groups and links for this ticket.
1047 This routine expects to be called from Ticket->Create _inside of a transaction_
1049 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1051 It will return true on success and undef on failure.
1057 sub _CreateTicketGroups {
1060 my @types = (qw(Requestor Owner Cc AdminCc));
1062 foreach my $type (@types) {
1063 my $type_obj = RT::Group->new($self->CurrentUser);
1064 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1065 Instance => $self->Id,
1068 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1069 $self->Id.": ".$msg);
1081 A constructor which returns an RT::Group object containing the owner of this ticket.
1087 my $owner_obj = RT::Group->new($self->CurrentUser);
1088 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1089 return ($owner_obj);
1097 AddWatcher takes a parameter hash. The keys are as follows:
1099 Type One of Requestor, Cc, AdminCc
1101 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1103 Email The email address of the new watcher. If a user with this
1104 email address can't be found, a new nonprivileged user will be created.
1106 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.
1114 PrincipalId => undef,
1119 # ModifyTicket works in any case
1120 return $self->_AddWatcher( %args )
1121 if $self->CurrentUserHasRight('ModifyTicket');
1122 if ( $args{'Email'} ) {
1123 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1124 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1127 if ( lc $self->CurrentUser->EmailAddress
1128 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1130 $args{'PrincipalId'} = $self->CurrentUser->id;
1131 delete $args{'Email'};
1135 # If the watcher isn't the current user then the current user has no right
1137 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1138 return ( 0, $self->loc("Permission Denied") );
1141 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1142 if ( $args{'Type'} eq 'AdminCc' ) {
1143 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1144 return ( 0, $self->loc('Permission Denied') );
1148 # If it's a Requestor or Cc and they don't have 'Watch', bail
1149 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1150 unless ( $self->CurrentUserHasRight('Watch') ) {
1151 return ( 0, $self->loc('Permission Denied') );
1155 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1156 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1159 return $self->_AddWatcher( %args );
1162 #This contains the meat of AddWatcher. but can be called from a routine like
1163 # Create, which doesn't need the additional acl check
1169 PrincipalId => undef,
1175 my $principal = RT::Principal->new($self->CurrentUser);
1176 if ($args{'Email'}) {
1177 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1178 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'})));
1180 my $user = RT::User->new(RT->SystemUser);
1181 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1182 $args{'PrincipalId'} = $pid if $pid;
1184 if ($args{'PrincipalId'}) {
1185 $principal->Load($args{'PrincipalId'});
1186 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1187 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'})))
1188 if RT::EmailParser->IsRTAddress( $email );
1194 # If we can't find this watcher, we need to bail.
1195 unless ($principal->Id) {
1196 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1197 return(0, $self->loc("Could not find or create that user"));
1201 my $group = RT::Group->new($self->CurrentUser);
1202 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1203 unless ($group->id) {
1204 return(0,$self->loc("Group not found"));
1207 if ( $group->HasMember( $principal)) {
1209 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1213 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1214 InsideTransaction => 1 );
1216 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1218 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1221 unless ( $args{'Silent'} ) {
1222 $self->_NewTransaction(
1223 Type => 'AddWatcher',
1224 NewValue => $principal->Id,
1225 Field => $args{'Type'}
1229 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1235 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1238 Deletes a Ticket watcher. Takes two arguments:
1240 Type (one of Requestor,Cc,AdminCc)
1244 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1246 Email (the email address of an existing wathcer)
1255 my %args = ( Type => undef,
1256 PrincipalId => undef,
1260 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1261 return ( 0, $self->loc("No principal specified") );
1263 my $principal = RT::Principal->new( $self->CurrentUser );
1264 if ( $args{'PrincipalId'} ) {
1266 $principal->Load( $args{'PrincipalId'} );
1269 my $user = RT::User->new( $self->CurrentUser );
1270 $user->LoadByEmail( $args{'Email'} );
1271 $principal->Load( $user->Id );
1274 # If we can't find this watcher, we need to bail.
1275 unless ( $principal->Id ) {
1276 return ( 0, $self->loc("Could not find that principal") );
1279 my $group = RT::Group->new( $self->CurrentUser );
1280 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1281 unless ( $group->id ) {
1282 return ( 0, $self->loc("Group not found") );
1286 #If the watcher we're trying to add is for the current user
1287 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1289 # If it's an AdminCc and they don't have
1290 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1291 if ( $args{'Type'} eq 'AdminCc' ) {
1292 unless ( $self->CurrentUserHasRight('ModifyTicket')
1293 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1294 return ( 0, $self->loc('Permission Denied') );
1298 # If it's a Requestor or Cc and they don't have
1299 # 'Watch' or 'ModifyTicket', bail
1300 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1302 unless ( $self->CurrentUserHasRight('ModifyTicket')
1303 or $self->CurrentUserHasRight('Watch') ) {
1304 return ( 0, $self->loc('Permission Denied') );
1308 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1310 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1314 # If the watcher isn't the current user
1315 # and the current user doesn't have 'ModifyTicket' bail
1317 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1318 return ( 0, $self->loc("Permission Denied") );
1324 # see if this user is already a watcher.
1326 unless ( $group->HasMember($principal) ) {
1328 $self->loc( 'That principal is not a [_1] for this ticket',
1332 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1334 $RT::Logger->error( "Failed to delete "
1336 . " as a member of group "
1342 'Could not remove that principal as a [_1] for this ticket',
1346 unless ( $args{'Silent'} ) {
1347 $self->_NewTransaction( Type => 'DelWatcher',
1348 OldValue => $principal->Id,
1349 Field => $args{'Type'} );
1353 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1354 $principal->Object->Name,
1362 =head2 SquelchMailTo [EMAIL]
1364 Takes an optional email address to never email about updates to this ticket.
1367 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1375 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1379 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1384 return $self->_SquelchMailTo(@_);
1387 sub _SquelchMailTo {
1391 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1392 unless grep { $_->Content eq $attr }
1393 $self->Attributes->Named('SquelchMailTo');
1395 my @attributes = $self->Attributes->Named('SquelchMailTo');
1396 return (@attributes);
1400 =head2 UnsquelchMailTo ADDRESS
1402 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1404 Returns a tuple of (status, message)
1408 sub UnsquelchMailTo {
1411 my $address = shift;
1412 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1413 return ( 0, $self->loc("Permission Denied") );
1416 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1417 return ($val, $msg);
1422 =head2 RequestorAddresses
1424 B<Returns> String: All Ticket Requestor email addresses as a string.
1428 sub RequestorAddresses {
1431 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1435 return ( $self->Requestors->MemberEmailAddressesAsString );
1439 =head2 AdminCcAddresses
1441 returns String: All Ticket AdminCc email addresses as a string
1445 sub AdminCcAddresses {
1448 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1452 return ( $self->AdminCc->MemberEmailAddressesAsString )
1458 returns String: All Ticket Ccs as a string of email addresses
1465 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1468 return ( $self->Cc->MemberEmailAddressesAsString);
1478 Returns this ticket's Requestors as an RT::Group object
1485 my $group = RT::Group->new($self->CurrentUser);
1486 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1487 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1495 Private non-ACLed variant of Reqeustors so that we can look them up for the
1496 purposes of customer auto-association during create.
1503 my $group = RT::Group->new($RT::SystemUser);
1504 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1511 Returns an RT::Group object which contains this ticket's Ccs.
1512 If the user doesn't have "ShowTicket" permission, returns an empty group
1519 my $group = RT::Group->new($self->CurrentUser);
1520 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1521 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1532 Returns an RT::Group object which contains this ticket's AdminCcs.
1533 If the user doesn't have "ShowTicket" permission, returns an empty group
1540 my $group = RT::Group->new($self->CurrentUser);
1541 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1542 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1551 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1553 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1555 Takes a param hash with the attributes Type and either PrincipalId or Email
1557 Type is one of Requestor, Cc, AdminCc and Owner
1559 PrincipalId is an RT::Principal id, and Email is an email address.
1561 Returns true if the specified principal (or the one corresponding to the
1562 specified address) is a member of the group Type for this ticket.
1564 XX TODO: This should be Memoized.
1571 my %args = ( Type => 'Requestor',
1572 PrincipalId => undef,
1577 # Load the relevant group.
1578 my $group = RT::Group->new($self->CurrentUser);
1579 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1581 # Find the relevant principal.
1582 if (!$args{PrincipalId} && $args{Email}) {
1583 # Look up the specified user.
1584 my $user = RT::User->new($self->CurrentUser);
1585 $user->LoadByEmail($args{Email});
1587 $args{PrincipalId} = $user->PrincipalId;
1590 # A non-existent user can't be a group member.
1595 # Ask if it has the member in question
1596 return $group->HasMember( $args{'PrincipalId'} );
1601 =head2 IsRequestor PRINCIPAL_ID
1603 Takes an L<RT::Principal> id.
1605 Returns true if the principal is a requestor of the current ticket.
1613 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1619 =head2 IsCc PRINCIPAL_ID
1621 Takes an RT::Principal id.
1622 Returns true if the principal is a Cc of the current ticket.
1631 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1637 =head2 IsAdminCc PRINCIPAL_ID
1639 Takes an RT::Principal id.
1640 Returns true if the principal is an AdminCc of the current ticket.
1648 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1656 Takes an RT::User object. Returns true if that user is this ticket's owner.
1657 returns undef otherwise
1665 # no ACL check since this is used in acl decisions
1666 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1670 #Tickets won't yet have owners when they're being created.
1671 unless ( $self->OwnerObj->id ) {
1675 if ( $person->id == $self->OwnerObj->id ) {
1687 =head2 TransactionAddresses
1689 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1690 all this ticket's Create, Comment or Correspond transactions. The keys are
1691 stringified email addresses. Each value is an L<Email::Address> object.
1693 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.
1698 sub TransactionAddresses {
1700 my $txns = $self->Transactions;
1704 my $attachments = RT::Attachments->new( $self->CurrentUser );
1705 $attachments->LimitByTicket( $self->id );
1706 $attachments->Columns( qw( id Headers TransactionId));
1709 foreach my $type (qw(Create Comment Correspond)) {
1710 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1714 ENTRYAGGREGATOR => 'OR',
1719 while ( my $att = $attachments->Next ) {
1720 foreach my $addrlist ( values %{$att->Addresses } ) {
1721 foreach my $addr (@$addrlist) {
1723 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1725 if ( $addresses{ $addr->address }
1726 && $addresses{ $addr->address }->phrase
1727 && not $addr->phrase );
1729 # skips "comment-only" addresses
1730 next unless ( $addr->address );
1731 $addresses{ $addr->address } = $addr;
1750 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1754 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1755 my $id = $QueueObj->Load($Value);
1769 my $NewQueue = shift;
1771 #Redundant. ACL gets checked in _Set;
1772 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1773 return ( 0, $self->loc("Permission Denied") );
1776 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1777 $NewQueueObj->Load($NewQueue);
1779 unless ( $NewQueueObj->Id() ) {
1780 return ( 0, $self->loc("That queue does not exist") );
1783 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1784 return ( 0, $self->loc('That is the same value') );
1786 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1787 return ( 0, $self->loc("You may not create requests in that queue.") );
1791 my $old_lifecycle = $self->QueueObj->Lifecycle;
1792 my $new_lifecycle = $NewQueueObj->Lifecycle;
1793 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1794 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1795 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1797 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1798 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1802 if ( $new_status ) {
1803 my $clone = RT::Ticket->new( RT->SystemUser );
1804 $clone->Load( $self->Id );
1805 unless ( $clone->Id ) {
1806 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1809 my $now = RT::Date->new( $self->CurrentUser );
1812 my $old_status = $clone->Status;
1814 #If we're changing the status from initial in old to not intial in new,
1815 # record that we've started
1816 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1817 #Set the Started time to "now"
1821 RecordTransaction => 0
1825 #When we close a ticket, set the 'Resolved' attribute to now.
1826 # It's misnamed, but that's just historical.
1827 if ( $new_lifecycle->IsInactive($new_status) ) {
1829 Field => 'Resolved',
1831 RecordTransaction => 0,
1835 #Actually update the status
1836 my ($val, $msg)= $clone->_Set(
1838 Value => $new_status,
1839 RecordTransaction => 0,
1841 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1845 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1848 # Clear the queue object cache;
1849 $self->{_queue_obj} = undef;
1851 # Untake the ticket if we have no permissions in the new queue
1852 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1853 my $clone = RT::Ticket->new( RT->SystemUser );
1854 $clone->Load( $self->Id );
1855 unless ( $clone->Id ) {
1856 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1858 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1859 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1862 # On queue change, change queue for reminders too
1863 my $reminder_collection = $self->Reminders->Collection;
1864 while ( my $reminder = $reminder_collection->Next ) {
1865 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1866 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1870 return ($status, $msg);
1877 Takes nothing. returns this ticket's queue object
1884 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1886 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1888 #We call __Value so that we can avoid the ACL decision and some deep recursion
1889 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1891 return ($self->{_queue_obj});
1896 Takes nothing. Returns SubjectTag for this ticket. Includes
1897 queue's subject tag or rtname if that is not set, ticket
1898 id and braces, for example:
1900 [support.example.com #123456]
1908 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1917 Returns an RT::Date object containing this ticket's due date
1924 my $time = RT::Date->new( $self->CurrentUser );
1926 # -1 is RT::Date slang for never
1927 if ( my $due = $self->Due ) {
1928 $time->Set( Format => 'sql', Value => $due );
1931 $time->Set( Format => 'unix', Value => -1 );
1941 Returns this ticket's due date as a human readable string
1947 return $self->DueObj->AsString();
1954 Returns an RT::Date object of this ticket's 'resolved' time.
1961 my $time = RT::Date->new( $self->CurrentUser );
1962 $time->Set( Format => 'sql', Value => $self->Resolved );
1967 =head2 FirstActiveStatus
1969 Returns the first active status that the ticket could transition to,
1970 according to its current Queue's lifecycle. May return undef if there
1971 is no such possible status to transition to, or we are already in it.
1972 This is used in L<RT::Action::AutoOpen>, for instance.
1976 sub FirstActiveStatus {
1979 my $lifecycle = $self->QueueObj->Lifecycle;
1980 my $status = $self->Status;
1981 my @active = $lifecycle->Active;
1982 # no change if no active statuses in the lifecycle
1983 return undef unless @active;
1985 # no change if the ticket is already has first status from the list of active
1986 return undef if lc $status eq lc $active[0];
1988 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1992 =head2 FirstInactiveStatus
1994 Returns the first inactive 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 resolve action in UnsafeEmailCommands, for instance.
2001 sub FirstInactiveStatus {
2004 my $lifecycle = $self->QueueObj->Lifecycle;
2005 my $status = $self->Status;
2006 my @inactive = $lifecycle->Inactive;
2007 # no change if no inactive statuses in the lifecycle
2008 return undef unless @inactive;
2010 # no change if the ticket is already has first status from the list of inactive
2011 return undef if lc $status eq lc $inactive[0];
2013 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
2019 Takes a date in ISO format or undef
2020 Returns a transaction id and a message
2021 The client calls "Start" to note that the project was started on the date in $date.
2022 A null date means "now"
2028 my $time = shift || 0;
2030 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2031 return ( 0, $self->loc("Permission Denied") );
2034 #We create a date object to catch date weirdness
2035 my $time_obj = RT::Date->new( $self->CurrentUser() );
2037 $time_obj->Set( Format => 'ISO', Value => $time );
2040 $time_obj->SetToNow();
2043 # We need $TicketAsSystem, in case the current user doesn't have
2045 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
2046 $TicketAsSystem->Load( $self->Id );
2047 # Now that we're starting, open this ticket
2048 # TODO: do we really want to force this as policy? it should be a scrip
2049 my $next = $TicketAsSystem->FirstActiveStatus;
2051 $self->SetStatus( $next ) if defined $next;
2053 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2061 Returns an RT::Date object which contains this ticket's
2069 my $time = RT::Date->new( $self->CurrentUser );
2070 $time->Set( Format => 'sql', Value => $self->Started );
2078 Returns an RT::Date object which contains this ticket's
2086 my $time = RT::Date->new( $self->CurrentUser );
2087 $time->Set( Format => 'sql', Value => $self->Starts );
2095 Returns an RT::Date object which contains this ticket's
2103 my $time = RT::Date->new( $self->CurrentUser );
2104 $time->Set( Format => 'sql', Value => $self->Told );
2112 A convenience method that returns ToldObj->AsString
2114 TODO: This should be deprecated
2120 if ( $self->Told ) {
2121 return $self->ToldObj->AsString();
2130 =head2 TimeWorkedAsString
2132 Returns the amount of time worked on this ticket as a Text String
2136 sub TimeWorkedAsString {
2138 my $value = $self->TimeWorked;
2140 # return the # of minutes worked turned into seconds and written as
2141 # a simple text string, this is not really a date object, but if we
2142 # diff a number of seconds vs the epoch, we'll get a nice description
2144 return "" unless $value;
2145 return RT::Date->new( $self->CurrentUser )
2146 ->DurationAsString( $value * 60 );
2151 =head2 TimeLeftAsString
2153 Returns the amount of time left on this ticket as a Text String
2157 sub TimeLeftAsString {
2159 my $value = $self->TimeLeft;
2160 return "" unless $value;
2161 return RT::Date->new( $self->CurrentUser )
2162 ->DurationAsString( $value * 60 );
2170 Comment on this ticket.
2171 Takes a hash with the following attributes:
2172 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2175 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2177 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2178 They will, however, be prepared and you'll be able to access them through the TransactionObj
2180 Returns: Transaction id, Error Message, Transaction Object
2181 (note the different order from Create()!)
2188 my %args = ( CcMessageTo => undef,
2189 BccMessageTo => undef,
2196 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2197 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2198 return ( 0, $self->loc("Permission Denied"), undef );
2200 $args{'NoteType'} = 'Comment';
2202 $RT::Handle->BeginTransaction();
2203 if ($args{'DryRun'}) {
2204 $args{'CommitScrips'} = 0;
2207 my @results = $self->_RecordNote(%args);
2208 if ($args{'DryRun'}) {
2209 $RT::Handle->Rollback();
2211 $RT::Handle->Commit();
2220 Correspond on this ticket.
2221 Takes a hashref with the following attributes:
2224 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2226 if there's no MIMEObj, Content is used to build a MIME::Entity object
2228 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2229 They will, however, be prepared and you'll be able to access them through the TransactionObj
2231 Returns: Transaction id, Error Message, Transaction Object
2232 (note the different order from Create()!)
2239 my %args = ( CcMessageTo => undef,
2240 BccMessageTo => undef,
2246 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2247 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2248 return ( 0, $self->loc("Permission Denied"), undef );
2250 $args{'NoteType'} = 'Correspond';
2252 $RT::Handle->BeginTransaction();
2253 if ($args{'DryRun'}) {
2254 $args{'CommitScrips'} = 0;
2257 my @results = $self->_RecordNote(%args);
2259 #Set the last told date to now if this isn't mail from the requestor.
2260 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2261 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2263 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2265 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2268 if ($args{'DryRun'}) {
2269 $RT::Handle->Rollback();
2271 $RT::Handle->Commit();
2282 the meat of both comment and correspond.
2284 Performs no access control checks. hence, dangerous.
2291 CcMessageTo => undef,
2292 BccMessageTo => undef,
2297 NoteType => 'Correspond',
2300 SquelchMailTo => undef,
2305 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2306 return ( 0, $self->loc("No message attached"), undef );
2309 unless ( $args{'MIMEObj'} ) {
2310 $args{'MIMEObj'} = MIME::Entity->build(
2311 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2315 # convert text parts into utf-8
2316 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2318 # If we've been passed in CcMessageTo and BccMessageTo fields,
2319 # add them to the mime object for passing on to the transaction handler
2320 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2321 # RT-Send-Bcc: headers
2324 foreach my $type (qw/Cc Bcc/) {
2325 if ( defined $args{ $type . 'MessageTo' } ) {
2327 my $addresses = join ', ', (
2328 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2329 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2330 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2334 foreach my $argument (qw(Encrypt Sign)) {
2335 $args{'MIMEObj'}->head->replace(
2336 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2337 ) if defined $args{ $argument };
2340 # If this is from an external source, we need to come up with its
2341 # internal Message-ID now, so all emails sent because of this
2342 # message have a common Message-ID
2343 my $org = RT->Config->Get('Organization');
2344 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2345 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2346 $args{'MIMEObj'}->head->set(
2347 'RT-Message-ID' => Encode::encode_utf8(
2348 RT::Interface::Email::GenMessageId( Ticket => $self )
2353 #Record the correspondence (write the transaction)
2354 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2355 Type => $args{'NoteType'},
2356 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2357 TimeTaken => $args{'TimeTaken'},
2358 MIMEObj => $args{'MIMEObj'},
2359 CommitScrips => $args{'CommitScrips'},
2360 SquelchMailTo => $args{'SquelchMailTo'},
2361 CustomFields => $args{'CustomFields'},
2365 $RT::Logger->err("$self couldn't init a transaction $msg");
2366 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2369 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2375 Builds a MIME object from the given C<UpdateSubject> and
2376 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2377 C<< DryRun => 1 >>, and returns the transaction so produced.
2385 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2386 $action = 'Correspond';
2388 $action = 'Comment';
2391 my $Message = MIME::Entity->build(
2392 Type => 'text/plain',
2393 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2395 Data => $args{'UpdateContent'} || "",
2398 my ( $Transaction, $Description, $Object ) = $self->$action(
2399 CcMessageTo => $args{'UpdateCc'},
2400 BccMessageTo => $args{'UpdateBcc'},
2401 MIMEObj => $Message,
2402 TimeTaken => $args{'UpdateTimeWorked'},
2405 unless ( $Transaction ) {
2406 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2414 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2415 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2416 the resulting L<RT::Transaction>.
2423 my $Message = MIME::Entity->build(
2424 Type => 'text/plain',
2425 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2426 (defined $args{'Cc'} ?
2427 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2429 Data => $args{'Content'} || "",
2432 my ( $Transaction, $Object, $Description ) = $self->Create(
2433 Type => $args{'Type'} || 'ticket',
2434 Queue => $args{'Queue'},
2435 Owner => $args{'Owner'},
2436 Requestor => $args{'Requestors'},
2438 AdminCc => $args{'AdminCc'},
2439 InitialPriority => $args{'InitialPriority'},
2440 FinalPriority => $args{'FinalPriority'},
2441 TimeLeft => $args{'TimeLeft'},
2442 TimeEstimated => $args{'TimeEstimated'},
2443 TimeWorked => $args{'TimeWorked'},
2444 Subject => $args{'Subject'},
2445 Status => $args{'Status'},
2446 MIMEObj => $Message,
2449 unless ( $Transaction ) {
2450 $RT::Logger->error("Couldn't fire Create action: $Description");
2461 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2464 my $type = shift || "";
2466 my $cache_key = "$field$type";
2467 return $self->{ $cache_key } if $self->{ $cache_key };
2469 my $links = $self->{ $cache_key }
2470 = RT::Links->new( $self->CurrentUser );
2471 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2472 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2476 # Maybe this ticket is a merge ticket
2477 #my $limit_on = 'Local'. $field;
2478 # at least to myself
2480 FIELD => $field, #$limit_on,
2481 OPERATOR => 'MATCHES',
2482 VALUE => 'fsck.com-rt://%/ticket/'. $self->id,
2483 ENTRYAGGREGATOR => 'OR',
2486 FIELD => $field, #$limit_on,
2487 OPERATOR => 'MATCHES',
2488 VALUE => 'fsck.com-rt://%/ticket/'. $_,
2489 ENTRYAGGREGATOR => 'OR',
2490 ) foreach $self->Merged;
2503 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2504 SilentBase and SilentTarget. Either Base or Target must be null.
2505 The null value will be replaced with this ticket\'s id.
2507 If Silent is true then no transaction would be recorded, in other
2508 case you can control creation of transactions on both base and
2509 target with SilentBase and SilentTarget respectively. By default
2510 both transactions are created.
2521 SilentBase => undef,
2522 SilentTarget => undef,
2526 unless ( $args{'Target'} || $args{'Base'} ) {
2527 $RT::Logger->error("Base or Target must be specified");
2528 return ( 0, $self->loc('Either base or target must be specified') );
2533 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2534 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2535 return ( 0, $self->loc("Permission Denied") );
2538 # If the other URI is an RT::Ticket, we want to make sure the user
2539 # can modify it too...
2540 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2541 return (0, $msg) unless $status;
2542 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2545 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2546 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2548 return ( 0, $self->loc("Permission Denied") );
2551 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2552 return ( 0, $Msg ) unless $val;
2554 return ( $val, $Msg ) if $args{'Silent'};
2556 my ($direction, $remote_link);
2558 if ( $args{'Base'} ) {
2559 $remote_link = $args{'Base'};
2560 $direction = 'Target';
2562 elsif ( $args{'Target'} ) {
2563 $remote_link = $args{'Target'};
2564 $direction = 'Base';
2567 my $remote_uri = RT::URI->new( $self->CurrentUser );
2568 $remote_uri->FromURI( $remote_link );
2570 unless ( $args{ 'Silent'. $direction } ) {
2571 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2572 Type => 'DeleteLink',
2573 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2574 OldValue => $remote_uri->URI || $remote_link,
2577 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2580 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2581 my $OtherObj = $remote_uri->Object;
2582 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2583 Type => 'DeleteLink',
2584 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2585 : $LINKDIRMAP{$args{'Type'}}->{Target},
2586 OldValue => $self->URI,
2587 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2590 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2593 return ( $val, $Msg );
2600 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2602 If Silent is true then no transaction would be recorded, in other
2603 case you can control creation of transactions on both base and
2604 target with SilentBase and SilentTarget respectively. By default
2605 both transactions are created.
2611 my %args = ( Target => '',
2615 SilentBase => undef,
2616 SilentTarget => undef,
2619 unless ( $args{'Target'} || $args{'Base'} ) {
2620 $RT::Logger->error("Base or Target must be specified");
2621 return ( 0, $self->loc('Either base or target must be specified') );
2625 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2626 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2627 return ( 0, $self->loc("Permission Denied") );
2630 # If the other URI is an RT::Ticket, we want to make sure the user
2631 # can modify it too...
2632 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2633 return (0, $msg) unless $status;
2634 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2637 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2638 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2640 return ( 0, $self->loc("Permission Denied") );
2643 return ( 0, "Can't link to a deleted ticket" )
2644 if $other_ticket && $other_ticket->Status eq 'deleted';
2646 return $self->_AddLink(%args);
2649 sub __GetTicketFromURI {
2651 my %args = ( URI => '', @_ );
2653 # If the other URI is an RT::Ticket, we want to make sure the user
2654 # can modify it too...
2655 my $uri_obj = RT::URI->new( $self->CurrentUser );
2656 $uri_obj->FromURI( $args{'URI'} );
2658 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2659 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2660 $RT::Logger->warning( $msg );
2663 my $obj = $uri_obj->Resolver->Object;
2664 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2665 return (1, 'Found not a ticket', undef);
2667 return (1, 'Found ticket', $obj);
2672 Private non-acled variant of AddLink so that links can be added during create.
2678 my %args = ( Target => '',
2682 SilentBase => undef,
2683 SilentTarget => undef,
2686 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2687 return ($val, $msg) if !$val || $exist;
2688 return ($val, $msg) if $args{'Silent'};
2690 my ($direction, $remote_link);
2691 if ( $args{'Target'} ) {
2692 $remote_link = $args{'Target'};
2693 $direction = 'Base';
2694 } elsif ( $args{'Base'} ) {
2695 $remote_link = $args{'Base'};
2696 $direction = 'Target';
2699 my $remote_uri = RT::URI->new( $self->CurrentUser );
2700 $remote_uri->FromURI( $remote_link );
2702 unless ( $args{ 'Silent'. $direction } ) {
2703 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2705 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2706 NewValue => $remote_uri->URI || $remote_link,
2709 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2712 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2713 my $OtherObj = $remote_uri->Object;
2714 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2716 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2717 : $LINKDIRMAP{$args{'Type'}}->{Target},
2718 NewValue => $self->URI,
2719 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2722 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2725 return ( $val, $msg );
2733 MergeInto take the id of the ticket to merge this ticket into.
2739 my $ticket_id = shift;
2741 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2742 return ( 0, $self->loc("Permission Denied") );
2745 # Load up the new ticket.
2746 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2747 $MergeInto->Load($ticket_id);
2749 # make sure it exists.
2750 unless ( $MergeInto->Id ) {
2751 return ( 0, $self->loc("New ticket doesn't exist") );
2754 # Make sure the current user can modify the new ticket.
2755 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2756 return ( 0, $self->loc("Permission Denied") );
2759 delete $MERGE_CACHE{'effective'}{ $self->id };
2760 delete @{ $MERGE_CACHE{'merged'} }{
2761 $ticket_id, $MergeInto->id, $self->id
2764 $RT::Handle->BeginTransaction();
2766 $self->_MergeInto( $MergeInto );
2768 $RT::Handle->Commit();
2770 return ( 1, $self->loc("Merge Successful") );
2775 my $MergeInto = shift;
2778 # We use EffectiveId here even though it duplicates information from
2779 # the links table becasue of the massive performance hit we'd take
2780 # by trying to do a separate database query for merge info everytime
2783 #update this ticket's effective id to the new ticket's id.
2784 my ( $id_val, $id_msg ) = $self->__Set(
2785 Field => 'EffectiveId',
2786 Value => $MergeInto->Id()
2790 $RT::Handle->Rollback();
2791 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2795 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2796 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2797 my ( $status_val, $status_msg )
2798 = $self->__Set( Field => 'Status', Value => $force_status );
2800 unless ($status_val) {
2801 $RT::Handle->Rollback();
2803 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2805 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2809 # update all the links that point to that old ticket
2810 my $old_links_to = RT::Links->new($self->CurrentUser);
2811 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2814 while (my $link = $old_links_to->Next) {
2815 if (exists $old_seen{$link->Base."-".$link->Type}) {
2818 elsif ($link->Base eq $MergeInto->URI) {
2821 # First, make sure the link doesn't already exist. then move it over.
2822 my $tmp = RT::Link->new(RT->SystemUser);
2823 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2827 $link->SetTarget($MergeInto->URI);
2828 $link->SetLocalTarget($MergeInto->id);
2830 $old_seen{$link->Base."-".$link->Type} =1;
2835 my $old_links_from = RT::Links->new($self->CurrentUser);
2836 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2838 while (my $link = $old_links_from->Next) {
2839 if (exists $old_seen{$link->Type."-".$link->Target}) {
2842 if ($link->Target eq $MergeInto->URI) {
2845 # First, make sure the link doesn't already exist. then move it over.
2846 my $tmp = RT::Link->new(RT->SystemUser);
2847 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2851 $link->SetBase($MergeInto->URI);
2852 $link->SetLocalBase($MergeInto->id);
2853 $old_seen{$link->Type."-".$link->Target} =1;
2859 # Update time fields
2860 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2862 my $mutator = "Set$type";
2863 $MergeInto->$mutator(
2864 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2867 #add all of this ticket's watchers to that ticket.
2868 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2870 my $people = $self->$watcher_type->MembersObj;
2871 my $addwatcher_type = $watcher_type;
2872 $addwatcher_type =~ s/s$//;
2874 while ( my $watcher = $people->Next ) {
2876 my ($val, $msg) = $MergeInto->_AddWatcher(
2877 Type => $addwatcher_type,
2879 PrincipalId => $watcher->MemberId
2882 $RT::Logger->debug($msg);
2888 #find all of the tickets that were merged into this ticket.
2889 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2890 $old_mergees->Limit(
2891 FIELD => 'EffectiveId',
2896 # update their EffectiveId fields to the new ticket's id
2897 while ( my $ticket = $old_mergees->Next() ) {
2898 my ( $val, $msg ) = $ticket->__Set(
2899 Field => 'EffectiveId',
2900 Value => $MergeInto->Id()
2904 #make a new link: this ticket is merged into that other ticket.
2905 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2907 $MergeInto->_SetLastUpdated;
2912 Returns list of tickets' ids that's been merged into this ticket.
2920 return @{ $MERGE_CACHE{'merged'}{ $id } }
2921 if $MERGE_CACHE{'merged'}{ $id };
2923 my $mergees = RT::Tickets->new( $self->CurrentUser );
2925 FIELD => 'EffectiveId',
2933 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2934 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2943 Takes nothing and returns an RT::User object of
2951 #If this gets ACLed, we lose on a rights check in User.pm and
2952 #get deep recursion. if we need ACLs here, we need
2953 #an equiv without ACLs
2955 my $owner = RT::User->new( $self->CurrentUser );
2956 $owner->Load( $self->__Value('Owner') );
2958 #Return the owner object
2964 =head2 OwnerAsString
2966 Returns the owner's email address
2972 return ( $self->OwnerObj->EmailAddress );
2980 Takes two arguments:
2981 the Id or Name of the owner
2982 and (optionally) the type of the SetOwner Transaction. It defaults
2983 to 'Set'. 'Steal' is also a valid option.
2990 my $NewOwner = shift;
2991 my $Type = shift || "Set";
2993 $RT::Handle->BeginTransaction();
2995 $self->_SetLastUpdated(); # lock the ticket
2996 $self->Load( $self->id ); # in case $self changed while waiting for lock
2998 my $OldOwnerObj = $self->OwnerObj;
3000 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3001 $NewOwnerObj->Load( $NewOwner );
3002 unless ( $NewOwnerObj->Id ) {
3003 $RT::Handle->Rollback();
3004 return ( 0, $self->loc("That user does not exist") );
3008 # must have ModifyTicket rights
3009 # or TakeTicket/StealTicket and $NewOwner is self
3010 # see if it's a take
3011 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
3012 unless ( $self->CurrentUserHasRight('ModifyTicket')
3013 || $self->CurrentUserHasRight('TakeTicket') ) {
3014 $RT::Handle->Rollback();
3015 return ( 0, $self->loc("Permission Denied") );
3019 # see if it's a steal
3020 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
3021 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3023 unless ( $self->CurrentUserHasRight('ModifyTicket')
3024 || $self->CurrentUserHasRight('StealTicket') ) {
3025 $RT::Handle->Rollback();
3026 return ( 0, $self->loc("Permission Denied") );
3030 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3031 $RT::Handle->Rollback();
3032 return ( 0, $self->loc("Permission Denied") );
3036 # If we're not stealing and the ticket has an owner and it's not
3038 if ( $Type ne 'Steal' and $Type ne 'Force'
3039 and $OldOwnerObj->Id != RT->Nobody->Id
3040 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3042 $RT::Handle->Rollback();
3043 return ( 0, $self->loc("You can only take tickets that are unowned") )
3044 if $NewOwnerObj->id == $self->CurrentUser->id;
3047 $self->loc("You can only reassign tickets that you own or that are unowned" )
3051 #If we've specified a new owner and that user can't modify the ticket
3052 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3053 $RT::Handle->Rollback();
3054 return ( 0, $self->loc("That user may not own tickets in that queue") );
3057 # If the ticket has an owner and it's the new owner, we don't need
3059 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3060 $RT::Handle->Rollback();
3061 return ( 0, $self->loc("That user already owns that ticket") );
3064 # Delete the owner in the owner group, then add a new one
3065 # TODO: is this safe? it's not how we really want the API to work
3066 # for most things, but it's fast.
3067 my ( $del_id, $del_msg );
3068 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3069 ($del_id, $del_msg) = $owner->Delete();
3070 last unless ($del_id);
3074 $RT::Handle->Rollback();
3075 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3078 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3079 PrincipalId => $NewOwnerObj->PrincipalId,
3080 InsideTransaction => 1 );
3082 $RT::Handle->Rollback();
3083 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3086 # We call set twice with slightly different arguments, so
3087 # as to not have an SQL transaction span two RT transactions
3089 my ( $val, $msg ) = $self->_Set(
3091 RecordTransaction => 0,
3092 Value => $NewOwnerObj->Id,
3094 TransactionType => 'Set',
3095 CheckACL => 0, # don't check acl
3099 $RT::Handle->Rollback;
3100 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3103 ($val, $msg) = $self->_NewTransaction(
3106 NewValue => $NewOwnerObj->Id,
3107 OldValue => $OldOwnerObj->Id,
3112 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3113 $OldOwnerObj->Name, $NewOwnerObj->Name );
3116 $RT::Handle->Rollback();
3120 $RT::Handle->Commit();
3122 return ( $val, $msg );
3129 A convenince method to set the ticket's owner to the current user
3135 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3142 Convenience method to set the owner to 'nobody' if the current user is the owner.
3148 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3155 A convenience method to change the owner of the current ticket to the
3156 current user. Even if it's owned by another user.
3163 if ( $self->IsOwner( $self->CurrentUser ) ) {
3164 return ( 0, $self->loc("You already own this ticket") );
3167 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3177 =head2 ValidateStatus STATUS
3179 Takes a string. Returns true if that status is a valid status for this ticket.
3180 Returns false otherwise.
3184 sub ValidateStatus {
3188 #Make sure the status passed in is valid
3189 return 1 if $self->QueueObj->IsValidStatus($status);
3192 while ( my $caller = (caller($i++))[3] ) {
3193 return 1 if $caller eq 'RT::Ticket::SetQueue';
3201 =head2 SetStatus STATUS
3203 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3205 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3206 If FORCE is true, ignore unresolved dependencies and force a status change.
3207 if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3208 is not set and the status is changed from initial to not initial.
3216 $args{Status} = shift;
3222 # this only allows us to SetStarted, not we must SetStarted.
3223 # this option was added for rtir initially
3224 $args{SetStarted} = 1 unless exists $args{SetStarted};
3227 my $lifecycle = $self->QueueObj->Lifecycle;
3229 my $new = $args{'Status'};
3230 unless ( $lifecycle->IsValid( $new ) ) {
3231 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3234 my $old = $self->__Value('Status');
3235 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3236 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3239 my $check_right = $lifecycle->CheckRight( $old => $new );
3240 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3241 return ( 0, $self->loc('Permission Denied') );
3244 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3245 return (0, $self->loc('That ticket has unresolved dependencies'));
3248 my $now = RT::Date->new( $self->CurrentUser );
3251 my $raw_started = RT::Date->new(RT->SystemUser);
3252 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3254 #If we're changing the status from new, record that we've started
3255 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3256 #Set the Started time to "now"
3260 RecordTransaction => 0
3264 #When we close a ticket, set the 'Resolved' attribute to now.
3265 # It's misnamed, but that's just historical.
3266 if ( $lifecycle->IsInactive($new) ) {
3268 Field => 'Resolved',
3270 RecordTransaction => 0,
3274 #Actually update the status
3275 my ($val, $msg)= $self->_Set(
3277 Value => $args{Status},
3280 TransactionType => 'Status',
3282 return ($val, $msg);
3289 Takes no arguments. Marks this ticket for garbage collection
3295 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3296 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3298 return ( $self->SetStatus('deleted') );
3302 =head2 SetTold ISO [TIMETAKEN]
3304 Updates the told and records a transaction
3311 $told = shift if (@_);
3312 my $timetaken = shift || 0;
3314 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3315 return ( 0, $self->loc("Permission Denied") );
3318 my $datetold = RT::Date->new( $self->CurrentUser );
3320 $datetold->Set( Format => 'iso',
3324 $datetold->SetToNow();
3327 return ( $self->_Set( Field => 'Told',
3328 Value => $datetold->ISO,
3329 TimeTaken => $timetaken,
3330 TransactionType => 'Told' ) );
3335 Updates the told without a transaction or acl check. Useful when we're sending replies.
3342 my $now = RT::Date->new( $self->CurrentUser );
3345 #use __Set to get no ACLs ;)
3346 return ( $self->__Set( Field => 'Told',
3347 Value => $now->ISO ) );
3357 my $uid = $self->CurrentUser->id;
3358 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3359 return if $attr && $attr->Content gt $self->LastUpdated;
3361 my $txns = $self->Transactions;
3362 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3363 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3364 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3368 VALUE => $attr->Content
3370 $txns->RowsPerPage(1);
3371 return $txns->First;
3374 =head2 RanTransactionBatch
3376 Acts as a guard around running TransactionBatch scrips.
3378 Should be false until you enter the code that runs TransactionBatch scrips
3380 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3384 sub RanTransactionBatch {
3388 if ( defined $val ) {
3389 return $self->{_RanTransactionBatch} = $val;
3391 return $self->{_RanTransactionBatch};
3397 =head2 TransactionBatch
3399 Returns an array reference of all transactions created on this ticket during
3400 this ticket object's lifetime or since last application of a batch, or undef
3403 Only works when the C<UseTransactionBatch> config option is set to true.
3407 sub TransactionBatch {
3409 return $self->{_TransactionBatch};
3412 =head2 ApplyTransactionBatch
3414 Applies scrips on the current batch of transactions and shinks it. Usually
3415 batch is applied when object is destroyed, but in some cases it's too late.
3419 sub ApplyTransactionBatch {
3422 my $batch = $self->TransactionBatch;
3423 return unless $batch && @$batch;
3425 $self->_ApplyTransactionBatch;
3427 $self->{_TransactionBatch} = [];
3430 sub _ApplyTransactionBatch {
3433 return if $self->RanTransactionBatch;
3434 $self->RanTransactionBatch(1);
3436 my $still_exists = RT::Ticket->new( RT->SystemUser );
3437 $still_exists->Load( $self->Id );
3438 if (not $still_exists->Id) {
3439 # The ticket has been removed from the database, but we still
3440 # have pending TransactionBatch txns for it. Unfortunately,
3441 # because it isn't in the DB anymore, attempting to run scrips
3442 # on it may produce unpredictable results; simply drop the
3443 # batched transactions.
3444 $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.");
3448 my $batch = $self->TransactionBatch;
3451 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3454 RT::Scrips->new(RT->SystemUser)->Apply(
3455 Stage => 'TransactionBatch',
3457 TransactionObj => $batch->[0],
3461 # Entry point of the rule system
3462 my $rules = RT::Ruleset->FindAllRules(
3463 Stage => 'TransactionBatch',
3465 TransactionObj => $batch->[0],
3468 RT::Ruleset->CommitRules($rules);
3474 # DESTROY methods need to localize $@, or it may unset it. This
3475 # causes $m->abort to not bubble all of the way up. See perlbug
3476 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3479 # The following line eliminates reentrancy.
3480 # It protects against the fact that perl doesn't deal gracefully
3481 # when an object's refcount is changed in its destructor.
3482 return if $self->{_Destroyed}++;
3484 if (in_global_destruction()) {
3485 unless ($ENV{'HARNESS_ACTIVE'}) {
3486 warn "Too late to safely run transaction-batch scrips!"
3487 ." This is typically caused by using ticket objects"
3488 ." at the top-level of a script which uses the RT API."
3489 ." Be sure to explicitly undef such ticket objects,"
3490 ." or put them inside of a lexical scope.";
3495 return $self->ApplyTransactionBatch;
3501 sub _OverlayAccessible {
3503 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3504 Queue => { 'read' => 1, 'write' => 1 },
3505 Requestors => { 'read' => 1, 'write' => 1 },
3506 Owner => { 'read' => 1, 'write' => 1 },
3507 Subject => { 'read' => 1, 'write' => 1 },
3508 InitialPriority => { 'read' => 1, 'write' => 1 },
3509 FinalPriority => { 'read' => 1, 'write' => 1 },
3510 Priority => { 'read' => 1, 'write' => 1 },
3511 Status => { 'read' => 1, 'write' => 1 },
3512 TimeEstimated => { 'read' => 1, 'write' => 1 },
3513 TimeWorked => { 'read' => 1, 'write' => 1 },
3514 TimeLeft => { 'read' => 1, 'write' => 1 },
3515 Told => { 'read' => 1, 'write' => 1 },
3516 Resolved => { 'read' => 1 },
3517 Type => { 'read' => 1 },
3518 Starts => { 'read' => 1, 'write' => 1 },
3519 Started => { 'read' => 1, 'write' => 1 },
3520 Due => { 'read' => 1, 'write' => 1 },
3521 Creator => { 'read' => 1, 'auto' => 1 },
3522 Created => { 'read' => 1, 'auto' => 1 },
3523 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3524 LastUpdated => { 'read' => 1, 'auto' => 1 }
3534 my %args = ( Field => undef,
3537 RecordTransaction => 1,
3540 TransactionType => 'Set',
3543 if ($args{'CheckACL'}) {
3544 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3545 return ( 0, $self->loc("Permission Denied"));
3549 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3550 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3551 return(0, $self->loc("Internal Error"));
3554 #if the user is trying to modify the record
3556 #Take care of the old value we really don't want to get in an ACL loop.
3557 # so ask the super::_Value
3558 my $Old = $self->SUPER::_Value("$args{'Field'}");
3561 if ( $args{'UpdateTicket'} ) {
3564 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3565 Value => $args{'Value'} );
3567 #If we can't actually set the field to the value, don't record
3568 # a transaction. instead, get out of here.
3569 return ( 0, $msg ) unless $ret;
3572 if ( $args{'RecordTransaction'} == 1 ) {
3574 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3575 Type => $args{'TransactionType'},
3576 Field => $args{'Field'},
3577 NewValue => $args{'Value'},
3579 TimeTaken => $args{'TimeTaken'},
3581 return ( $Trans, scalar $TransObj->BriefDescription );
3584 return ( $ret, $msg );
3592 Takes the name of a table column.
3593 Returns its value as a string, if the user passes an ACL check
3602 #if the field is public, return it.
3603 if ( $self->_Accessible( $field, 'public' ) ) {
3605 #$RT::Logger->debug("Skipping ACL check for $field");
3606 return ( $self->SUPER::_Value($field) );
3610 #If the current user doesn't have ACLs, don't let em at it.
3612 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3615 return ( $self->SUPER::_Value($field) );
3621 =head2 _UpdateTimeTaken
3623 This routine will increment the timeworked counter. it should
3624 only be called from _NewTransaction
3628 sub _UpdateTimeTaken {
3630 my $Minutes = shift;
3633 $Total = $self->SUPER::_Value("TimeWorked");
3634 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3636 Field => "TimeWorked",
3647 =head2 CurrentUserHasRight
3649 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3650 1 if the user has that right. It returns 0 if the user doesn't have that right.
3654 sub CurrentUserHasRight {
3658 return $self->CurrentUser->PrincipalObj->HasRight(
3665 =head2 CurrentUserCanSee
3667 Returns true if the current user can see the ticket, using ShowTicket
3671 sub CurrentUserCanSee {
3673 return $self->CurrentUserHasRight('ShowTicket');
3678 Takes a paramhash with the attributes 'Right' and 'Principal'
3679 'Right' is a ticket-scoped textual right from RT::ACE
3680 'Principal' is an RT::User object
3682 Returns 1 if the principal has the right. Returns undef if not.
3694 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3696 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3697 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3702 $args{'Principal'}->HasRight(
3704 Right => $args{'Right'}
3713 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3714 It isn't acutally a searchbuilder collection itself.
3721 unless ($self->{'__reminders'}) {
3722 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3723 $self->{'__reminders'}->Ticket($self->id);
3725 return $self->{'__reminders'};
3734 Returns an RT::Transactions object of all transactions on this ticket
3741 my $transactions = RT::Transactions->new( $self->CurrentUser );
3743 #If the user has no rights, return an empty object
3744 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3745 $transactions->LimitToTicket($self->id);
3747 # if the user may not see comments do not return them
3748 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3749 $transactions->Limit(
3755 $transactions->Limit(
3759 VALUE => "CommentEmailRecord",
3760 ENTRYAGGREGATOR => 'AND'
3765 $transactions->Limit(
3769 ENTRYAGGREGATOR => 'AND'
3773 return ($transactions);
3779 =head2 TransactionCustomFields
3781 Returns the custom fields that transactions on tickets will have.
3785 sub TransactionCustomFields {
3787 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3788 $cfs->SetContextObject( $self );
3794 =head2 CustomFieldValues
3796 # Do name => id mapping (if needed) before falling back to
3797 # RT::Record's CustomFieldValues
3803 sub CustomFieldValues {
3807 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3809 my $cf = RT::CustomField->new( $self->CurrentUser );
3810 $cf->SetContextObject( $self );
3811 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3812 unless ( $cf->id ) {
3813 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3816 # If we didn't find a valid cfid, give up.
3817 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3819 return $self->SUPER::CustomFieldValues( $cf->id );
3824 =head2 CustomFieldLookupType
3826 Returns the RT::Ticket lookup type, which can be passed to
3827 RT::CustomField->Create() via the 'LookupType' hash key.
3832 sub CustomFieldLookupType {
3833 "RT::Queue-RT::Ticket";
3836 =head2 ACLEquivalenceObjects
3838 This method returns a list of objects for which a user's rights also apply
3839 to this ticket. Generally, this is only the ticket's queue, but some RT
3840 extensions may make other objects available too.
3842 This method is called from L<RT::Principal/HasRight>.
3846 sub ACLEquivalenceObjects {
3848 return $self->QueueObj;
3857 Jesse Vincent, jesse@bestpractical.com
3867 use base 'RT::Record';
3869 sub Table {'Tickets'}
3878 Returns the current value of id.
3879 (In the database, id is stored as int(11).)
3887 Returns the current value of EffectiveId.
3888 (In the database, EffectiveId is stored as int(11).)
3892 =head2 SetEffectiveId VALUE
3895 Set EffectiveId to VALUE.
3896 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3897 (In the database, EffectiveId will be stored as a int(11).)
3905 Returns the current value of Queue.
3906 (In the database, Queue is stored as int(11).)
3910 =head2 SetQueue VALUE
3914 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3915 (In the database, Queue will be stored as a int(11).)
3923 Returns the current value of Type.
3924 (In the database, Type is stored as varchar(16).)
3928 =head2 SetType VALUE
3932 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3933 (In the database, Type will be stored as a varchar(16).)
3939 =head2 IssueStatement
3941 Returns the current value of IssueStatement.
3942 (In the database, IssueStatement is stored as int(11).)
3946 =head2 SetIssueStatement VALUE
3949 Set IssueStatement to VALUE.
3950 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3951 (In the database, IssueStatement will be stored as a int(11).)
3959 Returns the current value of Resolution.
3960 (In the database, Resolution is stored as int(11).)
3964 =head2 SetResolution VALUE
3967 Set Resolution to VALUE.
3968 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3969 (In the database, Resolution will be stored as a int(11).)
3977 Returns the current value of Owner.
3978 (In the database, Owner is stored as int(11).)
3982 =head2 SetOwner VALUE
3986 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3987 (In the database, Owner will be stored as a int(11).)
3995 Returns the current value of Subject.
3996 (In the database, Subject is stored as varchar(200).)
4000 =head2 SetSubject VALUE
4003 Set Subject to VALUE.
4004 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4005 (In the database, Subject will be stored as a varchar(200).)
4011 =head2 InitialPriority
4013 Returns the current value of InitialPriority.
4014 (In the database, InitialPriority is stored as int(11).)
4018 =head2 SetInitialPriority VALUE
4021 Set InitialPriority to VALUE.
4022 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4023 (In the database, InitialPriority will be stored as a int(11).)
4029 =head2 FinalPriority
4031 Returns the current value of FinalPriority.
4032 (In the database, FinalPriority is stored as int(11).)
4036 =head2 SetFinalPriority VALUE
4039 Set FinalPriority to VALUE.
4040 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4041 (In the database, FinalPriority will be stored as a int(11).)
4049 Returns the current value of Priority.
4050 (In the database, Priority is stored as int(11).)
4054 =head2 SetPriority VALUE
4057 Set Priority to VALUE.
4058 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4059 (In the database, Priority will be stored as a int(11).)
4065 =head2 TimeEstimated
4067 Returns the current value of TimeEstimated.
4068 (In the database, TimeEstimated is stored as int(11).)
4072 =head2 SetTimeEstimated VALUE
4075 Set TimeEstimated to VALUE.
4076 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4077 (In the database, TimeEstimated will be stored as a int(11).)
4085 Returns the current value of TimeWorked.
4086 (In the database, TimeWorked is stored as int(11).)
4090 =head2 SetTimeWorked VALUE
4093 Set TimeWorked to VALUE.
4094 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4095 (In the database, TimeWorked will be stored as a int(11).)
4103 Returns the current value of Status.
4104 (In the database, Status is stored as varchar(64).)
4108 =head2 SetStatus VALUE
4111 Set Status to VALUE.
4112 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4113 (In the database, Status will be stored as a varchar(64).)
4121 Returns the current value of TimeLeft.
4122 (In the database, TimeLeft is stored as int(11).)
4126 =head2 SetTimeLeft VALUE
4129 Set TimeLeft to VALUE.
4130 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4131 (In the database, TimeLeft will be stored as a int(11).)
4139 Returns the current value of Told.
4140 (In the database, Told is stored as datetime.)
4144 =head2 SetTold VALUE
4148 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4149 (In the database, Told will be stored as a datetime.)
4157 Returns the current value of Starts.
4158 (In the database, Starts is stored as datetime.)
4162 =head2 SetStarts VALUE
4165 Set Starts to VALUE.
4166 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4167 (In the database, Starts will be stored as a datetime.)
4175 Returns the current value of Started.
4176 (In the database, Started is stored as datetime.)
4180 =head2 SetStarted VALUE
4183 Set Started to VALUE.
4184 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4185 (In the database, Started will be stored as a datetime.)
4193 Returns the current value of Due.
4194 (In the database, Due is stored as datetime.)
4202 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4203 (In the database, Due will be stored as a datetime.)
4211 Returns the current value of Resolved.
4212 (In the database, Resolved is stored as datetime.)
4216 =head2 SetResolved VALUE
4219 Set Resolved to VALUE.
4220 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4221 (In the database, Resolved will be stored as a datetime.)
4227 =head2 LastUpdatedBy
4229 Returns the current value of LastUpdatedBy.
4230 (In the database, LastUpdatedBy is stored as int(11).)
4238 Returns the current value of LastUpdated.
4239 (In the database, LastUpdated is stored as datetime.)
4247 Returns the current value of Creator.
4248 (In the database, Creator is stored as int(11).)
4256 Returns the current value of Created.
4257 (In the database, Created is stored as datetime.)
4265 Returns the current value of Disabled.
4266 (In the database, Disabled is stored as smallint(6).)
4270 =head2 SetDisabled VALUE
4273 Set Disabled to VALUE.
4274 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4275 (In the database, Disabled will be stored as a smallint(6).)
4282 sub _CoreAccessible {
4286 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
4288 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4290 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4292 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
4294 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4296 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4298 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4300 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => '[no subject]'},
4302 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4304 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4306 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4308 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4310 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4312 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
4314 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4316 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4318 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4320 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4322 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4324 {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4326 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4328 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4330 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
4332 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
4334 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
4339 RT::Base->_ImportOverlays();