1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
54 my $ticket = new RT::Ticket($CurrentUser);
55 $ticket->Load($ticket_id);
59 This module lets you manipulate RT\'s ticket object.
71 no warnings qw(redefine);
82 use RT::URI::fsck_com_rt;
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
92 MemberOf => { Type => 'MemberOf',
94 Parents => { Type => 'MemberOf',
96 Members => { Type => 'MemberOf',
98 Children => { Type => 'MemberOf',
100 HasMember => { Type => 'MemberOf',
102 RefersTo => { Type => 'RefersTo',
104 ReferredToBy => { Type => 'RefersTo',
106 DependsOn => { Type => 'DependsOn',
108 DependedOnBy => { Type => 'DependsOn',
110 MergedInto => { Type => 'MergedInto',
118 # A helper table for links mapping to make it easier
119 # to build and parse links between tickets
122 MemberOf => { Base => 'MemberOf',
123 Target => 'HasMember', },
124 RefersTo => { Base => 'RefersTo',
125 Target => 'ReferredToBy', },
126 DependsOn => { Base => 'DependsOn',
127 Target => 'DependedOnBy', },
128 MergedInto => { Base => 'MergedInto',
129 Target => 'MergedInto', },
135 sub LINKTYPEMAP { return \%LINKTYPEMAP }
136 sub LINKDIRMAP { return \%LINKDIRMAP }
142 Takes a single argument. This can be a ticket id, ticket alias or
143 local ticket uri. If the ticket can't be loaded, returns undef.
144 Otherwise, returns the ticket id.
152 #TODO modify this routine to look at EffectiveId and do the recursive load
153 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
155 # FIXME: there is no TicketBaseURI option in config
156 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
157 #If it's a local URI, turn it into a ticket id
158 if ( $base_uri && defined $id && $id =~ /^$base_uri(\d+)$/ ) {
162 #If it's a remote URI, we're going to punt for now
163 elsif ( $id =~ '://' ) {
167 #If we have an integer URI, load the ticket
168 if ( defined $id && $id =~ /^\d+$/ ) {
169 my ($ticketid,$msg) = $self->LoadById($id);
172 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
177 #It's not a URI. It's not a numerical ticket ID. Punt!
179 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
183 #If we're merged, resolve the merge.
184 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
185 $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
186 return ( $self->Load( $self->EffectiveId ) );
189 #Ok. we're loaded. lets get outa here.
190 return ( $self->Id );
200 Arguments: ARGS is a hash of named parameters. Valid parameters are:
203 Queue - Either a Queue object or a Queue Name
204 Requestor - A reference to a list of email addresses or RT user Names
205 Cc - A reference to a list of email addresses or Names
206 AdminCc - A reference to a list of email addresses or Names
207 SquelchMailTo - A reference to a list of email addresses -
208 who should this ticket not mail
209 Type -- The ticket\'s type. ignore this for now
210 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
211 Subject -- A string describing the subject of the ticket
212 Priority -- an integer from 0 to 99
213 InitialPriority -- an integer from 0 to 99
214 FinalPriority -- an integer from 0 to 99
215 Status -- any valid status (Defined in RT::Queue)
216 TimeEstimated -- an integer. estimated time for this task in minutes
217 TimeWorked -- an integer. time worked so far in minutes
218 TimeLeft -- an integer. time remaining in minutes
219 Starts -- an ISO date describing the ticket\'s start date and time in GMT
220 Due -- an ISO date describing the ticket\'s due date and time in GMT
221 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
222 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
224 Ticket links can be set up during create by passing the link type as a hask key and
225 the ticket id to be linked to as a value (or a URI when linking to other objects).
226 Multiple links of the same type can be created by passing an array ref. For example:
229 DependsOn => [ 15, 22 ],
230 RefersTo => 'http://www.bestpractical.com',
232 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
233 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
234 C<Members> and C<Children> are aliases for C<HasMember>.
236 Returns: TICKETID, Transaction Object, Error Message
246 EffectiveId => undef,
251 SquelchMailTo => undef,
255 InitialPriority => undef,
256 FinalPriority => undef,
267 _RecordTransaction => 1,
272 my ($ErrStr, @non_fatal_errors);
274 my $QueueObj = RT::Queue->new( $RT::SystemUser );
275 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
276 $QueueObj->Load( $args{'Queue'}->Id );
278 elsif ( $args{'Queue'} ) {
279 $QueueObj->Load( $args{'Queue'} );
282 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
285 #Can't create a ticket without a queue.
286 unless ( $QueueObj->Id ) {
287 $RT::Logger->debug("$self No queue given for ticket creation.");
288 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
292 #Now that we have a queue, Check the ACLS
294 $self->CurrentUser->HasRight(
295 Right => 'CreateTicket',
302 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
305 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
306 return ( 0, 0, $self->loc('Invalid value for status') );
309 #Since we have a queue, we can set queue defaults
312 # If there's no queue default initial priority and it's not set, set it to 0
313 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
314 unless defined $args{'InitialPriority'};
317 # If there's no queue default final priority and it's not set, set it to 0
318 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
319 unless defined $args{'FinalPriority'};
321 # Priority may have changed from InitialPriority, for the case
322 # where we're importing tickets (eg, from an older RT version.)
323 $args{'Priority'} = $args{'InitialPriority'}
324 unless defined $args{'Priority'};
327 #TODO we should see what sort of due date we're getting, rather +
328 # than assuming it's in ISO format.
330 #Set the due date. if we didn't get fed one, use the queue default due in
331 my $Due = new RT::Date( $self->CurrentUser );
332 if ( defined $args{'Due'} ) {
333 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
335 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
337 $Due->AddDays( $due_in );
340 my $Starts = new RT::Date( $self->CurrentUser );
341 if ( defined $args{'Starts'} ) {
342 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
345 my $Started = new RT::Date( $self->CurrentUser );
346 if ( defined $args{'Started'} ) {
347 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
349 elsif ( $args{'Status'} ne 'new' ) {
353 my $Resolved = new RT::Date( $self->CurrentUser );
354 if ( defined $args{'Resolved'} ) {
355 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
358 #If the status is an inactive status, set the resolved date
359 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
361 $RT::Logger->debug( "Got a ". $args{'Status'}
362 ."(inactive) ticket with undefined resolved date. Setting to now."
369 # {{{ Dealing with time fields
371 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
372 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
373 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
377 # {{{ Deal with setting the owner
380 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
381 if ( $args{'Owner'}->id ) {
382 $Owner = $args{'Owner'};
384 $RT::Logger->error('passed not loaded owner object');
385 push @non_fatal_errors, $self->loc("Invalid owner object");
390 #If we've been handed something else, try to load the user.
391 elsif ( $args{'Owner'} ) {
392 $Owner = RT::User->new( $self->CurrentUser );
393 $Owner->Load( $args{'Owner'} );
394 $Owner->LoadByEmail( $args{'Owner'} )
396 unless ( $Owner->Id ) {
397 push @non_fatal_errors,
398 $self->loc("Owner could not be set.") . " "
399 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
404 #If we have a proposed owner and they don't have the right
405 #to own a ticket, scream about it and make them not the owner
408 if ( $Owner && $Owner->Id != $RT::Nobody->Id
409 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
411 $DeferOwner = $Owner;
413 $RT::Logger->debug('going to deffer setting owner');
417 #If we haven't been handed a valid owner, make it nobody.
418 unless ( defined($Owner) && $Owner->Id ) {
419 $Owner = new RT::User( $self->CurrentUser );
420 $Owner->Load( $RT::Nobody->Id );
425 # We attempt to load or create each of the people who might have a role for this ticket
426 # _outside_ the transaction, so we don't get into ticket creation races
427 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
428 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
429 foreach my $watcher ( splice @{ $args{$type} } ) {
430 next unless $watcher;
431 if ( $watcher =~ /^\d+$/ ) {
432 push @{ $args{$type} }, $watcher;
434 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
435 foreach my $address( @addresses ) {
436 my $user = RT::User->new( $RT::SystemUser );
437 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
439 push @non_fatal_errors,
440 $self->loc("Couldn't load or create user: [_1]", $msg);
442 push @{ $args{$type} }, $user->id;
449 $RT::Handle->BeginTransaction();
452 Queue => $QueueObj->Id,
454 Subject => $args{'Subject'},
455 InitialPriority => $args{'InitialPriority'},
456 FinalPriority => $args{'FinalPriority'},
457 Priority => $args{'Priority'},
458 Status => $args{'Status'},
459 TimeWorked => $args{'TimeWorked'},
460 TimeEstimated => $args{'TimeEstimated'},
461 TimeLeft => $args{'TimeLeft'},
462 Type => $args{'Type'},
463 Starts => $Starts->ISO,
464 Started => $Started->ISO,
465 Resolved => $Resolved->ISO,
469 # Parameters passed in during an import that we probably don't want to touch, otherwise
470 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
471 $params{$attr} = $args{$attr} if $args{$attr};
474 # Delete null integer parameters
476 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
478 delete $params{$attr}
479 unless ( exists $params{$attr} && $params{$attr} );
482 # Delete the time worked if we're counting it in the transaction
483 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
485 my ($id,$ticket_message) = $self->SUPER::Create( %params );
487 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
488 $RT::Handle->Rollback();
490 $self->loc("Ticket could not be created due to an internal error")
494 #Set the ticket's effective ID now that we've created it.
495 my ( $val, $msg ) = $self->__Set(
496 Field => 'EffectiveId',
497 Value => ( $args{'EffectiveId'} || $id )
500 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
501 $RT::Handle->Rollback;
503 $self->loc("Ticket could not be created due to an internal error")
507 my $create_groups_ret = $self->_CreateTicketGroups();
508 unless ($create_groups_ret) {
509 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
511 . ". aborting Ticket creation." );
512 $RT::Handle->Rollback();
514 $self->loc("Ticket could not be created due to an internal error")
518 # Set the owner in the Groups table
519 # We denormalize it into the Ticket table too because doing otherwise would
520 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
521 $self->OwnerGroup->_AddMember(
522 PrincipalId => $Owner->PrincipalId,
523 InsideTransaction => 1
524 ) unless $DeferOwner;
528 # {{{ Deal with setting up watchers
530 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
531 # we know it's an array ref
532 foreach my $watcher ( @{ $args{$type} } ) {
534 # Note that we're using AddWatcher, rather than _AddWatcher, as we
535 # actually _want_ that ACL check. Otherwise, random ticket creators
536 # could make themselves adminccs and maybe get ticket rights. that would
538 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
540 my ($val, $msg) = $self->$method(
542 PrincipalId => $watcher,
545 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
550 if ($args{'SquelchMailTo'}) {
551 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
552 : $args{'SquelchMailTo'};
553 $self->_SquelchMailTo( @squelch );
559 # {{{ Add all the custom fields
561 foreach my $arg ( keys %args ) {
562 next unless $arg =~ /^CustomField-(\d+)$/i;
566 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
568 next unless defined $value && length $value;
570 # Allow passing in uploaded LargeContent etc by hash reference
571 my ($status, $msg) = $self->_AddCustomFieldValue(
572 (UNIVERSAL::isa( $value => 'HASH' )
577 RecordTransaction => 0,
579 push @non_fatal_errors, $msg unless $status;
585 # {{{ Deal with setting up links
587 # TODO: Adding link may fire scrips on other end and those scrips
588 # could create transactions on this ticket before 'Create' transaction.
590 # We should implement different schema: record 'Create' transaction,
591 # create links and only then fire create transaction's scrips.
593 # Ideal variant: add all links without firing scrips, record create
594 # transaction and only then fire scrips on the other ends of links.
598 foreach my $type ( keys %LINKTYPEMAP ) {
599 next unless ( defined $args{$type} );
601 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
603 # Check rights on the other end of the link if we must
604 # then run _AddLink that doesn't check for ACLs
605 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
606 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
608 push @non_fatal_errors, $msg;
611 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
612 push @non_fatal_errors, $self->loc('Linking. Permission denied');
617 my ( $wval, $wmsg ) = $self->_AddLink(
618 Type => $LINKTYPEMAP{$type}->{'Type'},
619 $LINKTYPEMAP{$type}->{'Mode'} => $link,
620 Silent => !$args{'_RecordTransaction'},
621 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
625 push @non_fatal_errors, $wmsg unless ($wval);
630 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
631 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
633 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
635 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
636 . ") was proposed as a ticket owner but has no rights to own "
637 . "tickets in " . $QueueObj->Name );
638 push @non_fatal_errors, $self->loc(
639 "Owner '[_1]' does not have rights to own this ticket.",
643 $Owner = $DeferOwner;
644 $self->__Set(Field => 'Owner', Value => $Owner->id);
647 $self->OwnerGroup->_AddMember(
648 PrincipalId => $Owner->PrincipalId,
649 InsideTransaction => 1
653 if ( $args{'_RecordTransaction'} ) {
655 # {{{ Add a transaction for the create
656 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
658 TimeTaken => $args{'TimeWorked'},
659 MIMEObj => $args{'MIMEObj'},
660 CommitScrips => !$args{'DryRun'},
663 if ( $self->Id && $Trans ) {
665 $TransObj->UpdateCustomFields(ARGSRef => \%args);
667 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
668 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
669 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
672 $RT::Handle->Rollback();
674 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
675 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
676 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
679 if ( $args{'DryRun'} ) {
680 $RT::Handle->Rollback();
681 return ($self->id, $TransObj, $ErrStr);
683 $RT::Handle->Commit();
684 return ( $self->Id, $TransObj->Id, $ErrStr );
690 # Not going to record a transaction
691 $RT::Handle->Commit();
692 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
693 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
694 return ( $self->Id, 0, $ErrStr );
702 # {{{ _Parse822HeadersForAttributes Content
704 =head2 _Parse822HeadersForAttributes Content
706 Takes an RFC822 style message and parses its attributes into a hash.
710 sub _Parse822HeadersForAttributes {
715 my @lines = ( split ( /\n/, $content ) );
716 while ( defined( my $line = shift @lines ) ) {
717 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
722 if ( defined( $args{$tag} ) )
723 { #if we're about to get a second value, make it an array
724 $args{$tag} = [ $args{$tag} ];
726 if ( ref( $args{$tag} ) )
727 { #If it's an array, we want to push the value
728 push @{ $args{$tag} }, $value;
730 else { #if there's nothing there, just set the value
731 $args{$tag} = $value;
733 } elsif ($line =~ /^$/) {
735 #TODO: this won't work, since "" isn't of the form "foo:value"
737 while ( defined( my $l = shift @lines ) ) {
738 push @{ $args{'content'} }, $l;
744 foreach my $date qw(due starts started resolved) {
745 my $dateobj = RT::Date->new($RT::SystemUser);
746 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
747 $dateobj->Set( Format => 'unix', Value => $args{$date} );
750 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
752 $args{$date} = $dateobj->ISO;
754 $args{'mimeobj'} = MIME::Entity->new();
755 $args{'mimeobj'}->build(
756 Type => ( $args{'contenttype'} || 'text/plain' ),
757 Data => ($args{'content'} || '')
767 =head2 Import PARAMHASH
770 Doesn\'t create a transaction.
771 Doesn\'t supply queue defaults, etc.
779 my ( $ErrStr, $QueueObj, $Owner );
783 EffectiveId => undef,
787 Owner => $RT::Nobody->Id,
788 Subject => '[no subject]',
789 InitialPriority => undef,
790 FinalPriority => undef,
801 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
802 $QueueObj = RT::Queue->new($RT::SystemUser);
803 $QueueObj->Load( $args{'Queue'} );
805 #TODO error check this and return 0 if it\'s not loading properly +++
807 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
808 $QueueObj = RT::Queue->new($RT::SystemUser);
809 $QueueObj->Load( $args{'Queue'}->Id );
813 "$self " . $args{'Queue'} . " not a recognised queue object." );
816 #Can't create a ticket without a queue.
817 unless ( defined($QueueObj) and $QueueObj->Id ) {
818 $RT::Logger->debug("$self No queue given for ticket creation.");
819 return ( 0, $self->loc('Could not create ticket. Queue not set') );
822 #Now that we have a queue, Check the ACLS
824 $self->CurrentUser->HasRight(
825 Right => 'CreateTicket',
831 $self->loc("No permission to create tickets in the queue '[_1]'"
835 # {{{ Deal with setting the owner
837 # Attempt to take user object, user name or user id.
838 # Assign to nobody if lookup fails.
839 if ( defined( $args{'Owner'} ) ) {
840 if ( ref( $args{'Owner'} ) ) {
841 $Owner = $args{'Owner'};
844 $Owner = new RT::User( $self->CurrentUser );
845 $Owner->Load( $args{'Owner'} );
846 if ( !defined( $Owner->id ) ) {
847 $Owner->Load( $RT::Nobody->id );
852 #If we have a proposed owner and they don't have the right
853 #to own a ticket, scream about it and make them not the owner
856 and ( $Owner->Id != $RT::Nobody->Id )
866 $RT::Logger->warning( "$self user "
870 . "as a ticket owner but has no rights to own "
872 . $QueueObj->Name . "'" );
877 #If we haven't been handed a valid owner, make it nobody.
878 unless ( defined($Owner) ) {
879 $Owner = new RT::User( $self->CurrentUser );
880 $Owner->Load( $RT::Nobody->UserObj->Id );
885 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
886 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
889 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
890 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
891 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
892 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
894 # If we're coming in with an id, set that now.
895 my $EffectiveId = undef;
897 $EffectiveId = $args{'id'};
901 my $id = $self->SUPER::Create(
903 EffectiveId => $EffectiveId,
904 Queue => $QueueObj->Id,
906 Subject => $args{'Subject'}, # loc
907 InitialPriority => $args{'InitialPriority'}, # loc
908 FinalPriority => $args{'FinalPriority'}, # loc
909 Priority => $args{'InitialPriority'}, # loc
910 Status => $args{'Status'}, # loc
911 TimeWorked => $args{'TimeWorked'}, # loc
912 Type => $args{'Type'}, # loc
913 Created => $args{'Created'}, # loc
914 Told => $args{'Told'}, # loc
915 LastUpdated => $args{'Updated'}, # loc
916 Resolved => $args{'Resolved'}, # loc
917 Due => $args{'Due'}, # loc
920 # If the ticket didn't have an id
921 # Set the ticket's effective ID now that we've created it.
923 $self->Load( $args{'id'} );
927 $self->__Set( Field => 'EffectiveId', Value => $id );
931 $self . "->Import couldn't set EffectiveId: $msg" );
935 my $create_groups_ret = $self->_CreateTicketGroups();
936 unless ($create_groups_ret) {
938 "Couldn't create ticket groups for ticket " . $self->Id );
941 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
944 foreach $watcher ( @{ $args{'Cc'} } ) {
945 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
947 foreach $watcher ( @{ $args{'AdminCc'} } ) {
948 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
951 foreach $watcher ( @{ $args{'Requestor'} } ) {
952 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
956 return ( $self->Id, $ErrStr );
961 # {{{ Routines dealing with watchers.
963 # {{{ _CreateTicketGroups
965 =head2 _CreateTicketGroups
967 Create the ticket groups and links for this ticket.
968 This routine expects to be called from Ticket->Create _inside of a transaction_
970 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
972 It will return true on success and undef on failure.
978 sub _CreateTicketGroups {
981 my @types = qw(Requestor Owner Cc AdminCc);
983 foreach my $type (@types) {
984 my $type_obj = RT::Group->new($self->CurrentUser);
985 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
986 Instance => $self->Id,
989 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
990 $self->Id.": ".$msg);
1000 # {{{ sub OwnerGroup
1004 A constructor which returns an RT::Group object containing the owner of this ticket.
1010 my $owner_obj = RT::Group->new($self->CurrentUser);
1011 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1012 return ($owner_obj);
1018 # {{{ sub AddWatcher
1022 AddWatcher takes a parameter hash. The keys are as follows:
1024 Type One of Requestor, Cc, AdminCc
1026 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1028 Email The email address of the new watcher. If a user with this
1029 email address can't be found, a new nonprivileged user will be created.
1031 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.
1039 PrincipalId => undef,
1044 # ModifyTicket works in any case
1045 return $self->_AddWatcher( %args )
1046 if $self->CurrentUserHasRight('ModifyTicket');
1047 if ( $args{'Email'} ) {
1048 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1049 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1052 if ( lc $self->CurrentUser->UserObj->EmailAddress
1053 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1055 $args{'PrincipalId'} = $self->CurrentUser->id;
1056 delete $args{'Email'};
1060 # If the watcher isn't the current user then the current user has no right
1062 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1063 return ( 0, $self->loc("Permission Denied") );
1066 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1067 if ( $args{'Type'} eq 'AdminCc' ) {
1068 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1069 return ( 0, $self->loc('Permission Denied') );
1073 # If it's a Requestor or Cc and they don't have 'Watch', bail
1074 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1075 unless ( $self->CurrentUserHasRight('Watch') ) {
1076 return ( 0, $self->loc('Permission Denied') );
1080 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1081 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1084 return $self->_AddWatcher( %args );
1087 #This contains the meat of AddWatcher. but can be called from a routine like
1088 # Create, which doesn't need the additional acl check
1094 PrincipalId => undef,
1100 my $principal = RT::Principal->new($self->CurrentUser);
1101 if ($args{'Email'}) {
1102 my $user = RT::User->new($RT::SystemUser);
1103 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1104 $args{'PrincipalId'} = $pid if $pid;
1106 if ($args{'PrincipalId'}) {
1107 $principal->Load($args{'PrincipalId'});
1111 # If we can't find this watcher, we need to bail.
1112 unless ($principal->Id) {
1113 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1114 return(0, $self->loc("Could not find or create that user"));
1118 my $group = RT::Group->new($self->CurrentUser);
1119 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1120 unless ($group->id) {
1121 return(0,$self->loc("Group not found"));
1124 if ( $group->HasMember( $principal)) {
1126 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1130 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1131 InsideTransaction => 1 );
1133 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1135 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1138 unless ( $args{'Silent'} ) {
1139 $self->_NewTransaction(
1140 Type => 'AddWatcher',
1141 NewValue => $principal->Id,
1142 Field => $args{'Type'}
1146 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1152 # {{{ sub DeleteWatcher
1154 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1157 Deletes a Ticket watcher. Takes two arguments:
1159 Type (one of Requestor,Cc,AdminCc)
1163 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1165 Email (the email address of an existing wathcer)
1174 my %args = ( Type => undef,
1175 PrincipalId => undef,
1179 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1180 return ( 0, $self->loc("No principal specified") );
1182 my $principal = RT::Principal->new( $self->CurrentUser );
1183 if ( $args{'PrincipalId'} ) {
1185 $principal->Load( $args{'PrincipalId'} );
1188 my $user = RT::User->new( $self->CurrentUser );
1189 $user->LoadByEmail( $args{'Email'} );
1190 $principal->Load( $user->Id );
1193 # If we can't find this watcher, we need to bail.
1194 unless ( $principal->Id ) {
1195 return ( 0, $self->loc("Could not find that principal") );
1198 my $group = RT::Group->new( $self->CurrentUser );
1199 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1200 unless ( $group->id ) {
1201 return ( 0, $self->loc("Group not found") );
1205 #If the watcher we're trying to add is for the current user
1206 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1208 # If it's an AdminCc and they don't have
1209 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1210 if ( $args{'Type'} eq 'AdminCc' ) {
1211 unless ( $self->CurrentUserHasRight('ModifyTicket')
1212 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1213 return ( 0, $self->loc('Permission Denied') );
1217 # If it's a Requestor or Cc and they don't have
1218 # 'Watch' or 'ModifyTicket', bail
1219 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1221 unless ( $self->CurrentUserHasRight('ModifyTicket')
1222 or $self->CurrentUserHasRight('Watch') ) {
1223 return ( 0, $self->loc('Permission Denied') );
1227 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1229 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1233 # If the watcher isn't the current user
1234 # and the current user doesn't have 'ModifyTicket' bail
1236 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1237 return ( 0, $self->loc("Permission Denied") );
1243 # see if this user is already a watcher.
1245 unless ( $group->HasMember($principal) ) {
1247 $self->loc( 'That principal is not a [_1] for this ticket',
1251 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1253 $RT::Logger->error( "Failed to delete "
1255 . " as a member of group "
1261 'Could not remove that principal as a [_1] for this ticket',
1265 unless ( $args{'Silent'} ) {
1266 $self->_NewTransaction( Type => 'DelWatcher',
1267 OldValue => $principal->Id,
1268 Field => $args{'Type'} );
1272 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1273 $principal->Object->Name,
1282 =head2 SquelchMailTo [EMAIL]
1284 Takes an optional email address to never email about updates to this ticket.
1287 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1295 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1299 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1304 return $self->_SquelchMailTo(@_);
1307 sub _SquelchMailTo {
1311 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1312 unless grep { $_->Content eq $attr }
1313 $self->Attributes->Named('SquelchMailTo');
1315 my @attributes = $self->Attributes->Named('SquelchMailTo');
1316 return (@attributes);
1320 =head2 UnsquelchMailTo ADDRESS
1322 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1324 Returns a tuple of (status, message)
1328 sub UnsquelchMailTo {
1331 my $address = shift;
1332 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1333 return ( 0, $self->loc("Permission Denied") );
1336 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1337 return ($val, $msg);
1341 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1343 =head2 RequestorAddresses
1345 B<Returns> String: All Ticket Requestor email addresses as a string.
1349 sub RequestorAddresses {
1352 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1356 return ( $self->Requestors->MemberEmailAddressesAsString );
1360 =head2 AdminCcAddresses
1362 returns String: All Ticket AdminCc email addresses as a string
1366 sub AdminCcAddresses {
1369 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1373 return ( $self->AdminCc->MemberEmailAddressesAsString )
1379 returns String: All Ticket Ccs as a string of email addresses
1386 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1389 return ( $self->Cc->MemberEmailAddressesAsString);
1395 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1397 # {{{ sub Requestors
1402 Returns this ticket's Requestors as an RT::Group object
1409 my $group = RT::Group->new($self->CurrentUser);
1410 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1411 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1424 Returns an RT::Group object which contains this ticket's Ccs.
1425 If the user doesn't have "ShowTicket" permission, returns an empty group
1432 my $group = RT::Group->new($self->CurrentUser);
1433 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1434 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1447 Returns an RT::Group object which contains this ticket's AdminCcs.
1448 If the user doesn't have "ShowTicket" permission, returns an empty group
1455 my $group = RT::Group->new($self->CurrentUser);
1456 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1457 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1467 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1470 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1472 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1474 Takes a param hash with the attributes Type and either PrincipalId or Email
1476 Type is one of Requestor, Cc, AdminCc and Owner
1478 PrincipalId is an RT::Principal id, and Email is an email address.
1480 Returns true if the specified principal (or the one corresponding to the
1481 specified address) is a member of the group Type for this ticket.
1483 XX TODO: This should be Memoized.
1490 my %args = ( Type => 'Requestor',
1491 PrincipalId => undef,
1496 # Load the relevant group.
1497 my $group = RT::Group->new($self->CurrentUser);
1498 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1500 # Find the relevant principal.
1501 if (!$args{PrincipalId} && $args{Email}) {
1502 # Look up the specified user.
1503 my $user = RT::User->new($self->CurrentUser);
1504 $user->LoadByEmail($args{Email});
1506 $args{PrincipalId} = $user->PrincipalId;
1509 # A non-existent user can't be a group member.
1514 # Ask if it has the member in question
1515 return $group->HasMember( $args{'PrincipalId'} );
1520 # {{{ sub IsRequestor
1522 =head2 IsRequestor PRINCIPAL_ID
1524 Takes an L<RT::Principal> id.
1526 Returns true if the principal is a requestor of the current ticket.
1534 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1542 =head2 IsCc PRINCIPAL_ID
1544 Takes an RT::Principal id.
1545 Returns true if the principal is a Cc of the current ticket.
1554 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1562 =head2 IsAdminCc PRINCIPAL_ID
1564 Takes an RT::Principal id.
1565 Returns true if the principal is an AdminCc of the current ticket.
1573 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1583 Takes an RT::User object. Returns true if that user is this ticket's owner.
1584 returns undef otherwise
1592 # no ACL check since this is used in acl decisions
1593 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1597 #Tickets won't yet have owners when they're being created.
1598 unless ( $self->OwnerObj->id ) {
1602 if ( $person->id == $self->OwnerObj->id ) {
1617 =head2 TransactionAddresses
1619 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for all this ticket's Create, Comment or Correspond transactions.
1620 The keys are C<To>, C<Cc> and C<Bcc>. The values are lists of C<Email::Address> objects.
1622 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.
1627 sub TransactionAddresses {
1629 my $txns = $self->Transactions;
1632 foreach my $type (qw(Create Comment Correspond)) {
1633 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1636 while (my $txn = $txns->Next) {
1637 my $txnaddrs = $txn->Addresses;
1638 foreach my $addrlist ( values %$txnaddrs ) {
1639 foreach my $addr (@$addrlist) {
1640 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1641 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1642 # skips "comment-only" addresses
1643 next unless ($addr->address);
1644 $addresses{$addr->address} = $addr;
1656 # {{{ Routines dealing with queues
1658 # {{{ sub ValidateQueue
1665 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1669 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1670 my $id = $QueueObj->Load($Value);
1686 my $NewQueue = shift;
1688 #Redundant. ACL gets checked in _Set;
1689 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1690 return ( 0, $self->loc("Permission Denied") );
1693 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1694 $NewQueueObj->Load($NewQueue);
1696 unless ( $NewQueueObj->Id() ) {
1697 return ( 0, $self->loc("That queue does not exist") );
1700 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1701 return ( 0, $self->loc('That is the same value') );
1704 $self->CurrentUser->HasRight(
1705 Right => 'CreateTicket',
1706 Object => $NewQueueObj
1710 return ( 0, $self->loc("You may not create requests in that queue.") );
1714 $self->OwnerObj->HasRight(
1715 Right => 'OwnTicket',
1716 Object => $NewQueueObj
1720 my $clone = RT::Ticket->new( $RT::SystemUser );
1721 $clone->Load( $self->Id );
1722 unless ( $clone->Id ) {
1723 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1725 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1726 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1729 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1732 # On queue change, change queue for reminders too
1733 my $reminder_collection = $self->Reminders->Collection;
1734 while ( my $reminder = $reminder_collection->Next ) {
1735 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1736 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1740 return ($status, $msg);
1749 Takes nothing. returns this ticket's queue object
1756 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1758 #We call __Value so that we can avoid the ACL decision and some deep recursion
1759 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1760 return ($queue_obj);
1767 # {{{ Date printing routines
1773 Returns an RT::Date object containing this ticket's due date
1780 my $time = new RT::Date( $self->CurrentUser );
1782 # -1 is RT::Date slang for never
1783 if ( my $due = $self->Due ) {
1784 $time->Set( Format => 'sql', Value => $due );
1787 $time->Set( Format => 'unix', Value => -1 );
1795 # {{{ sub DueAsString
1799 Returns this ticket's due date as a human readable string
1805 return $self->DueObj->AsString();
1810 # {{{ sub ResolvedObj
1814 Returns an RT::Date object of this ticket's 'resolved' time.
1821 my $time = new RT::Date( $self->CurrentUser );
1822 $time->Set( Format => 'sql', Value => $self->Resolved );
1828 # {{{ sub SetStarted
1832 Takes a date in ISO format or undef
1833 Returns a transaction id and a message
1834 The client calls "Start" to note that the project was started on the date in $date.
1835 A null date means "now"
1841 my $time = shift || 0;
1843 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1844 return ( 0, $self->loc("Permission Denied") );
1847 #We create a date object to catch date weirdness
1848 my $time_obj = new RT::Date( $self->CurrentUser() );
1850 $time_obj->Set( Format => 'ISO', Value => $time );
1853 $time_obj->SetToNow();
1856 #Now that we're starting, open this ticket
1857 #TODO do we really want to force this as policy? it should be a scrip
1859 #We need $TicketAsSystem, in case the current user doesn't have
1862 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1863 $TicketAsSystem->Load( $self->Id );
1864 if ( $TicketAsSystem->Status eq 'new' ) {
1865 $TicketAsSystem->Open();
1868 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1874 # {{{ sub StartedObj
1878 Returns an RT::Date object which contains this ticket's
1886 my $time = new RT::Date( $self->CurrentUser );
1887 $time->Set( Format => 'sql', Value => $self->Started );
1897 Returns an RT::Date object which contains this ticket's
1905 my $time = new RT::Date( $self->CurrentUser );
1906 $time->Set( Format => 'sql', Value => $self->Starts );
1916 Returns an RT::Date object which contains this ticket's
1924 my $time = new RT::Date( $self->CurrentUser );
1925 $time->Set( Format => 'sql', Value => $self->Told );
1931 # {{{ sub ToldAsString
1935 A convenience method that returns ToldObj->AsString
1937 TODO: This should be deprecated
1943 if ( $self->Told ) {
1944 return $self->ToldObj->AsString();
1953 # {{{ sub TimeWorkedAsString
1955 =head2 TimeWorkedAsString
1957 Returns the amount of time worked on this ticket as a Text String
1961 sub TimeWorkedAsString {
1963 my $value = $self->TimeWorked;
1965 # return the # of minutes worked turned into seconds and written as
1966 # a simple text string, this is not really a date object, but if we
1967 # diff a number of seconds vs the epoch, we'll get a nice description
1969 return "" unless $value;
1970 return RT::Date->new( $self->CurrentUser )
1971 ->DurationAsString( $value * 60 );
1976 # {{{ sub TimeLeftAsString
1978 =head2 TimeLeftAsString
1980 Returns the amount of time left on this ticket as a Text String
1984 sub TimeLeftAsString {
1986 my $value = $self->TimeLeft;
1987 return "" unless $value;
1988 return RT::Date->new( $self->CurrentUser )
1989 ->DurationAsString( $value * 60 );
1994 # {{{ Routines dealing with correspondence/comments
2000 Comment on this ticket.
2001 Takes a hash with the following attributes:
2002 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2005 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2007 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2008 They will, however, be prepared and you'll be able to access them through the TransactionObj
2010 Returns: Transaction id, Error Message, Transaction Object
2011 (note the different order from Create()!)
2018 my %args = ( CcMessageTo => undef,
2019 BccMessageTo => undef,
2026 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2027 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2028 return ( 0, $self->loc("Permission Denied"), undef );
2030 $args{'NoteType'} = 'Comment';
2032 if ($args{'DryRun'}) {
2033 $RT::Handle->BeginTransaction();
2034 $args{'CommitScrips'} = 0;
2037 my @results = $self->_RecordNote(%args);
2038 if ($args{'DryRun'}) {
2039 $RT::Handle->Rollback();
2046 # {{{ sub Correspond
2050 Correspond on this ticket.
2051 Takes a hashref with the following attributes:
2054 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2056 if there's no MIMEObj, Content is used to build a MIME::Entity object
2058 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2059 They will, however, be prepared and you'll be able to access them through the TransactionObj
2061 Returns: Transaction id, Error Message, Transaction Object
2062 (note the different order from Create()!)
2069 my %args = ( CcMessageTo => undef,
2070 BccMessageTo => undef,
2076 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2077 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2078 return ( 0, $self->loc("Permission Denied"), undef );
2081 $args{'NoteType'} = 'Correspond';
2082 if ($args{'DryRun'}) {
2083 $RT::Handle->BeginTransaction();
2084 $args{'CommitScrips'} = 0;
2087 my @results = $self->_RecordNote(%args);
2089 #Set the last told date to now if this isn't mail from the requestor.
2090 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2091 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2093 if ($args{'DryRun'}) {
2094 $RT::Handle->Rollback();
2103 # {{{ sub _RecordNote
2107 the meat of both comment and correspond.
2109 Performs no access control checks. hence, dangerous.
2116 CcMessageTo => undef,
2117 BccMessageTo => undef,
2122 NoteType => 'Correspond',
2128 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2129 return ( 0, $self->loc("No message attached"), undef );
2132 unless ( $args{'MIMEObj'} ) {
2133 $args{'MIMEObj'} = MIME::Entity->build(
2134 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2138 # convert text parts into utf-8
2139 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2141 # If we've been passed in CcMessageTo and BccMessageTo fields,
2142 # add them to the mime object for passing on to the transaction handler
2143 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2144 # RT-Send-Bcc: headers
2147 foreach my $type (qw/Cc Bcc/) {
2148 if ( defined $args{ $type . 'MessageTo' } ) {
2150 my $addresses = join ', ', (
2151 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2152 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2153 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2157 foreach my $argument (qw(Encrypt Sign)) {
2158 $args{'MIMEObj'}->head->add(
2159 "X-RT-$argument" => $args{ $argument }
2160 ) if defined $args{ $argument };
2163 # If this is from an external source, we need to come up with its
2164 # internal Message-ID now, so all emails sent because of this
2165 # message have a common Message-ID
2166 my $org = RT->Config->Get('Organization');
2167 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2168 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2169 $args{'MIMEObj'}->head->set(
2170 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2174 #Record the correspondence (write the transaction)
2175 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2176 Type => $args{'NoteType'},
2177 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2178 TimeTaken => $args{'TimeTaken'},
2179 MIMEObj => $args{'MIMEObj'},
2180 CommitScrips => $args{'CommitScrips'},
2184 $RT::Logger->err("$self couldn't init a transaction $msg");
2185 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2188 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2200 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2203 my $type = shift || "";
2205 unless ( $self->{"$field$type"} ) {
2206 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2207 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2208 # Maybe this ticket is a merged ticket
2209 my $Tickets = new RT::Tickets( $self->CurrentUser );
2210 # at least to myself
2211 $self->{"$field$type"}->Limit( FIELD => $field,
2212 VALUE => $self->URI,
2213 ENTRYAGGREGATOR => 'OR' );
2214 $Tickets->Limit( FIELD => 'EffectiveId',
2215 VALUE => $self->EffectiveId );
2216 while (my $Ticket = $Tickets->Next) {
2217 $self->{"$field$type"}->Limit( FIELD => $field,
2218 VALUE => $Ticket->URI,
2219 ENTRYAGGREGATOR => 'OR' );
2221 $self->{"$field$type"}->Limit( FIELD => 'Type',
2226 return ( $self->{"$field$type"} );
2231 # {{{ sub DeleteLink
2235 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2236 SilentBase and SilentTarget. Either Base or Target must be null.
2237 The null value will be replaced with this ticket\'s id.
2239 If Silent is true then no transaction would be recorded, in other
2240 case you can control creation of transactions on both base and
2241 target with SilentBase and SilentTarget respectively. By default
2242 both transactions are created.
2253 SilentBase => undef,
2254 SilentTarget => undef,
2258 unless ( $args{'Target'} || $args{'Base'} ) {
2259 $RT::Logger->error("Base or Target must be specified");
2260 return ( 0, $self->loc('Either base or target must be specified') );
2265 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2266 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2267 return ( 0, $self->loc("Permission Denied") );
2270 # If the other URI is an RT::Ticket, we want to make sure the user
2271 # can modify it too...
2272 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2273 return (0, $msg) unless $status;
2274 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2277 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2278 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2280 return ( 0, $self->loc("Permission Denied") );
2283 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2284 return ( 0, $Msg ) unless $val;
2286 return ( $val, $Msg ) if $args{'Silent'};
2288 my ($direction, $remote_link);
2290 if ( $args{'Base'} ) {
2291 $remote_link = $args{'Base'};
2292 $direction = 'Target';
2294 elsif ( $args{'Target'} ) {
2295 $remote_link = $args{'Target'};
2296 $direction = 'Base';
2299 my $remote_uri = RT::URI->new( $self->CurrentUser );
2300 $remote_uri->FromURI( $remote_link );
2302 unless ( $args{ 'Silent'. $direction } ) {
2303 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2304 Type => 'DeleteLink',
2305 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2306 OldValue => $remote_uri->URI || $remote_link,
2309 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2312 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2313 my $OtherObj = $remote_uri->Object;
2314 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2315 Type => 'DeleteLink',
2316 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2317 : $LINKDIRMAP{$args{'Type'}}->{Target},
2318 OldValue => $self->URI,
2319 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2322 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2325 return ( $val, $Msg );
2334 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2336 If Silent is true then no transaction would be recorded, in other
2337 case you can control creation of transactions on both base and
2338 target with SilentBase and SilentTarget respectively. By default
2339 both transactions are created.
2345 my %args = ( Target => '',
2349 SilentBase => undef,
2350 SilentTarget => undef,
2353 unless ( $args{'Target'} || $args{'Base'} ) {
2354 $RT::Logger->error("Base or Target must be specified");
2355 return ( 0, $self->loc('Either base or target must be specified') );
2359 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2360 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2361 return ( 0, $self->loc("Permission Denied") );
2364 # If the other URI is an RT::Ticket, we want to make sure the user
2365 # can modify it too...
2366 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2367 return (0, $msg) unless $status;
2368 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2371 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2372 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2374 return ( 0, $self->loc("Permission Denied") );
2377 return $self->_AddLink(%args);
2380 sub __GetTicketFromURI {
2382 my %args = ( URI => '', @_ );
2384 # If the other URI is an RT::Ticket, we want to make sure the user
2385 # can modify it too...
2386 my $uri_obj = RT::URI->new( $self->CurrentUser );
2387 $uri_obj->FromURI( $args{'URI'} );
2389 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2390 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2391 $RT::Logger->warning( $msg );
2394 my $obj = $uri_obj->Resolver->Object;
2395 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2396 return (1, 'Found not a ticket', undef);
2398 return (1, 'Found ticket', $obj);
2403 Private non-acled variant of AddLink so that links can be added during create.
2409 my %args = ( Target => '',
2413 SilentBase => undef,
2414 SilentTarget => undef,
2417 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2418 return ($val, $msg) if !$val || $exist;
2419 return ($val, $msg) if $args{'Silent'};
2421 my ($direction, $remote_link);
2422 if ( $args{'Target'} ) {
2423 $remote_link = $args{'Target'};
2424 $direction = 'Base';
2425 } elsif ( $args{'Base'} ) {
2426 $remote_link = $args{'Base'};
2427 $direction = 'Target';
2430 my $remote_uri = RT::URI->new( $self->CurrentUser );
2431 $remote_uri->FromURI( $remote_link );
2433 unless ( $args{ 'Silent'. $direction } ) {
2434 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2436 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2437 NewValue => $remote_uri->URI || $remote_link,
2440 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2443 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2444 my $OtherObj = $remote_uri->Object;
2445 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2447 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2448 : $LINKDIRMAP{$args{'Type'}}->{Target},
2449 NewValue => $self->URI,
2450 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2453 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2456 return ( $val, $msg );
2466 MergeInto take the id of the ticket to merge this ticket into.
2474 my $ticket_id = shift;
2476 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2477 return ( 0, $self->loc("Permission Denied") );
2480 # Load up the new ticket.
2481 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2482 $MergeInto->Load($ticket_id);
2484 # make sure it exists.
2485 unless ( $MergeInto->Id ) {
2486 return ( 0, $self->loc("New ticket doesn't exist") );
2489 # Make sure the current user can modify the new ticket.
2490 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2491 return ( 0, $self->loc("Permission Denied") );
2494 $RT::Handle->BeginTransaction();
2496 # We use EffectiveId here even though it duplicates information from
2497 # the links table becasue of the massive performance hit we'd take
2498 # by trying to do a separate database query for merge info everytime
2501 #update this ticket's effective id to the new ticket's id.
2502 my ( $id_val, $id_msg ) = $self->__Set(
2503 Field => 'EffectiveId',
2504 Value => $MergeInto->Id()
2508 $RT::Handle->Rollback();
2509 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2513 if ( $self->__Value('Status') ne 'resolved' ) {
2515 my ( $status_val, $status_msg )
2516 = $self->__Set( Field => 'Status', Value => 'resolved' );
2518 unless ($status_val) {
2519 $RT::Handle->Rollback();
2522 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2526 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2530 # update all the links that point to that old ticket
2531 my $old_links_to = RT::Links->new($self->CurrentUser);
2532 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2535 while (my $link = $old_links_to->Next) {
2536 if (exists $old_seen{$link->Base."-".$link->Type}) {
2539 elsif ($link->Base eq $MergeInto->URI) {
2542 # First, make sure the link doesn't already exist. then move it over.
2543 my $tmp = RT::Link->new($RT::SystemUser);
2544 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2548 $link->SetTarget($MergeInto->URI);
2549 $link->SetLocalTarget($MergeInto->id);
2551 $old_seen{$link->Base."-".$link->Type} =1;
2556 my $old_links_from = RT::Links->new($self->CurrentUser);
2557 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2559 while (my $link = $old_links_from->Next) {
2560 if (exists $old_seen{$link->Type."-".$link->Target}) {
2563 if ($link->Target eq $MergeInto->URI) {
2566 # First, make sure the link doesn't already exist. then move it over.
2567 my $tmp = RT::Link->new($RT::SystemUser);
2568 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2572 $link->SetBase($MergeInto->URI);
2573 $link->SetLocalBase($MergeInto->id);
2574 $old_seen{$link->Type."-".$link->Target} =1;
2580 # Update time fields
2581 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2583 my $mutator = "Set$type";
2584 $MergeInto->$mutator(
2585 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2588 #add all of this ticket's watchers to that ticket.
2589 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2591 my $people = $self->$watcher_type->MembersObj;
2592 my $addwatcher_type = $watcher_type;
2593 $addwatcher_type =~ s/s$//;
2595 while ( my $watcher = $people->Next ) {
2597 my ($val, $msg) = $MergeInto->_AddWatcher(
2598 Type => $addwatcher_type,
2600 PrincipalId => $watcher->MemberId
2603 $RT::Logger->warning($msg);
2609 #find all of the tickets that were merged into this ticket.
2610 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2611 $old_mergees->Limit(
2612 FIELD => 'EffectiveId',
2617 # update their EffectiveId fields to the new ticket's id
2618 while ( my $ticket = $old_mergees->Next() ) {
2619 my ( $val, $msg ) = $ticket->__Set(
2620 Field => 'EffectiveId',
2621 Value => $MergeInto->Id()
2625 #make a new link: this ticket is merged into that other ticket.
2626 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2628 $MergeInto->_SetLastUpdated;
2630 $RT::Handle->Commit();
2631 return ( 1, $self->loc("Merge Successful") );
2636 Returns list of tickets' ids that's been merged into this ticket.
2643 my $mergees = new RT::Tickets( $self->CurrentUser );
2645 FIELD => 'EffectiveId',
2654 return map $_->id, @{ $mergees->ItemsArrayRef || [] };
2661 # {{{ Routines dealing with ownership
2667 Takes nothing and returns an RT::User object of
2675 #If this gets ACLed, we lose on a rights check in User.pm and
2676 #get deep recursion. if we need ACLs here, we need
2677 #an equiv without ACLs
2679 my $owner = new RT::User( $self->CurrentUser );
2680 $owner->Load( $self->__Value('Owner') );
2682 #Return the owner object
2688 # {{{ sub OwnerAsString
2690 =head2 OwnerAsString
2692 Returns the owner's email address
2698 return ( $self->OwnerObj->EmailAddress );
2708 Takes two arguments:
2709 the Id or Name of the owner
2710 and (optionally) the type of the SetOwner Transaction. It defaults
2711 to 'Give'. 'Steal' is also a valid option.
2718 my $NewOwner = shift;
2719 my $Type = shift || "Give";
2721 $RT::Handle->BeginTransaction();
2723 $self->_SetLastUpdated(); # lock the ticket
2724 $self->Load( $self->id ); # in case $self changed while waiting for lock
2726 my $OldOwnerObj = $self->OwnerObj;
2728 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2729 $NewOwnerObj->Load( $NewOwner );
2730 unless ( $NewOwnerObj->Id ) {
2731 $RT::Handle->Rollback();
2732 return ( 0, $self->loc("That user does not exist") );
2736 # must have ModifyTicket rights
2737 # or TakeTicket/StealTicket and $NewOwner is self
2738 # see if it's a take
2739 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2740 unless ( $self->CurrentUserHasRight('ModifyTicket')
2741 || $self->CurrentUserHasRight('TakeTicket') ) {
2742 $RT::Handle->Rollback();
2743 return ( 0, $self->loc("Permission Denied") );
2747 # see if it's a steal
2748 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2749 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2751 unless ( $self->CurrentUserHasRight('ModifyTicket')
2752 || $self->CurrentUserHasRight('StealTicket') ) {
2753 $RT::Handle->Rollback();
2754 return ( 0, $self->loc("Permission Denied") );
2758 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2759 $RT::Handle->Rollback();
2760 return ( 0, $self->loc("Permission Denied") );
2764 # If we're not stealing and the ticket has an owner and it's not
2766 if ( $Type ne 'Steal' and $Type ne 'Force'
2767 and $OldOwnerObj->Id != $RT::Nobody->Id
2768 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2770 $RT::Handle->Rollback();
2771 return ( 0, $self->loc("You can only take tickets that are unowned") )
2772 if $NewOwnerObj->id == $self->CurrentUser->id;
2775 $self->loc("You can only reassign tickets that you own or that are unowned" )
2779 #If we've specified a new owner and that user can't modify the ticket
2780 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2781 $RT::Handle->Rollback();
2782 return ( 0, $self->loc("That user may not own tickets in that queue") );
2785 # If the ticket has an owner and it's the new owner, we don't need
2787 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2788 $RT::Handle->Rollback();
2789 return ( 0, $self->loc("That user already owns that ticket") );
2792 # Delete the owner in the owner group, then add a new one
2793 # TODO: is this safe? it's not how we really want the API to work
2794 # for most things, but it's fast.
2795 my ( $del_id, $del_msg );
2796 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2797 ($del_id, $del_msg) = $owner->Delete();
2798 last unless ($del_id);
2802 $RT::Handle->Rollback();
2803 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2806 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2807 PrincipalId => $NewOwnerObj->PrincipalId,
2808 InsideTransaction => 1 );
2810 $RT::Handle->Rollback();
2811 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2814 # We call set twice with slightly different arguments, so
2815 # as to not have an SQL transaction span two RT transactions
2817 my ( $val, $msg ) = $self->_Set(
2819 RecordTransaction => 0,
2820 Value => $NewOwnerObj->Id,
2822 TransactionType => $Type,
2823 CheckACL => 0, # don't check acl
2827 $RT::Handle->Rollback;
2828 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2831 ($val, $msg) = $self->_NewTransaction(
2834 NewValue => $NewOwnerObj->Id,
2835 OldValue => $OldOwnerObj->Id,
2840 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2841 $OldOwnerObj->Name, $NewOwnerObj->Name );
2844 $RT::Handle->Rollback();
2848 $RT::Handle->Commit();
2850 return ( $val, $msg );
2859 A convenince method to set the ticket's owner to the current user
2865 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2874 Convenience method to set the owner to 'nobody' if the current user is the owner.
2880 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2889 A convenience method to change the owner of the current ticket to the
2890 current user. Even if it's owned by another user.
2897 if ( $self->IsOwner( $self->CurrentUser ) ) {
2898 return ( 0, $self->loc("You already own this ticket") );
2901 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2911 # {{{ Routines dealing with status
2913 # {{{ sub ValidateStatus
2915 =head2 ValidateStatus STATUS
2917 Takes a string. Returns true if that status is a valid status for this ticket.
2918 Returns false otherwise.
2922 sub ValidateStatus {
2926 #Make sure the status passed in is valid
2927 unless ( $self->QueueObj->IsValidStatus($status) ) {
2939 =head2 SetStatus STATUS
2941 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
2943 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE). If FORCE is true, ignore unresolved dependencies and force a status change.
2954 $args{Status} = shift;
2961 if ( $args{Status} eq 'deleted') {
2962 unless ($self->CurrentUserHasRight('DeleteTicket')) {
2963 return ( 0, $self->loc('Permission Denied') );
2966 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2967 return ( 0, $self->loc('Permission Denied') );
2971 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
2972 return (0, $self->loc('That ticket has unresolved dependencies'));
2975 my $now = RT::Date->new( $self->CurrentUser );
2978 #If we're changing the status from new, record that we've started
2979 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
2981 #Set the Started time to "now"
2982 $self->_Set( Field => 'Started',
2984 RecordTransaction => 0 );
2987 #When we close a ticket, set the 'Resolved' attribute to now.
2988 # It's misnamed, but that's just historical.
2989 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
2990 $self->_Set( Field => 'Resolved',
2992 RecordTransaction => 0 );
2995 #Actually update the status
2996 my ($val, $msg)= $self->_Set( Field => 'Status',
2997 Value => $args{Status},
3000 TransactionType => 'Status' );
3011 Takes no arguments. Marks this ticket for garbage collection
3017 return ( $self->SetStatus('deleted') );
3019 # TODO: garbage collection
3028 Sets this ticket's status to stalled
3034 return ( $self->SetStatus('stalled') );
3043 Sets this ticket's status to rejected
3049 return ( $self->SetStatus('rejected') );
3058 Sets this ticket\'s status to Open
3064 return ( $self->SetStatus('open') );
3073 Sets this ticket\'s status to Resolved
3079 return ( $self->SetStatus('resolved') );
3087 # {{{ Actions + Routines dealing with transactions
3089 # {{{ sub SetTold and _SetTold
3091 =head2 SetTold ISO [TIMETAKEN]
3093 Updates the told and records a transaction
3100 $told = shift if (@_);
3101 my $timetaken = shift || 0;
3103 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3104 return ( 0, $self->loc("Permission Denied") );
3107 my $datetold = new RT::Date( $self->CurrentUser );
3109 $datetold->Set( Format => 'iso',
3113 $datetold->SetToNow();
3116 return ( $self->_Set( Field => 'Told',
3117 Value => $datetold->ISO,
3118 TimeTaken => $timetaken,
3119 TransactionType => 'Told' ) );
3124 Updates the told without a transaction or acl check. Useful when we're sending replies.
3131 my $now = new RT::Date( $self->CurrentUser );
3134 #use __Set to get no ACLs ;)
3135 return ( $self->__Set( Field => 'Told',
3136 Value => $now->ISO ) );
3146 my $uid = $self->CurrentUser->id;
3147 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3148 return if $attr && $attr->Content gt $self->LastUpdated;
3150 my $txns = $self->Transactions;
3151 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3152 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3153 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3157 VALUE => $attr->Content
3159 $txns->RowsPerPage(1);
3160 return $txns->First;
3165 =head2 TransactionBatch
3167 Returns an array reference of all transactions created on this ticket during
3168 this ticket object's lifetime or since last application of a batch, or undef
3171 Only works when the C<UseTransactionBatch> config option is set to true.
3175 sub TransactionBatch {
3177 return $self->{_TransactionBatch};
3180 =head2 ApplyTransactionBatch
3182 Applies scrips on the current batch of transactions and shinks it. Usually
3183 batch is applied when object is destroyed, but in some cases it's too late.
3187 sub ApplyTransactionBatch {
3190 my $batch = $self->TransactionBatch;
3191 return unless $batch && @$batch;
3193 $self->_ApplyTransactionBatch;
3195 $self->{_TransactionBatch} = [];
3198 sub _ApplyTransactionBatch {
3200 my $batch = $self->TransactionBatch;
3203 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3206 RT::Scrips->new($RT::SystemUser)->Apply(
3207 Stage => 'TransactionBatch',
3209 TransactionObj => $batch->[0],
3213 # Entry point of the rule system
3214 my $rules = RT::Ruleset->FindAllRules(
3215 Stage => 'TransactionBatch',
3217 TransactionObj => $batch->[0],
3220 RT::Ruleset->CommitRules($rules);
3226 # DESTROY methods need to localize $@, or it may unset it. This
3227 # causes $m->abort to not bubble all of the way up. See perlbug
3228 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3231 # The following line eliminates reentrancy.
3232 # It protects against the fact that perl doesn't deal gracefully
3233 # when an object's refcount is changed in its destructor.
3234 return if $self->{_Destroyed}++;
3236 my $batch = $self->TransactionBatch;
3237 return unless $batch && @$batch;
3239 return $self->_ApplyTransactionBatch;
3244 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3246 # {{{ sub _OverlayAccessible
3248 sub _OverlayAccessible {
3250 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3251 Queue => { 'read' => 1, 'write' => 1 },
3252 Requestors => { 'read' => 1, 'write' => 1 },
3253 Owner => { 'read' => 1, 'write' => 1 },
3254 Subject => { 'read' => 1, 'write' => 1 },
3255 InitialPriority => { 'read' => 1, 'write' => 1 },
3256 FinalPriority => { 'read' => 1, 'write' => 1 },
3257 Priority => { 'read' => 1, 'write' => 1 },
3258 Status => { 'read' => 1, 'write' => 1 },
3259 TimeEstimated => { 'read' => 1, 'write' => 1 },
3260 TimeWorked => { 'read' => 1, 'write' => 1 },
3261 TimeLeft => { 'read' => 1, 'write' => 1 },
3262 Told => { 'read' => 1, 'write' => 1 },
3263 Resolved => { 'read' => 1 },
3264 Type => { 'read' => 1 },
3265 Starts => { 'read' => 1, 'write' => 1 },
3266 Started => { 'read' => 1, 'write' => 1 },
3267 Due => { 'read' => 1, 'write' => 1 },
3268 Creator => { 'read' => 1, 'auto' => 1 },
3269 Created => { 'read' => 1, 'auto' => 1 },
3270 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3271 LastUpdated => { 'read' => 1, 'auto' => 1 }
3283 my %args = ( Field => undef,
3286 RecordTransaction => 1,
3289 TransactionType => 'Set',
3292 if ($args{'CheckACL'}) {
3293 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3294 return ( 0, $self->loc("Permission Denied"));
3298 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3299 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3300 return(0, $self->loc("Internal Error"));
3303 #if the user is trying to modify the record
3305 #Take care of the old value we really don't want to get in an ACL loop.
3306 # so ask the super::_Value
3307 my $Old = $self->SUPER::_Value("$args{'Field'}");
3310 if ( $args{'UpdateTicket'} ) {
3313 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3314 Value => $args{'Value'} );
3316 #If we can't actually set the field to the value, don't record
3317 # a transaction. instead, get out of here.
3318 return ( 0, $msg ) unless $ret;
3321 if ( $args{'RecordTransaction'} == 1 ) {
3323 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3324 Type => $args{'TransactionType'},
3325 Field => $args{'Field'},
3326 NewValue => $args{'Value'},
3328 TimeTaken => $args{'TimeTaken'},
3330 return ( $Trans, scalar $TransObj->BriefDescription );
3333 return ( $ret, $msg );
3343 Takes the name of a table column.
3344 Returns its value as a string, if the user passes an ACL check
3353 #if the field is public, return it.
3354 if ( $self->_Accessible( $field, 'public' ) ) {
3356 #$RT::Logger->debug("Skipping ACL check for $field");
3357 return ( $self->SUPER::_Value($field) );
3361 #If the current user doesn't have ACLs, don't let em at it.
3363 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3366 return ( $self->SUPER::_Value($field) );
3372 # {{{ sub _UpdateTimeTaken
3374 =head2 _UpdateTimeTaken
3376 This routine will increment the timeworked counter. it should
3377 only be called from _NewTransaction
3381 sub _UpdateTimeTaken {
3383 my $Minutes = shift;
3386 $Total = $self->SUPER::_Value("TimeWorked");
3387 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3389 Field => "TimeWorked",
3400 # {{{ Routines dealing with ACCESS CONTROL
3402 # {{{ sub CurrentUserHasRight
3404 =head2 CurrentUserHasRight
3406 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3407 1 if the user has that right. It returns 0 if the user doesn't have that right.
3411 sub CurrentUserHasRight {
3415 return $self->CurrentUser->PrincipalObj->HasRight(
3427 Takes a paramhash with the attributes 'Right' and 'Principal'
3428 'Right' is a ticket-scoped textual right from RT::ACE
3429 'Principal' is an RT::User object
3431 Returns 1 if the principal has the right. Returns undef if not.
3443 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3445 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3446 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3451 $args{'Principal'}->HasRight(
3453 Right => $args{'Right'}
3464 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3465 It isn't acutally a searchbuilder collection itself.
3472 unless ($self->{'__reminders'}) {
3473 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3474 $self->{'__reminders'}->Ticket($self->id);
3476 return $self->{'__reminders'};
3482 # {{{ sub Transactions
3486 Returns an RT::Transactions object of all transactions on this ticket
3493 my $transactions = RT::Transactions->new( $self->CurrentUser );
3495 #If the user has no rights, return an empty object
3496 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3497 $transactions->LimitToTicket($self->id);
3499 # if the user may not see comments do not return them
3500 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3501 $transactions->Limit(
3507 $transactions->Limit(
3511 VALUE => "CommentEmailRecord",
3512 ENTRYAGGREGATOR => 'AND'
3517 $transactions->Limit(
3521 ENTRYAGGREGATOR => 'AND'
3525 return ($transactions);
3531 # {{{ TransactionCustomFields
3533 =head2 TransactionCustomFields
3535 Returns the custom fields that transactions on tickets will have.
3539 sub TransactionCustomFields {
3541 return $self->QueueObj->TicketTransactionCustomFields;
3546 # {{{ sub CustomFieldValues
3548 =head2 CustomFieldValues
3550 # Do name => id mapping (if needed) before falling back to
3551 # RT::Record's CustomFieldValues
3557 sub CustomFieldValues {
3561 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3563 my $cf = RT::CustomField->new( $self->CurrentUser );
3564 $cf->SetContextObject( $self );
3565 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3566 unless ( $cf->id ) {
3567 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3570 # If we didn't find a valid cfid, give up.
3571 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3573 return $self->SUPER::CustomFieldValues( $cf->id );
3578 # {{{ sub CustomFieldLookupType
3580 =head2 CustomFieldLookupType
3582 Returns the RT::Ticket lookup type, which can be passed to
3583 RT::CustomField->Create() via the 'LookupType' hash key.
3589 sub CustomFieldLookupType {
3590 "RT::Queue-RT::Ticket";
3593 =head2 ACLEquivalenceObjects
3595 This method returns a list of objects for which a user's rights also apply
3596 to this ticket. Generally, this is only the ticket's queue, but some RT
3597 extensions may make other objects available too.
3599 This method is called from L<RT::Principal/HasRight>.
3603 sub ACLEquivalenceObjects {
3605 return $self->QueueObj;
3614 Jesse Vincent, jesse@bestpractical.com