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 }
147 Takes a single argument. This can be a ticket id, ticket alias or
148 local ticket uri. If the ticket can't be loaded, returns undef.
149 Otherwise, returns the ticket id.
156 $id = '' unless defined $id;
158 # TODO: modify this routine to look at EffectiveId and
159 # do the recursive load thing. be careful to cache all
160 # the interim tickets we try so we don't loop forever.
162 # FIXME: there is no TicketBaseURI option in config
163 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
164 #If it's a local URI, turn it into a ticket id
165 if ( $base_uri && $id =~ /^$base_uri(\d+)$/ ) {
169 unless ( $id =~ /^\d+$/ ) {
170 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
174 $id = $MERGE_CACHE{'effective'}{ $id }
175 if $MERGE_CACHE{'effective'}{ $id };
177 my ($ticketid, $msg) = $self->LoadById( $id );
178 unless ( $self->Id ) {
179 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
183 #If we're merged, resolve the merge.
184 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
186 "We found a merged ticket. "
187 . $self->id ."/". $self->EffectiveId
189 my $real_id = $self->Load( $self->EffectiveId );
190 $MERGE_CACHE{'effective'}{ $id } = $real_id;
194 #Ok. we're loaded. lets get outa here.
204 Arguments: ARGS is a hash of named parameters. Valid parameters are:
207 Queue - Either a Queue object or a Queue Name
208 Requestor - A reference to a list of email addresses or RT user Names
209 Cc - A reference to a list of email addresses or Names
210 AdminCc - A reference to a list of email addresses or Names
211 SquelchMailTo - A reference to a list of email addresses -
212 who should this ticket not mail
213 Type -- The ticket\'s type. ignore this for now
214 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
215 Subject -- A string describing the subject of the ticket
216 Priority -- an integer from 0 to 99
217 InitialPriority -- an integer from 0 to 99
218 FinalPriority -- an integer from 0 to 99
219 Status -- any valid status (Defined in RT::Queue)
220 TimeEstimated -- an integer. estimated time for this task in minutes
221 TimeWorked -- an integer. time worked so far in minutes
222 TimeLeft -- an integer. time remaining in minutes
223 Starts -- an ISO date describing the ticket\'s start date and time in GMT
224 Due -- an ISO date describing the ticket\'s due date and time in GMT
225 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
226 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
228 Ticket links can be set up during create by passing the link type as a hask key and
229 the ticket id to be linked to as a value (or a URI when linking to other objects).
230 Multiple links of the same type can be created by passing an array ref. For example:
233 DependsOn => [ 15, 22 ],
234 RefersTo => 'http://www.bestpractical.com',
236 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
237 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
238 C<Members> and C<Children> are aliases for C<HasMember>.
240 Returns: TICKETID, Transaction Object, Error Message
250 EffectiveId => undef,
255 SquelchMailTo => undef,
259 InitialPriority => undef,
260 FinalPriority => undef,
271 _RecordTransaction => 1,
276 my ($ErrStr, @non_fatal_errors);
278 my $QueueObj = RT::Queue->new( $RT::SystemUser );
279 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
280 $QueueObj->Load( $args{'Queue'}->Id );
282 elsif ( $args{'Queue'} ) {
283 $QueueObj->Load( $args{'Queue'} );
286 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
289 #Can't create a ticket without a queue.
290 unless ( $QueueObj->Id ) {
291 $RT::Logger->debug("$self No queue given for ticket creation.");
292 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
296 #Now that we have a queue, Check the ACLS
298 $self->CurrentUser->HasRight(
299 Right => 'CreateTicket',
306 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
309 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
310 return ( 0, 0, $self->loc('Invalid value for status') );
313 #Since we have a queue, we can set queue defaults
316 # If there's no queue default initial priority and it's not set, set it to 0
317 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
318 unless defined $args{'InitialPriority'};
321 # If there's no queue default final priority and it's not set, set it to 0
322 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
323 unless defined $args{'FinalPriority'};
325 # Priority may have changed from InitialPriority, for the case
326 # where we're importing tickets (eg, from an older RT version.)
327 $args{'Priority'} = $args{'InitialPriority'}
328 unless defined $args{'Priority'};
331 #TODO we should see what sort of due date we're getting, rather +
332 # than assuming it's in ISO format.
334 #Set the due date. if we didn't get fed one, use the queue default due in
335 my $Due = new RT::Date( $self->CurrentUser );
336 if ( defined $args{'Due'} ) {
337 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
339 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
341 $Due->AddDays( $due_in );
344 my $Starts = new RT::Date( $self->CurrentUser );
345 if ( defined $args{'Starts'} ) {
346 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
349 my $Started = new RT::Date( $self->CurrentUser );
350 if ( defined $args{'Started'} ) {
351 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
353 elsif ( $args{'Status'} ne 'new' ) {
357 my $Resolved = new RT::Date( $self->CurrentUser );
358 if ( defined $args{'Resolved'} ) {
359 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
362 #If the status is an inactive status, set the resolved date
363 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
365 $RT::Logger->debug( "Got a ". $args{'Status'}
366 ."(inactive) ticket with undefined resolved date. Setting to now."
373 # {{{ Dealing with time fields
375 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
376 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
377 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
381 # {{{ Deal with setting the owner
384 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
385 if ( $args{'Owner'}->id ) {
386 $Owner = $args{'Owner'};
388 $RT::Logger->error('passed not loaded owner object');
389 push @non_fatal_errors, $self->loc("Invalid owner object");
394 #If we've been handed something else, try to load the user.
395 elsif ( $args{'Owner'} ) {
396 $Owner = RT::User->new( $self->CurrentUser );
397 $Owner->Load( $args{'Owner'} );
398 $Owner->LoadByEmail( $args{'Owner'} )
400 unless ( $Owner->Id ) {
401 push @non_fatal_errors,
402 $self->loc("Owner could not be set.") . " "
403 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
408 #If we have a proposed owner and they don't have the right
409 #to own a ticket, scream about it and make them not the owner
412 if ( $Owner && $Owner->Id != $RT::Nobody->Id
413 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
415 $DeferOwner = $Owner;
417 $RT::Logger->debug('going to deffer setting owner');
421 #If we haven't been handed a valid owner, make it nobody.
422 unless ( defined($Owner) && $Owner->Id ) {
423 $Owner = new RT::User( $self->CurrentUser );
424 $Owner->Load( $RT::Nobody->Id );
429 # We attempt to load or create each of the people who might have a role for this ticket
430 # _outside_ the transaction, so we don't get into ticket creation races
431 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
432 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
433 foreach my $watcher ( splice @{ $args{$type} } ) {
434 next unless $watcher;
435 if ( $watcher =~ /^\d+$/ ) {
436 push @{ $args{$type} }, $watcher;
438 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
439 foreach my $address( @addresses ) {
440 my $user = RT::User->new( $RT::SystemUser );
441 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
443 push @non_fatal_errors,
444 $self->loc("Couldn't load or create user: [_1]", $msg);
446 push @{ $args{$type} }, $user->id;
453 $RT::Handle->BeginTransaction();
456 Queue => $QueueObj->Id,
458 Subject => $args{'Subject'},
459 InitialPriority => $args{'InitialPriority'},
460 FinalPriority => $args{'FinalPriority'},
461 Priority => $args{'Priority'},
462 Status => $args{'Status'},
463 TimeWorked => $args{'TimeWorked'},
464 TimeEstimated => $args{'TimeEstimated'},
465 TimeLeft => $args{'TimeLeft'},
466 Type => $args{'Type'},
467 Starts => $Starts->ISO,
468 Started => $Started->ISO,
469 Resolved => $Resolved->ISO,
473 # Parameters passed in during an import that we probably don't want to touch, otherwise
474 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
475 $params{$attr} = $args{$attr} if $args{$attr};
478 # Delete null integer parameters
480 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
482 delete $params{$attr}
483 unless ( exists $params{$attr} && $params{$attr} );
486 # Delete the time worked if we're counting it in the transaction
487 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
489 my ($id,$ticket_message) = $self->SUPER::Create( %params );
491 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
492 $RT::Handle->Rollback();
494 $self->loc("Ticket could not be created due to an internal error")
498 #Set the ticket's effective ID now that we've created it.
499 my ( $val, $msg ) = $self->__Set(
500 Field => 'EffectiveId',
501 Value => ( $args{'EffectiveId'} || $id )
504 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
505 $RT::Handle->Rollback;
507 $self->loc("Ticket could not be created due to an internal error")
511 my $create_groups_ret = $self->_CreateTicketGroups();
512 unless ($create_groups_ret) {
513 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
515 . ". aborting Ticket creation." );
516 $RT::Handle->Rollback();
518 $self->loc("Ticket could not be created due to an internal error")
522 # Set the owner in the Groups table
523 # We denormalize it into the Ticket table too because doing otherwise would
524 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
525 $self->OwnerGroup->_AddMember(
526 PrincipalId => $Owner->PrincipalId,
527 InsideTransaction => 1
528 ) unless $DeferOwner;
532 # {{{ Deal with setting up watchers
534 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
535 # we know it's an array ref
536 foreach my $watcher ( @{ $args{$type} } ) {
538 # Note that we're using AddWatcher, rather than _AddWatcher, as we
539 # actually _want_ that ACL check. Otherwise, random ticket creators
540 # could make themselves adminccs and maybe get ticket rights. that would
542 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
544 my ($val, $msg) = $self->$method(
546 PrincipalId => $watcher,
549 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
554 if ($args{'SquelchMailTo'}) {
555 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
556 : $args{'SquelchMailTo'};
557 $self->_SquelchMailTo( @squelch );
563 # {{{ Deal with auto-customer association
565 #unless we already have (a) customer(s)...
566 unless ( $self->Customers->Count ) {
568 #first find any requestors with emails but *without* customer targets
569 my @NoCust_Requestors =
570 grep { $_->EmailAddress && ! $_->Customers->Count }
571 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
573 for my $Requestor (@NoCust_Requestors) {
575 #perhaps the stuff in here should be in a User method??
577 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
579 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
581 ## false laziness w/RT/Interface/Web_Vendor.pm
582 my @link = ( 'Type' => 'MemberOf',
583 'Target' => "freeside://freeside/cust_main/$custnum",
586 my( $val, $msg ) = $Requestor->_AddLink(@link);
587 #XXX should do something with $msg# push @non_fatal_errors, $msg;
593 #find any requestors with customer targets
595 my %cust_target = ();
598 grep { $_->Customers->Count }
599 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
601 foreach my $Requestor ( @Requestors ) {
602 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
603 $cust_target{ $cust_link->Target } = 1;
607 #and then auto-associate this ticket with those customers
609 foreach my $cust_target ( keys %cust_target ) {
611 my @link = ( 'Type' => 'MemberOf',
612 #'Target' => "freeside://freeside/cust_main/$custnum",
613 'Target' => $cust_target,
616 my( $val, $msg ) = $self->_AddLink(@link);
617 push @non_fatal_errors, $msg;
625 # {{{ Add all the custom fields
627 foreach my $arg ( keys %args ) {
628 next unless $arg =~ /^CustomField-(\d+)$/i;
632 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
634 next unless defined $value && length $value;
636 # Allow passing in uploaded LargeContent etc by hash reference
637 my ($status, $msg) = $self->_AddCustomFieldValue(
638 (UNIVERSAL::isa( $value => 'HASH' )
643 RecordTransaction => 0,
645 push @non_fatal_errors, $msg unless $status;
651 # {{{ Deal with setting up links
653 # TODO: Adding link may fire scrips on other end and those scrips
654 # could create transactions on this ticket before 'Create' transaction.
656 # We should implement different schema: record 'Create' transaction,
657 # create links and only then fire create transaction's scrips.
659 # Ideal variant: add all links without firing scrips, record create
660 # transaction and only then fire scrips on the other ends of links.
664 foreach my $type ( keys %LINKTYPEMAP ) {
665 next unless ( defined $args{$type} );
667 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
669 # Check rights on the other end of the link if we must
670 # then run _AddLink that doesn't check for ACLs
671 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
672 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
674 push @non_fatal_errors, $msg;
677 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
678 push @non_fatal_errors, $self->loc('Linking. Permission denied');
683 my ( $wval, $wmsg ) = $self->_AddLink(
684 Type => $LINKTYPEMAP{$type}->{'Type'},
685 $LINKTYPEMAP{$type}->{'Mode'} => $link,
686 Silent => !$args{'_RecordTransaction'},
687 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
691 push @non_fatal_errors, $wmsg unless ($wval);
696 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
697 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
699 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
701 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
702 . ") was proposed as a ticket owner but has no rights to own "
703 . "tickets in " . $QueueObj->Name );
704 push @non_fatal_errors, $self->loc(
705 "Owner '[_1]' does not have rights to own this ticket.",
709 $Owner = $DeferOwner;
710 $self->__Set(Field => 'Owner', Value => $Owner->id);
712 $self->OwnerGroup->_AddMember(
713 PrincipalId => $Owner->PrincipalId,
714 InsideTransaction => 1
718 if ( $args{'_RecordTransaction'} ) {
720 # {{{ Add a transaction for the create
721 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
723 TimeTaken => $args{'TimeWorked'},
724 MIMEObj => $args{'MIMEObj'},
725 CommitScrips => !$args{'DryRun'},
728 if ( $self->Id && $Trans ) {
730 $TransObj->UpdateCustomFields(ARGSRef => \%args);
732 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
733 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
734 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
737 $RT::Handle->Rollback();
739 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
740 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
741 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
744 if ( $args{'DryRun'} ) {
745 $RT::Handle->Rollback();
746 return ($self->id, $TransObj, $ErrStr);
748 $RT::Handle->Commit();
749 return ( $self->Id, $TransObj->Id, $ErrStr );
755 # Not going to record a transaction
756 $RT::Handle->Commit();
757 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
758 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
759 return ( $self->Id, 0, $ErrStr );
767 # {{{ _Parse822HeadersForAttributes Content
769 =head2 _Parse822HeadersForAttributes Content
771 Takes an RFC822 style message and parses its attributes into a hash.
775 sub _Parse822HeadersForAttributes {
780 my @lines = ( split ( /\n/, $content ) );
781 while ( defined( my $line = shift @lines ) ) {
782 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
787 if ( defined( $args{$tag} ) )
788 { #if we're about to get a second value, make it an array
789 $args{$tag} = [ $args{$tag} ];
791 if ( ref( $args{$tag} ) )
792 { #If it's an array, we want to push the value
793 push @{ $args{$tag} }, $value;
795 else { #if there's nothing there, just set the value
796 $args{$tag} = $value;
798 } elsif ($line =~ /^$/) {
800 #TODO: this won't work, since "" isn't of the form "foo:value"
802 while ( defined( my $l = shift @lines ) ) {
803 push @{ $args{'content'} }, $l;
809 foreach my $date qw(due starts started resolved) {
810 my $dateobj = RT::Date->new($RT::SystemUser);
811 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
812 $dateobj->Set( Format => 'unix', Value => $args{$date} );
815 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
817 $args{$date} = $dateobj->ISO;
819 $args{'mimeobj'} = MIME::Entity->new();
820 $args{'mimeobj'}->build(
821 Type => ( $args{'contenttype'} || 'text/plain' ),
822 Data => ($args{'content'} || '')
832 =head2 Import PARAMHASH
835 Doesn\'t create a transaction.
836 Doesn\'t supply queue defaults, etc.
844 my ( $ErrStr, $QueueObj, $Owner );
848 EffectiveId => undef,
852 Owner => $RT::Nobody->Id,
853 Subject => '[no subject]',
854 InitialPriority => undef,
855 FinalPriority => undef,
866 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
867 $QueueObj = RT::Queue->new($RT::SystemUser);
868 $QueueObj->Load( $args{'Queue'} );
870 #TODO error check this and return 0 if it\'s not loading properly +++
872 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
873 $QueueObj = RT::Queue->new($RT::SystemUser);
874 $QueueObj->Load( $args{'Queue'}->Id );
878 "$self " . $args{'Queue'} . " not a recognised queue object." );
881 #Can't create a ticket without a queue.
882 unless ( defined($QueueObj) and $QueueObj->Id ) {
883 $RT::Logger->debug("$self No queue given for ticket creation.");
884 return ( 0, $self->loc('Could not create ticket. Queue not set') );
887 #Now that we have a queue, Check the ACLS
889 $self->CurrentUser->HasRight(
890 Right => 'CreateTicket',
896 $self->loc("No permission to create tickets in the queue '[_1]'"
900 # {{{ Deal with setting the owner
902 # Attempt to take user object, user name or user id.
903 # Assign to nobody if lookup fails.
904 if ( defined( $args{'Owner'} ) ) {
905 if ( ref( $args{'Owner'} ) ) {
906 $Owner = $args{'Owner'};
909 $Owner = new RT::User( $self->CurrentUser );
910 $Owner->Load( $args{'Owner'} );
911 if ( !defined( $Owner->id ) ) {
912 $Owner->Load( $RT::Nobody->id );
917 #If we have a proposed owner and they don't have the right
918 #to own a ticket, scream about it and make them not the owner
921 and ( $Owner->Id != $RT::Nobody->Id )
931 $RT::Logger->warning( "$self user "
935 . "as a ticket owner but has no rights to own "
937 . $QueueObj->Name . "'" );
942 #If we haven't been handed a valid owner, make it nobody.
943 unless ( defined($Owner) ) {
944 $Owner = new RT::User( $self->CurrentUser );
945 $Owner->Load( $RT::Nobody->UserObj->Id );
950 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
951 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
954 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
955 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
956 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
957 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
959 # If we're coming in with an id, set that now.
960 my $EffectiveId = undef;
962 $EffectiveId = $args{'id'};
966 my $id = $self->SUPER::Create(
968 EffectiveId => $EffectiveId,
969 Queue => $QueueObj->Id,
971 Subject => $args{'Subject'}, # loc
972 InitialPriority => $args{'InitialPriority'}, # loc
973 FinalPriority => $args{'FinalPriority'}, # loc
974 Priority => $args{'InitialPriority'}, # loc
975 Status => $args{'Status'}, # loc
976 TimeWorked => $args{'TimeWorked'}, # loc
977 Type => $args{'Type'}, # loc
978 Created => $args{'Created'}, # loc
979 Told => $args{'Told'}, # loc
980 LastUpdated => $args{'Updated'}, # loc
981 Resolved => $args{'Resolved'}, # loc
982 Due => $args{'Due'}, # loc
985 # If the ticket didn't have an id
986 # Set the ticket's effective ID now that we've created it.
988 $self->Load( $args{'id'} );
992 $self->__Set( Field => 'EffectiveId', Value => $id );
996 $self . "->Import couldn't set EffectiveId: $msg" );
1000 my $create_groups_ret = $self->_CreateTicketGroups();
1001 unless ($create_groups_ret) {
1003 "Couldn't create ticket groups for ticket " . $self->Id );
1006 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1009 foreach $watcher ( @{ $args{'Cc'} } ) {
1010 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1012 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1013 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1016 foreach $watcher ( @{ $args{'Requestor'} } ) {
1017 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1021 return ( $self->Id, $ErrStr );
1026 # {{{ Routines dealing with watchers.
1028 # {{{ _CreateTicketGroups
1030 =head2 _CreateTicketGroups
1032 Create the ticket groups and links for this ticket.
1033 This routine expects to be called from Ticket->Create _inside of a transaction_
1035 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1037 It will return true on success and undef on failure.
1043 sub _CreateTicketGroups {
1046 my @types = qw(Requestor Owner Cc AdminCc);
1048 foreach my $type (@types) {
1049 my $type_obj = RT::Group->new($self->CurrentUser);
1050 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1051 Instance => $self->Id,
1054 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1055 $self->Id.": ".$msg);
1065 # {{{ sub OwnerGroup
1069 A constructor which returns an RT::Group object containing the owner of this ticket.
1075 my $owner_obj = RT::Group->new($self->CurrentUser);
1076 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1077 return ($owner_obj);
1083 # {{{ sub AddWatcher
1087 AddWatcher takes a parameter hash. The keys are as follows:
1089 Type One of Requestor, Cc, AdminCc
1091 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1093 Email The email address of the new watcher. If a user with this
1094 email address can't be found, a new nonprivileged user will be created.
1096 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.
1104 PrincipalId => undef,
1109 # ModifyTicket works in any case
1110 return $self->_AddWatcher( %args )
1111 if $self->CurrentUserHasRight('ModifyTicket');
1112 if ( $args{'Email'} ) {
1113 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1114 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1117 if ( lc $self->CurrentUser->UserObj->EmailAddress
1118 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1120 $args{'PrincipalId'} = $self->CurrentUser->id;
1121 delete $args{'Email'};
1125 # If the watcher isn't the current user then the current user has no right
1127 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1128 return ( 0, $self->loc("Permission Denied") );
1131 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1132 if ( $args{'Type'} eq 'AdminCc' ) {
1133 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1134 return ( 0, $self->loc('Permission Denied') );
1138 # If it's a Requestor or Cc and they don't have 'Watch', bail
1139 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1140 unless ( $self->CurrentUserHasRight('Watch') ) {
1141 return ( 0, $self->loc('Permission Denied') );
1145 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1146 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1149 return $self->_AddWatcher( %args );
1152 #This contains the meat of AddWatcher. but can be called from a routine like
1153 # Create, which doesn't need the additional acl check
1159 PrincipalId => undef,
1165 my $principal = RT::Principal->new($self->CurrentUser);
1166 if ($args{'Email'}) {
1167 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1168 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'})));
1170 my $user = RT::User->new($RT::SystemUser);
1171 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1172 $args{'PrincipalId'} = $pid if $pid;
1174 if ($args{'PrincipalId'}) {
1175 $principal->Load($args{'PrincipalId'});
1176 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1177 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'})))
1178 if RT::EmailParser->IsRTAddress( $email );
1184 # If we can't find this watcher, we need to bail.
1185 unless ($principal->Id) {
1186 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1187 return(0, $self->loc("Could not find or create that user"));
1191 my $group = RT::Group->new($self->CurrentUser);
1192 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1193 unless ($group->id) {
1194 return(0,$self->loc("Group not found"));
1197 if ( $group->HasMember( $principal)) {
1199 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1203 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1204 InsideTransaction => 1 );
1206 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1208 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1211 unless ( $args{'Silent'} ) {
1212 $self->_NewTransaction(
1213 Type => 'AddWatcher',
1214 NewValue => $principal->Id,
1215 Field => $args{'Type'}
1219 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1225 # {{{ sub DeleteWatcher
1227 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1230 Deletes a Ticket watcher. Takes two arguments:
1232 Type (one of Requestor,Cc,AdminCc)
1236 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1238 Email (the email address of an existing wathcer)
1247 my %args = ( Type => undef,
1248 PrincipalId => undef,
1252 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1253 return ( 0, $self->loc("No principal specified") );
1255 my $principal = RT::Principal->new( $self->CurrentUser );
1256 if ( $args{'PrincipalId'} ) {
1258 $principal->Load( $args{'PrincipalId'} );
1261 my $user = RT::User->new( $self->CurrentUser );
1262 $user->LoadByEmail( $args{'Email'} );
1263 $principal->Load( $user->Id );
1266 # If we can't find this watcher, we need to bail.
1267 unless ( $principal->Id ) {
1268 return ( 0, $self->loc("Could not find that principal") );
1271 my $group = RT::Group->new( $self->CurrentUser );
1272 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1273 unless ( $group->id ) {
1274 return ( 0, $self->loc("Group not found") );
1278 #If the watcher we're trying to add is for the current user
1279 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1281 # If it's an AdminCc and they don't have
1282 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1283 if ( $args{'Type'} eq 'AdminCc' ) {
1284 unless ( $self->CurrentUserHasRight('ModifyTicket')
1285 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1286 return ( 0, $self->loc('Permission Denied') );
1290 # If it's a Requestor or Cc and they don't have
1291 # 'Watch' or 'ModifyTicket', bail
1292 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1294 unless ( $self->CurrentUserHasRight('ModifyTicket')
1295 or $self->CurrentUserHasRight('Watch') ) {
1296 return ( 0, $self->loc('Permission Denied') );
1300 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1302 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1306 # If the watcher isn't the current user
1307 # and the current user doesn't have 'ModifyTicket' bail
1309 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1310 return ( 0, $self->loc("Permission Denied") );
1316 # see if this user is already a watcher.
1318 unless ( $group->HasMember($principal) ) {
1320 $self->loc( 'That principal is not a [_1] for this ticket',
1324 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1326 $RT::Logger->error( "Failed to delete "
1328 . " as a member of group "
1334 'Could not remove that principal as a [_1] for this ticket',
1338 unless ( $args{'Silent'} ) {
1339 $self->_NewTransaction( Type => 'DelWatcher',
1340 OldValue => $principal->Id,
1341 Field => $args{'Type'} );
1345 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1346 $principal->Object->Name,
1355 =head2 SquelchMailTo [EMAIL]
1357 Takes an optional email address to never email about updates to this ticket.
1360 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1368 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1372 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1377 return $self->_SquelchMailTo(@_);
1380 sub _SquelchMailTo {
1384 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1385 unless grep { $_->Content eq $attr }
1386 $self->Attributes->Named('SquelchMailTo');
1388 my @attributes = $self->Attributes->Named('SquelchMailTo');
1389 return (@attributes);
1393 =head2 UnsquelchMailTo ADDRESS
1395 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1397 Returns a tuple of (status, message)
1401 sub UnsquelchMailTo {
1404 my $address = shift;
1405 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1406 return ( 0, $self->loc("Permission Denied") );
1409 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1410 return ($val, $msg);
1414 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1416 =head2 RequestorAddresses
1418 B<Returns> String: All Ticket Requestor email addresses as a string.
1422 sub RequestorAddresses {
1425 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1429 return ( $self->Requestors->MemberEmailAddressesAsString );
1433 =head2 AdminCcAddresses
1435 returns String: All Ticket AdminCc email addresses as a string
1439 sub AdminCcAddresses {
1442 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1446 return ( $self->AdminCc->MemberEmailAddressesAsString )
1452 returns String: All Ticket Ccs as a string of email addresses
1459 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1462 return ( $self->Cc->MemberEmailAddressesAsString);
1468 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1470 # {{{ sub Requestors
1475 Returns this ticket's Requestors as an RT::Group object
1482 my $group = RT::Group->new($self->CurrentUser);
1483 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1484 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1492 # {{{ sub _Requestors
1496 Private non-ACLed variant of Reqeustors so that we can look them up for the
1497 purposes of customer auto-association during create.
1504 my $group = RT::Group->new($RT::SystemUser);
1505 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1516 Returns an RT::Group object which contains this ticket's Ccs.
1517 If the user doesn't have "ShowTicket" permission, returns an empty group
1524 my $group = RT::Group->new($self->CurrentUser);
1525 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1526 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1539 Returns an RT::Group object which contains this ticket's AdminCcs.
1540 If the user doesn't have "ShowTicket" permission, returns an empty group
1547 my $group = RT::Group->new($self->CurrentUser);
1548 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1549 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1559 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1562 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1564 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1566 Takes a param hash with the attributes Type and either PrincipalId or Email
1568 Type is one of Requestor, Cc, AdminCc and Owner
1570 PrincipalId is an RT::Principal id, and Email is an email address.
1572 Returns true if the specified principal (or the one corresponding to the
1573 specified address) is a member of the group Type for this ticket.
1575 XX TODO: This should be Memoized.
1582 my %args = ( Type => 'Requestor',
1583 PrincipalId => undef,
1588 # Load the relevant group.
1589 my $group = RT::Group->new($self->CurrentUser);
1590 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1592 # Find the relevant principal.
1593 if (!$args{PrincipalId} && $args{Email}) {
1594 # Look up the specified user.
1595 my $user = RT::User->new($self->CurrentUser);
1596 $user->LoadByEmail($args{Email});
1598 $args{PrincipalId} = $user->PrincipalId;
1601 # A non-existent user can't be a group member.
1606 # Ask if it has the member in question
1607 return $group->HasMember( $args{'PrincipalId'} );
1612 # {{{ sub IsRequestor
1614 =head2 IsRequestor PRINCIPAL_ID
1616 Takes an L<RT::Principal> id.
1618 Returns true if the principal is a requestor of the current ticket.
1626 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1634 =head2 IsCc PRINCIPAL_ID
1636 Takes an RT::Principal id.
1637 Returns true if the principal is a Cc of the current ticket.
1646 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1654 =head2 IsAdminCc PRINCIPAL_ID
1656 Takes an RT::Principal id.
1657 Returns true if the principal is an AdminCc of the current ticket.
1665 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1675 Takes an RT::User object. Returns true if that user is this ticket's owner.
1676 returns undef otherwise
1684 # no ACL check since this is used in acl decisions
1685 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1689 #Tickets won't yet have owners when they're being created.
1690 unless ( $self->OwnerObj->id ) {
1694 if ( $person->id == $self->OwnerObj->id ) {
1709 =head2 TransactionAddresses
1711 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1712 all this ticket's Create, Comment or Correspond transactions. The keys are
1713 stringified email addresses. Each value is an L<Email::Address> object.
1715 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.
1720 sub TransactionAddresses {
1722 my $txns = $self->Transactions;
1725 foreach my $type (qw(Create Comment Correspond)) {
1726 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1729 while (my $txn = $txns->Next) {
1730 my $txnaddrs = $txn->Addresses;
1731 foreach my $addrlist ( values %$txnaddrs ) {
1732 foreach my $addr (@$addrlist) {
1733 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1734 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1735 # skips "comment-only" addresses
1736 next unless ($addr->address);
1737 $addresses{$addr->address} = $addr;
1749 # {{{ Routines dealing with queues
1751 # {{{ sub ValidateQueue
1758 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1762 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1763 my $id = $QueueObj->Load($Value);
1779 my $NewQueue = shift;
1781 #Redundant. ACL gets checked in _Set;
1782 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1783 return ( 0, $self->loc("Permission Denied") );
1786 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1787 $NewQueueObj->Load($NewQueue);
1789 unless ( $NewQueueObj->Id() ) {
1790 return ( 0, $self->loc("That queue does not exist") );
1793 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1794 return ( 0, $self->loc('That is the same value') );
1797 $self->CurrentUser->HasRight(
1798 Right => 'CreateTicket',
1799 Object => $NewQueueObj
1803 return ( 0, $self->loc("You may not create requests in that queue.") );
1807 $self->OwnerObj->HasRight(
1808 Right => 'OwnTicket',
1809 Object => $NewQueueObj
1813 my $clone = RT::Ticket->new( $RT::SystemUser );
1814 $clone->Load( $self->Id );
1815 unless ( $clone->Id ) {
1816 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1818 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1819 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1822 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1825 # On queue change, change queue for reminders too
1826 my $reminder_collection = $self->Reminders->Collection;
1827 while ( my $reminder = $reminder_collection->Next ) {
1828 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1829 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1833 return ($status, $msg);
1842 Takes nothing. returns this ticket's queue object
1849 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1851 #We call __Value so that we can avoid the ACL decision and some deep recursion
1852 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1853 return ($queue_obj);
1860 # {{{ Date printing routines
1866 Returns an RT::Date object containing this ticket's due date
1873 my $time = new RT::Date( $self->CurrentUser );
1875 # -1 is RT::Date slang for never
1876 if ( my $due = $self->Due ) {
1877 $time->Set( Format => 'sql', Value => $due );
1880 $time->Set( Format => 'unix', Value => -1 );
1888 # {{{ sub DueAsString
1892 Returns this ticket's due date as a human readable string
1898 return $self->DueObj->AsString();
1903 # {{{ sub ResolvedObj
1907 Returns an RT::Date object of this ticket's 'resolved' time.
1914 my $time = new RT::Date( $self->CurrentUser );
1915 $time->Set( Format => 'sql', Value => $self->Resolved );
1921 # {{{ sub SetStarted
1925 Takes a date in ISO format or undef
1926 Returns a transaction id and a message
1927 The client calls "Start" to note that the project was started on the date in $date.
1928 A null date means "now"
1934 my $time = shift || 0;
1936 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1937 return ( 0, $self->loc("Permission Denied") );
1940 #We create a date object to catch date weirdness
1941 my $time_obj = new RT::Date( $self->CurrentUser() );
1943 $time_obj->Set( Format => 'ISO', Value => $time );
1946 $time_obj->SetToNow();
1949 #Now that we're starting, open this ticket
1950 #TODO do we really want to force this as policy? it should be a scrip
1952 #We need $TicketAsSystem, in case the current user doesn't have
1955 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1956 $TicketAsSystem->Load( $self->Id );
1957 if ( $TicketAsSystem->Status eq 'new' ) {
1958 $TicketAsSystem->Open();
1961 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1967 # {{{ sub StartedObj
1971 Returns an RT::Date object which contains this ticket's
1979 my $time = new RT::Date( $self->CurrentUser );
1980 $time->Set( Format => 'sql', Value => $self->Started );
1990 Returns an RT::Date object which contains this ticket's
1998 my $time = new RT::Date( $self->CurrentUser );
1999 $time->Set( Format => 'sql', Value => $self->Starts );
2009 Returns an RT::Date object which contains this ticket's
2017 my $time = new RT::Date( $self->CurrentUser );
2018 $time->Set( Format => 'sql', Value => $self->Told );
2024 # {{{ sub ToldAsString
2028 A convenience method that returns ToldObj->AsString
2030 TODO: This should be deprecated
2036 if ( $self->Told ) {
2037 return $self->ToldObj->AsString();
2046 # {{{ sub TimeWorkedAsString
2048 =head2 TimeWorkedAsString
2050 Returns the amount of time worked on this ticket as a Text String
2054 sub TimeWorkedAsString {
2056 my $value = $self->TimeWorked;
2058 # return the # of minutes worked turned into seconds and written as
2059 # a simple text string, this is not really a date object, but if we
2060 # diff a number of seconds vs the epoch, we'll get a nice description
2062 return "" unless $value;
2063 return RT::Date->new( $self->CurrentUser )
2064 ->DurationAsString( $value * 60 );
2069 # {{{ sub TimeLeftAsString
2071 =head2 TimeLeftAsString
2073 Returns the amount of time left on this ticket as a Text String
2077 sub TimeLeftAsString {
2079 my $value = $self->TimeLeft;
2080 return "" unless $value;
2081 return RT::Date->new( $self->CurrentUser )
2082 ->DurationAsString( $value * 60 );
2087 # {{{ Routines dealing with correspondence/comments
2093 Comment on this ticket.
2094 Takes a hash with the following attributes:
2095 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2098 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2100 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2101 They will, however, be prepared and you'll be able to access them through the TransactionObj
2103 Returns: Transaction id, Error Message, Transaction Object
2104 (note the different order from Create()!)
2111 my %args = ( CcMessageTo => undef,
2112 BccMessageTo => undef,
2119 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2120 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2121 return ( 0, $self->loc("Permission Denied"), undef );
2123 $args{'NoteType'} = 'Comment';
2125 if ($args{'DryRun'}) {
2126 $RT::Handle->BeginTransaction();
2127 $args{'CommitScrips'} = 0;
2130 my @results = $self->_RecordNote(%args);
2131 if ($args{'DryRun'}) {
2132 $RT::Handle->Rollback();
2139 # {{{ sub Correspond
2143 Correspond on this ticket.
2144 Takes a hashref with the following attributes:
2147 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2149 if there's no MIMEObj, Content is used to build a MIME::Entity object
2151 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2152 They will, however, be prepared and you'll be able to access them through the TransactionObj
2154 Returns: Transaction id, Error Message, Transaction Object
2155 (note the different order from Create()!)
2162 my %args = ( CcMessageTo => undef,
2163 BccMessageTo => undef,
2169 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2170 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2171 return ( 0, $self->loc("Permission Denied"), undef );
2174 $args{'NoteType'} = 'Correspond';
2175 if ($args{'DryRun'}) {
2176 $RT::Handle->BeginTransaction();
2177 $args{'CommitScrips'} = 0;
2180 my @results = $self->_RecordNote(%args);
2182 #Set the last told date to now if this isn't mail from the requestor.
2183 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2184 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2186 if ($args{'DryRun'}) {
2187 $RT::Handle->Rollback();
2196 # {{{ sub _RecordNote
2200 the meat of both comment and correspond.
2202 Performs no access control checks. hence, dangerous.
2209 CcMessageTo => undef,
2210 BccMessageTo => undef,
2215 NoteType => 'Correspond',
2221 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2222 return ( 0, $self->loc("No message attached"), undef );
2225 unless ( $args{'MIMEObj'} ) {
2226 $args{'MIMEObj'} = MIME::Entity->build(
2227 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2231 # convert text parts into utf-8
2232 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2234 # If we've been passed in CcMessageTo and BccMessageTo fields,
2235 # add them to the mime object for passing on to the transaction handler
2236 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2237 # RT-Send-Bcc: headers
2240 foreach my $type (qw/Cc Bcc/) {
2241 if ( defined $args{ $type . 'MessageTo' } ) {
2243 my $addresses = join ', ', (
2244 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2245 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2246 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2250 foreach my $argument (qw(Encrypt Sign)) {
2251 $args{'MIMEObj'}->head->add(
2252 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2253 ) if defined $args{ $argument };
2256 # If this is from an external source, we need to come up with its
2257 # internal Message-ID now, so all emails sent because of this
2258 # message have a common Message-ID
2259 my $org = RT->Config->Get('Organization');
2260 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2261 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2262 $args{'MIMEObj'}->head->set(
2263 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2267 #Record the correspondence (write the transaction)
2268 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2269 Type => $args{'NoteType'},
2270 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2271 TimeTaken => $args{'TimeTaken'},
2272 MIMEObj => $args{'MIMEObj'},
2273 CommitScrips => $args{'CommitScrips'},
2277 $RT::Logger->err("$self couldn't init a transaction $msg");
2278 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2281 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2293 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2296 my $type = shift || "";
2298 my $cache_key = "$field$type";
2299 return $self->{ $cache_key } if $self->{ $cache_key };
2301 my $links = $self->{ $cache_key }
2302 = RT::Links->new( $self->CurrentUser );
2303 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2304 $links->Limit( FIELD => 'id', VALUE => 0 );
2308 # Maybe this ticket is a merge ticket
2309 my $limit_on = 'Local'. $field;
2310 # at least to myself
2314 ENTRYAGGREGATOR => 'OR',
2319 ENTRYAGGREGATOR => 'OR',
2320 ) foreach $self->Merged;
2331 # {{{ sub DeleteLink
2335 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2336 SilentBase and SilentTarget. Either Base or Target must be null.
2337 The null value will be replaced with this ticket\'s id.
2339 If Silent is true then no transaction would be recorded, in other
2340 case you can control creation of transactions on both base and
2341 target with SilentBase and SilentTarget respectively. By default
2342 both transactions are created.
2353 SilentBase => undef,
2354 SilentTarget => undef,
2358 unless ( $args{'Target'} || $args{'Base'} ) {
2359 $RT::Logger->error("Base or Target must be specified");
2360 return ( 0, $self->loc('Either base or target must be specified') );
2365 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2366 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2367 return ( 0, $self->loc("Permission Denied") );
2370 # If the other URI is an RT::Ticket, we want to make sure the user
2371 # can modify it too...
2372 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2373 return (0, $msg) unless $status;
2374 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2377 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2378 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2380 return ( 0, $self->loc("Permission Denied") );
2383 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2384 return ( 0, $Msg ) unless $val;
2386 return ( $val, $Msg ) if $args{'Silent'};
2388 my ($direction, $remote_link);
2390 if ( $args{'Base'} ) {
2391 $remote_link = $args{'Base'};
2392 $direction = 'Target';
2394 elsif ( $args{'Target'} ) {
2395 $remote_link = $args{'Target'};
2396 $direction = 'Base';
2399 my $remote_uri = RT::URI->new( $self->CurrentUser );
2400 $remote_uri->FromURI( $remote_link );
2402 unless ( $args{ 'Silent'. $direction } ) {
2403 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2404 Type => 'DeleteLink',
2405 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2406 OldValue => $remote_uri->URI || $remote_link,
2409 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2412 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2413 my $OtherObj = $remote_uri->Object;
2414 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2415 Type => 'DeleteLink',
2416 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2417 : $LINKDIRMAP{$args{'Type'}}->{Target},
2418 OldValue => $self->URI,
2419 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2422 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2425 return ( $val, $Msg );
2434 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2436 If Silent is true then no transaction would be recorded, in other
2437 case you can control creation of transactions on both base and
2438 target with SilentBase and SilentTarget respectively. By default
2439 both transactions are created.
2445 my %args = ( Target => '',
2449 SilentBase => undef,
2450 SilentTarget => undef,
2453 unless ( $args{'Target'} || $args{'Base'} ) {
2454 $RT::Logger->error("Base or Target must be specified");
2455 return ( 0, $self->loc('Either base or target must be specified') );
2459 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2460 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2461 return ( 0, $self->loc("Permission Denied") );
2464 # If the other URI is an RT::Ticket, we want to make sure the user
2465 # can modify it too...
2466 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2467 return (0, $msg) unless $status;
2468 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2471 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2472 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2474 return ( 0, $self->loc("Permission Denied") );
2477 return $self->_AddLink(%args);
2480 sub __GetTicketFromURI {
2482 my %args = ( URI => '', @_ );
2484 # If the other URI is an RT::Ticket, we want to make sure the user
2485 # can modify it too...
2486 my $uri_obj = RT::URI->new( $self->CurrentUser );
2487 $uri_obj->FromURI( $args{'URI'} );
2489 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2490 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2491 $RT::Logger->warning( $msg );
2494 my $obj = $uri_obj->Resolver->Object;
2495 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2496 return (1, 'Found not a ticket', undef);
2498 return (1, 'Found ticket', $obj);
2503 Private non-acled variant of AddLink so that links can be added during create.
2509 my %args = ( Target => '',
2513 SilentBase => undef,
2514 SilentTarget => undef,
2517 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2518 return ($val, $msg) if !$val || $exist;
2519 return ($val, $msg) if $args{'Silent'};
2521 my ($direction, $remote_link);
2522 if ( $args{'Target'} ) {
2523 $remote_link = $args{'Target'};
2524 $direction = 'Base';
2525 } elsif ( $args{'Base'} ) {
2526 $remote_link = $args{'Base'};
2527 $direction = 'Target';
2530 my $remote_uri = RT::URI->new( $self->CurrentUser );
2531 $remote_uri->FromURI( $remote_link );
2533 unless ( $args{ 'Silent'. $direction } ) {
2534 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2536 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2537 NewValue => $remote_uri->URI || $remote_link,
2540 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2543 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2544 my $OtherObj = $remote_uri->Object;
2545 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2547 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2548 : $LINKDIRMAP{$args{'Type'}}->{Target},
2549 NewValue => $self->URI,
2550 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2553 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2556 return ( $val, $msg );
2566 MergeInto take the id of the ticket to merge this ticket into.
2572 my $ticket_id = shift;
2574 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2575 return ( 0, $self->loc("Permission Denied") );
2578 # Load up the new ticket.
2579 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2580 $MergeInto->Load($ticket_id);
2582 # make sure it exists.
2583 unless ( $MergeInto->Id ) {
2584 return ( 0, $self->loc("New ticket doesn't exist") );
2587 # Make sure the current user can modify the new ticket.
2588 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2589 return ( 0, $self->loc("Permission Denied") );
2592 delete $MERGE_CACHE{'effective'}{ $self->id };
2593 delete @{ $MERGE_CACHE{'merged'} }{
2594 $ticket_id, $MergeInto->id, $self->id
2597 $RT::Handle->BeginTransaction();
2599 # We use EffectiveId here even though it duplicates information from
2600 # the links table becasue of the massive performance hit we'd take
2601 # by trying to do a separate database query for merge info everytime
2604 #update this ticket's effective id to the new ticket's id.
2605 my ( $id_val, $id_msg ) = $self->__Set(
2606 Field => 'EffectiveId',
2607 Value => $MergeInto->Id()
2611 $RT::Handle->Rollback();
2612 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2616 if ( $self->__Value('Status') ne 'resolved' ) {
2618 my ( $status_val, $status_msg )
2619 = $self->__Set( Field => 'Status', Value => 'resolved' );
2621 unless ($status_val) {
2622 $RT::Handle->Rollback();
2625 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2629 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2633 # update all the links that point to that old ticket
2634 my $old_links_to = RT::Links->new($self->CurrentUser);
2635 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2638 while (my $link = $old_links_to->Next) {
2639 if (exists $old_seen{$link->Base."-".$link->Type}) {
2642 elsif ($link->Base eq $MergeInto->URI) {
2645 # First, make sure the link doesn't already exist. then move it over.
2646 my $tmp = RT::Link->new($RT::SystemUser);
2647 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2651 $link->SetTarget($MergeInto->URI);
2652 $link->SetLocalTarget($MergeInto->id);
2654 $old_seen{$link->Base."-".$link->Type} =1;
2659 my $old_links_from = RT::Links->new($self->CurrentUser);
2660 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2662 while (my $link = $old_links_from->Next) {
2663 if (exists $old_seen{$link->Type."-".$link->Target}) {
2666 if ($link->Target eq $MergeInto->URI) {
2669 # First, make sure the link doesn't already exist. then move it over.
2670 my $tmp = RT::Link->new($RT::SystemUser);
2671 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2675 $link->SetBase($MergeInto->URI);
2676 $link->SetLocalBase($MergeInto->id);
2677 $old_seen{$link->Type."-".$link->Target} =1;
2683 # Update time fields
2684 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2686 my $mutator = "Set$type";
2687 $MergeInto->$mutator(
2688 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2691 #add all of this ticket's watchers to that ticket.
2692 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2694 my $people = $self->$watcher_type->MembersObj;
2695 my $addwatcher_type = $watcher_type;
2696 $addwatcher_type =~ s/s$//;
2698 while ( my $watcher = $people->Next ) {
2700 my ($val, $msg) = $MergeInto->_AddWatcher(
2701 Type => $addwatcher_type,
2703 PrincipalId => $watcher->MemberId
2706 $RT::Logger->warning($msg);
2712 #find all of the tickets that were merged into this ticket.
2713 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2714 $old_mergees->Limit(
2715 FIELD => 'EffectiveId',
2720 # update their EffectiveId fields to the new ticket's id
2721 while ( my $ticket = $old_mergees->Next() ) {
2722 my ( $val, $msg ) = $ticket->__Set(
2723 Field => 'EffectiveId',
2724 Value => $MergeInto->Id()
2728 #make a new link: this ticket is merged into that other ticket.
2729 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2731 $MergeInto->_SetLastUpdated;
2733 $RT::Handle->Commit();
2734 return ( 1, $self->loc("Merge Successful") );
2739 Returns list of tickets' ids that's been merged into this ticket.
2747 return @{ $MERGE_CACHE{'merged'}{ $id } }
2748 if $MERGE_CACHE{'merged'}{ $id };
2750 my $mergees = RT::Tickets->new( $self->CurrentUser );
2752 FIELD => 'EffectiveId',
2760 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2761 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2768 # {{{ Routines dealing with ownership
2774 Takes nothing and returns an RT::User object of
2782 #If this gets ACLed, we lose on a rights check in User.pm and
2783 #get deep recursion. if we need ACLs here, we need
2784 #an equiv without ACLs
2786 my $owner = new RT::User( $self->CurrentUser );
2787 $owner->Load( $self->__Value('Owner') );
2789 #Return the owner object
2795 # {{{ sub OwnerAsString
2797 =head2 OwnerAsString
2799 Returns the owner's email address
2805 return ( $self->OwnerObj->EmailAddress );
2815 Takes two arguments:
2816 the Id or Name of the owner
2817 and (optionally) the type of the SetOwner Transaction. It defaults
2818 to 'Give'. 'Steal' is also a valid option.
2825 my $NewOwner = shift;
2826 my $Type = shift || "Give";
2828 $RT::Handle->BeginTransaction();
2830 $self->_SetLastUpdated(); # lock the ticket
2831 $self->Load( $self->id ); # in case $self changed while waiting for lock
2833 my $OldOwnerObj = $self->OwnerObj;
2835 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2836 $NewOwnerObj->Load( $NewOwner );
2837 unless ( $NewOwnerObj->Id ) {
2838 $RT::Handle->Rollback();
2839 return ( 0, $self->loc("That user does not exist") );
2843 # must have ModifyTicket rights
2844 # or TakeTicket/StealTicket and $NewOwner is self
2845 # see if it's a take
2846 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2847 unless ( $self->CurrentUserHasRight('ModifyTicket')
2848 || $self->CurrentUserHasRight('TakeTicket') ) {
2849 $RT::Handle->Rollback();
2850 return ( 0, $self->loc("Permission Denied") );
2854 # see if it's a steal
2855 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2856 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2858 unless ( $self->CurrentUserHasRight('ModifyTicket')
2859 || $self->CurrentUserHasRight('StealTicket') ) {
2860 $RT::Handle->Rollback();
2861 return ( 0, $self->loc("Permission Denied") );
2865 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2866 $RT::Handle->Rollback();
2867 return ( 0, $self->loc("Permission Denied") );
2871 # If we're not stealing and the ticket has an owner and it's not
2873 if ( $Type ne 'Steal' and $Type ne 'Force'
2874 and $OldOwnerObj->Id != $RT::Nobody->Id
2875 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2877 $RT::Handle->Rollback();
2878 return ( 0, $self->loc("You can only take tickets that are unowned") )
2879 if $NewOwnerObj->id == $self->CurrentUser->id;
2882 $self->loc("You can only reassign tickets that you own or that are unowned" )
2886 #If we've specified a new owner and that user can't modify the ticket
2887 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2888 $RT::Handle->Rollback();
2889 return ( 0, $self->loc("That user may not own tickets in that queue") );
2892 # If the ticket has an owner and it's the new owner, we don't need
2894 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2895 $RT::Handle->Rollback();
2896 return ( 0, $self->loc("That user already owns that ticket") );
2899 # Delete the owner in the owner group, then add a new one
2900 # TODO: is this safe? it's not how we really want the API to work
2901 # for most things, but it's fast.
2902 my ( $del_id, $del_msg );
2903 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2904 ($del_id, $del_msg) = $owner->Delete();
2905 last unless ($del_id);
2909 $RT::Handle->Rollback();
2910 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2913 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2914 PrincipalId => $NewOwnerObj->PrincipalId,
2915 InsideTransaction => 1 );
2917 $RT::Handle->Rollback();
2918 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2921 # We call set twice with slightly different arguments, so
2922 # as to not have an SQL transaction span two RT transactions
2924 my ( $val, $msg ) = $self->_Set(
2926 RecordTransaction => 0,
2927 Value => $NewOwnerObj->Id,
2929 TransactionType => $Type,
2930 CheckACL => 0, # don't check acl
2934 $RT::Handle->Rollback;
2935 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2938 ($val, $msg) = $self->_NewTransaction(
2941 NewValue => $NewOwnerObj->Id,
2942 OldValue => $OldOwnerObj->Id,
2947 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2948 $OldOwnerObj->Name, $NewOwnerObj->Name );
2951 $RT::Handle->Rollback();
2955 $RT::Handle->Commit();
2957 return ( $val, $msg );
2966 A convenince method to set the ticket's owner to the current user
2972 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2981 Convenience method to set the owner to 'nobody' if the current user is the owner.
2987 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2996 A convenience method to change the owner of the current ticket to the
2997 current user. Even if it's owned by another user.
3004 if ( $self->IsOwner( $self->CurrentUser ) ) {
3005 return ( 0, $self->loc("You already own this ticket") );
3008 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3018 # {{{ Routines dealing with status
3020 # {{{ sub ValidateStatus
3022 =head2 ValidateStatus STATUS
3024 Takes a string. Returns true if that status is a valid status for this ticket.
3025 Returns false otherwise.
3029 sub ValidateStatus {
3033 #Make sure the status passed in is valid
3034 unless ( $self->QueueObj->IsValidStatus($status) ) {
3046 =head2 SetStatus STATUS
3048 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3050 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.
3061 $args{Status} = shift;
3068 if ( $args{Status} eq 'deleted') {
3069 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3070 return ( 0, $self->loc('Permission Denied') );
3073 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3074 return ( 0, $self->loc('Permission Denied') );
3078 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3079 return (0, $self->loc('That ticket has unresolved dependencies'));
3082 my $now = RT::Date->new( $self->CurrentUser );
3085 #If we're changing the status from new, record that we've started
3086 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3088 #Set the Started time to "now"
3089 $self->_Set( Field => 'Started',
3091 RecordTransaction => 0 );
3094 #When we close a ticket, set the 'Resolved' attribute to now.
3095 # It's misnamed, but that's just historical.
3096 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3097 $self->_Set( Field => 'Resolved',
3099 RecordTransaction => 0 );
3102 #Actually update the status
3103 my ($val, $msg)= $self->_Set( Field => 'Status',
3104 Value => $args{Status},
3107 TransactionType => 'Status' );
3118 Takes no arguments. Marks this ticket for garbage collection
3124 return ( $self->SetStatus('deleted') );
3126 # TODO: garbage collection
3135 Sets this ticket's status to stalled
3141 return ( $self->SetStatus('stalled') );
3150 Sets this ticket's status to rejected
3156 return ( $self->SetStatus('rejected') );
3165 Sets this ticket\'s status to Open
3171 return ( $self->SetStatus('open') );
3180 Sets this ticket\'s status to Resolved
3186 return ( $self->SetStatus('resolved') );
3194 # {{{ Actions + Routines dealing with transactions
3196 # {{{ sub SetTold and _SetTold
3198 =head2 SetTold ISO [TIMETAKEN]
3200 Updates the told and records a transaction
3207 $told = shift if (@_);
3208 my $timetaken = shift || 0;
3210 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3211 return ( 0, $self->loc("Permission Denied") );
3214 my $datetold = new RT::Date( $self->CurrentUser );
3216 $datetold->Set( Format => 'iso',
3220 $datetold->SetToNow();
3223 return ( $self->_Set( Field => 'Told',
3224 Value => $datetold->ISO,
3225 TimeTaken => $timetaken,
3226 TransactionType => 'Told' ) );
3231 Updates the told without a transaction or acl check. Useful when we're sending replies.
3238 my $now = new RT::Date( $self->CurrentUser );
3241 #use __Set to get no ACLs ;)
3242 return ( $self->__Set( Field => 'Told',
3243 Value => $now->ISO ) );
3253 my $uid = $self->CurrentUser->id;
3254 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3255 return if $attr && $attr->Content gt $self->LastUpdated;
3257 my $txns = $self->Transactions;
3258 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3259 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3260 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3264 VALUE => $attr->Content
3266 $txns->RowsPerPage(1);
3267 return $txns->First;
3272 =head2 TransactionBatch
3274 Returns an array reference of all transactions created on this ticket during
3275 this ticket object's lifetime or since last application of a batch, or undef
3278 Only works when the C<UseTransactionBatch> config option is set to true.
3282 sub TransactionBatch {
3284 return $self->{_TransactionBatch};
3287 =head2 ApplyTransactionBatch
3289 Applies scrips on the current batch of transactions and shinks it. Usually
3290 batch is applied when object is destroyed, but in some cases it's too late.
3294 sub ApplyTransactionBatch {
3297 my $batch = $self->TransactionBatch;
3298 return unless $batch && @$batch;
3300 $self->_ApplyTransactionBatch;
3302 $self->{_TransactionBatch} = [];
3305 sub _ApplyTransactionBatch {
3307 my $batch = $self->TransactionBatch;
3310 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3313 RT::Scrips->new($RT::SystemUser)->Apply(
3314 Stage => 'TransactionBatch',
3316 TransactionObj => $batch->[0],
3320 # Entry point of the rule system
3321 my $rules = RT::Ruleset->FindAllRules(
3322 Stage => 'TransactionBatch',
3324 TransactionObj => $batch->[0],
3327 RT::Ruleset->CommitRules($rules);
3333 # DESTROY methods need to localize $@, or it may unset it. This
3334 # causes $m->abort to not bubble all of the way up. See perlbug
3335 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3338 # The following line eliminates reentrancy.
3339 # It protects against the fact that perl doesn't deal gracefully
3340 # when an object's refcount is changed in its destructor.
3341 return if $self->{_Destroyed}++;
3343 my $batch = $self->TransactionBatch;
3344 return unless $batch && @$batch;
3346 return $self->_ApplyTransactionBatch;
3351 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3353 # {{{ sub _OverlayAccessible
3355 sub _OverlayAccessible {
3357 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3358 Queue => { 'read' => 1, 'write' => 1 },
3359 Requestors => { 'read' => 1, 'write' => 1 },
3360 Owner => { 'read' => 1, 'write' => 1 },
3361 Subject => { 'read' => 1, 'write' => 1 },
3362 InitialPriority => { 'read' => 1, 'write' => 1 },
3363 FinalPriority => { 'read' => 1, 'write' => 1 },
3364 Priority => { 'read' => 1, 'write' => 1 },
3365 Status => { 'read' => 1, 'write' => 1 },
3366 TimeEstimated => { 'read' => 1, 'write' => 1 },
3367 TimeWorked => { 'read' => 1, 'write' => 1 },
3368 TimeLeft => { 'read' => 1, 'write' => 1 },
3369 Told => { 'read' => 1, 'write' => 1 },
3370 Resolved => { 'read' => 1 },
3371 Type => { 'read' => 1 },
3372 Starts => { 'read' => 1, 'write' => 1 },
3373 Started => { 'read' => 1, 'write' => 1 },
3374 Due => { 'read' => 1, 'write' => 1 },
3375 Creator => { 'read' => 1, 'auto' => 1 },
3376 Created => { 'read' => 1, 'auto' => 1 },
3377 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3378 LastUpdated => { 'read' => 1, 'auto' => 1 }
3390 my %args = ( Field => undef,
3393 RecordTransaction => 1,
3396 TransactionType => 'Set',
3399 if ($args{'CheckACL'}) {
3400 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3401 return ( 0, $self->loc("Permission Denied"));
3405 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3406 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3407 return(0, $self->loc("Internal Error"));
3410 #if the user is trying to modify the record
3412 #Take care of the old value we really don't want to get in an ACL loop.
3413 # so ask the super::_Value
3414 my $Old = $self->SUPER::_Value("$args{'Field'}");
3417 if ( $args{'UpdateTicket'} ) {
3420 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3421 Value => $args{'Value'} );
3423 #If we can't actually set the field to the value, don't record
3424 # a transaction. instead, get out of here.
3425 return ( 0, $msg ) unless $ret;
3428 if ( $args{'RecordTransaction'} == 1 ) {
3430 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3431 Type => $args{'TransactionType'},
3432 Field => $args{'Field'},
3433 NewValue => $args{'Value'},
3435 TimeTaken => $args{'TimeTaken'},
3437 return ( $Trans, scalar $TransObj->BriefDescription );
3440 return ( $ret, $msg );
3450 Takes the name of a table column.
3451 Returns its value as a string, if the user passes an ACL check
3460 #if the field is public, return it.
3461 if ( $self->_Accessible( $field, 'public' ) ) {
3463 #$RT::Logger->debug("Skipping ACL check for $field");
3464 return ( $self->SUPER::_Value($field) );
3468 #If the current user doesn't have ACLs, don't let em at it.
3470 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3473 return ( $self->SUPER::_Value($field) );
3479 # {{{ sub _UpdateTimeTaken
3481 =head2 _UpdateTimeTaken
3483 This routine will increment the timeworked counter. it should
3484 only be called from _NewTransaction
3488 sub _UpdateTimeTaken {
3490 my $Minutes = shift;
3493 $Total = $self->SUPER::_Value("TimeWorked");
3494 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3496 Field => "TimeWorked",
3507 # {{{ Routines dealing with ACCESS CONTROL
3509 # {{{ sub CurrentUserHasRight
3511 =head2 CurrentUserHasRight
3513 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3514 1 if the user has that right. It returns 0 if the user doesn't have that right.
3518 sub CurrentUserHasRight {
3522 return $self->CurrentUser->PrincipalObj->HasRight(
3534 Takes a paramhash with the attributes 'Right' and 'Principal'
3535 'Right' is a ticket-scoped textual right from RT::ACE
3536 'Principal' is an RT::User object
3538 Returns 1 if the principal has the right. Returns undef if not.
3550 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3552 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3553 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3558 $args{'Principal'}->HasRight(
3560 Right => $args{'Right'}
3571 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3572 It isn't acutally a searchbuilder collection itself.
3579 unless ($self->{'__reminders'}) {
3580 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3581 $self->{'__reminders'}->Ticket($self->id);
3583 return $self->{'__reminders'};
3589 # {{{ sub Transactions
3593 Returns an RT::Transactions object of all transactions on this ticket
3600 my $transactions = RT::Transactions->new( $self->CurrentUser );
3602 #If the user has no rights, return an empty object
3603 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3604 $transactions->LimitToTicket($self->id);
3606 # if the user may not see comments do not return them
3607 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3608 $transactions->Limit(
3614 $transactions->Limit(
3618 VALUE => "CommentEmailRecord",
3619 ENTRYAGGREGATOR => 'AND'
3624 $transactions->Limit(
3628 ENTRYAGGREGATOR => 'AND'
3632 return ($transactions);
3638 # {{{ TransactionCustomFields
3640 =head2 TransactionCustomFields
3642 Returns the custom fields that transactions on tickets will have.
3646 sub TransactionCustomFields {
3648 return $self->QueueObj->TicketTransactionCustomFields;
3653 # {{{ sub CustomFieldValues
3655 =head2 CustomFieldValues
3657 # Do name => id mapping (if needed) before falling back to
3658 # RT::Record's CustomFieldValues
3664 sub CustomFieldValues {
3668 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3670 my $cf = RT::CustomField->new( $self->CurrentUser );
3671 $cf->SetContextObject( $self );
3672 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3673 unless ( $cf->id ) {
3674 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3677 # If we didn't find a valid cfid, give up.
3678 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3680 return $self->SUPER::CustomFieldValues( $cf->id );
3685 # {{{ sub CustomFieldLookupType
3687 =head2 CustomFieldLookupType
3689 Returns the RT::Ticket lookup type, which can be passed to
3690 RT::CustomField->Create() via the 'LookupType' hash key.
3696 sub CustomFieldLookupType {
3697 "RT::Queue-RT::Ticket";
3700 =head2 ACLEquivalenceObjects
3702 This method returns a list of objects for which a user's rights also apply
3703 to this ticket. Generally, this is only the ticket's queue, but some RT
3704 extensions may make other objects available too.
3706 This method is called from L<RT::Principal/HasRight>.
3710 sub ACLEquivalenceObjects {
3712 return $self->QueueObj;
3721 Jesse Vincent, jesse@bestpractical.com