1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
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;
84 use RT::URI::freeside;
89 # A helper table for links mapping to make it easier
90 # to build and parse links between tickets
93 MemberOf => { Type => 'MemberOf',
95 Parents => { Type => 'MemberOf',
97 Members => { Type => 'MemberOf',
99 Children => { Type => 'MemberOf',
101 HasMember => { Type => 'MemberOf',
103 RefersTo => { Type => 'RefersTo',
105 ReferredToBy => { Type => 'RefersTo',
107 DependsOn => { Type => 'DependsOn',
109 DependedOnBy => { Type => 'DependsOn',
111 MergedInto => { Type => 'MergedInto',
119 # A helper table for links mapping to make it easier
120 # to build and parse links between tickets
123 MemberOf => { Base => 'MemberOf',
124 Target => 'HasMember', },
125 RefersTo => { Base => 'RefersTo',
126 Target => 'ReferredToBy', },
127 DependsOn => { Base => 'DependsOn',
128 Target => 'DependedOnBy', },
129 MergedInto => { Base => 'MergedInto',
130 Target => 'MergedInto', },
136 sub LINKTYPEMAP { return \%LINKTYPEMAP }
137 sub LINKDIRMAP { return \%LINKDIRMAP }
148 Takes a single argument. This can be a ticket id, ticket alias or
149 local ticket uri. If the ticket can't be loaded, returns undef.
150 Otherwise, returns the ticket id.
157 $id = '' unless defined $id;
159 # TODO: modify this routine to look at EffectiveId and
160 # do the recursive load thing. be careful to cache all
161 # the interim tickets we try so we don't loop forever.
163 # FIXME: there is no TicketBaseURI option in config
164 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
165 #If it's a local URI, turn it into a ticket id
166 if ( $base_uri && $id =~ /^$base_uri(\d+)$/ ) {
170 unless ( $id =~ /^\d+$/ ) {
171 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
175 $id = $MERGE_CACHE{'effective'}{ $id }
176 if $MERGE_CACHE{'effective'}{ $id };
178 my ($ticketid, $msg) = $self->LoadById( $id );
179 unless ( $self->Id ) {
180 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
184 #If we're merged, resolve the merge.
185 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
187 "We found a merged ticket. "
188 . $self->id ."/". $self->EffectiveId
190 my $real_id = $self->Load( $self->EffectiveId );
191 $MERGE_CACHE{'effective'}{ $id } = $real_id;
195 #Ok. we're loaded. lets get outa here.
205 Arguments: ARGS is a hash of named parameters. Valid parameters are:
208 Queue - Either a Queue object or a Queue Name
209 Requestor - A reference to a list of email addresses or RT user Names
210 Cc - A reference to a list of email addresses or Names
211 AdminCc - A reference to a list of email addresses or Names
212 SquelchMailTo - A reference to a list of email addresses -
213 who should this ticket not mail
214 Type -- The ticket\'s type. ignore this for now
215 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
216 Subject -- A string describing the subject of the ticket
217 Priority -- an integer from 0 to 99
218 InitialPriority -- an integer from 0 to 99
219 FinalPriority -- an integer from 0 to 99
220 Status -- any valid status (Defined in RT::Queue)
221 TimeEstimated -- an integer. estimated time for this task in minutes
222 TimeWorked -- an integer. time worked so far in minutes
223 TimeLeft -- an integer. time remaining in minutes
224 Starts -- an ISO date describing the ticket\'s start date and time in GMT
225 Due -- an ISO date describing the ticket\'s due date and time in GMT
226 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
227 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
229 Ticket links can be set up during create by passing the link type as a hask key and
230 the ticket id to be linked to as a value (or a URI when linking to other objects).
231 Multiple links of the same type can be created by passing an array ref. For example:
234 DependsOn => [ 15, 22 ],
235 RefersTo => 'http://www.bestpractical.com',
237 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
238 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
239 C<Members> and C<Children> are aliases for C<HasMember>.
241 Returns: TICKETID, Transaction Object, Error Message
251 EffectiveId => undef,
256 SquelchMailTo => undef,
260 InitialPriority => undef,
261 FinalPriority => undef,
272 _RecordTransaction => 1,
277 my ($ErrStr, @non_fatal_errors);
279 my $QueueObj = RT::Queue->new( $RT::SystemUser );
280 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
281 $QueueObj->Load( $args{'Queue'}->Id );
283 elsif ( $args{'Queue'} ) {
284 $QueueObj->Load( $args{'Queue'} );
287 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
290 #Can't create a ticket without a queue.
291 unless ( $QueueObj->Id ) {
292 $RT::Logger->debug("$self No queue given for ticket creation.");
293 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
297 #Now that we have a queue, Check the ACLS
299 $self->CurrentUser->HasRight(
300 Right => 'CreateTicket',
307 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
310 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
311 return ( 0, 0, $self->loc('Invalid value for status') );
314 #Since we have a queue, we can set queue defaults
317 # If there's no queue default initial priority and it's not set, set it to 0
318 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
319 unless defined $args{'InitialPriority'};
322 # If there's no queue default final priority and it's not set, set it to 0
323 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
324 unless defined $args{'FinalPriority'};
326 # Priority may have changed from InitialPriority, for the case
327 # where we're importing tickets (eg, from an older RT version.)
328 $args{'Priority'} = $args{'InitialPriority'}
329 unless defined $args{'Priority'};
332 #TODO we should see what sort of due date we're getting, rather +
333 # than assuming it's in ISO format.
335 #Set the due date. if we didn't get fed one, use the queue default due in
336 my $Due = new RT::Date( $self->CurrentUser );
337 if ( defined $args{'Due'} ) {
338 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
340 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
342 $Due->AddDays( $due_in );
345 my $Starts = new RT::Date( $self->CurrentUser );
346 if ( defined $args{'Starts'} ) {
347 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
350 my $Started = new RT::Date( $self->CurrentUser );
351 if ( defined $args{'Started'} ) {
352 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
354 elsif ( $args{'Status'} ne 'new' ) {
358 my $Resolved = new RT::Date( $self->CurrentUser );
359 if ( defined $args{'Resolved'} ) {
360 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
363 #If the status is an inactive status, set the resolved date
364 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
366 $RT::Logger->debug( "Got a ". $args{'Status'}
367 ."(inactive) ticket with undefined resolved date. Setting to now."
374 # {{{ Dealing with time fields
376 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
377 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
378 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
382 # {{{ Deal with setting the owner
385 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
386 if ( $args{'Owner'}->id ) {
387 $Owner = $args{'Owner'};
389 $RT::Logger->error('passed not loaded owner object');
390 push @non_fatal_errors, $self->loc("Invalid owner object");
395 #If we've been handed something else, try to load the user.
396 elsif ( $args{'Owner'} ) {
397 $Owner = RT::User->new( $self->CurrentUser );
398 $Owner->Load( $args{'Owner'} );
399 $Owner->LoadByEmail( $args{'Owner'} )
401 unless ( $Owner->Id ) {
402 push @non_fatal_errors,
403 $self->loc("Owner could not be set.") . " "
404 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
409 #If we have a proposed owner and they don't have the right
410 #to own a ticket, scream about it and make them not the owner
413 if ( $Owner && $Owner->Id != $RT::Nobody->Id
414 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
416 $DeferOwner = $Owner;
418 $RT::Logger->debug('going to deffer setting owner');
422 #If we haven't been handed a valid owner, make it nobody.
423 unless ( defined($Owner) && $Owner->Id ) {
424 $Owner = new RT::User( $self->CurrentUser );
425 $Owner->Load( $RT::Nobody->Id );
430 # We attempt to load or create each of the people who might have a role for this ticket
431 # _outside_ the transaction, so we don't get into ticket creation races
432 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
433 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
434 foreach my $watcher ( splice @{ $args{$type} } ) {
435 next unless $watcher;
436 if ( $watcher =~ /^\d+$/ ) {
437 push @{ $args{$type} }, $watcher;
439 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
440 foreach my $address( @addresses ) {
441 my $user = RT::User->new( $RT::SystemUser );
442 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
444 push @non_fatal_errors,
445 $self->loc("Couldn't load or create user: [_1]", $msg);
447 push @{ $args{$type} }, $user->id;
454 $args{'Subject'} =~ s/\n//g;
456 $RT::Handle->BeginTransaction();
459 Queue => $QueueObj->Id,
461 Subject => $args{'Subject'},
462 InitialPriority => $args{'InitialPriority'},
463 FinalPriority => $args{'FinalPriority'},
464 Priority => $args{'Priority'},
465 Status => $args{'Status'},
466 TimeWorked => $args{'TimeWorked'},
467 TimeEstimated => $args{'TimeEstimated'},
468 TimeLeft => $args{'TimeLeft'},
469 Type => $args{'Type'},
470 Starts => $Starts->ISO,
471 Started => $Started->ISO,
472 Resolved => $Resolved->ISO,
476 # Parameters passed in during an import that we probably don't want to touch, otherwise
477 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
478 $params{$attr} = $args{$attr} if $args{$attr};
481 # Delete null integer parameters
483 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
485 delete $params{$attr}
486 unless ( exists $params{$attr} && $params{$attr} );
489 # Delete the time worked if we're counting it in the transaction
490 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
492 my ($id,$ticket_message) = $self->SUPER::Create( %params );
494 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
495 $RT::Handle->Rollback();
497 $self->loc("Ticket could not be created due to an internal error")
501 #Set the ticket's effective ID now that we've created it.
502 my ( $val, $msg ) = $self->__Set(
503 Field => 'EffectiveId',
504 Value => ( $args{'EffectiveId'} || $id )
507 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
508 $RT::Handle->Rollback;
510 $self->loc("Ticket could not be created due to an internal error")
514 my $create_groups_ret = $self->_CreateTicketGroups();
515 unless ($create_groups_ret) {
516 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
518 . ". aborting Ticket creation." );
519 $RT::Handle->Rollback();
521 $self->loc("Ticket could not be created due to an internal error")
525 # Set the owner in the Groups table
526 # We denormalize it into the Ticket table too because doing otherwise would
527 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
528 $self->OwnerGroup->_AddMember(
529 PrincipalId => $Owner->PrincipalId,
530 InsideTransaction => 1
531 ) unless $DeferOwner;
535 # {{{ Deal with setting up watchers
537 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
538 # we know it's an array ref
539 foreach my $watcher ( @{ $args{$type} } ) {
541 # Note that we're using AddWatcher, rather than _AddWatcher, as we
542 # actually _want_ that ACL check. Otherwise, random ticket creators
543 # could make themselves adminccs and maybe get ticket rights. that would
545 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
547 my ($val, $msg) = $self->$method(
549 PrincipalId => $watcher,
552 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
557 if ($args{'SquelchMailTo'}) {
558 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
559 : $args{'SquelchMailTo'};
560 $self->_SquelchMailTo( @squelch );
566 # {{{ Add all the custom fields
568 foreach my $arg ( keys %args ) {
569 next unless $arg =~ /^CustomField-(\d+)$/i;
573 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
575 next unless defined $value && length $value;
577 # Allow passing in uploaded LargeContent etc by hash reference
578 my ($status, $msg) = $self->_AddCustomFieldValue(
579 (UNIVERSAL::isa( $value => 'HASH' )
584 RecordTransaction => 0,
586 push @non_fatal_errors, $msg unless $status;
592 # {{{ Deal with setting up links
594 # TODO: Adding link may fire scrips on other end and those scrips
595 # could create transactions on this ticket before 'Create' transaction.
597 # We should implement different schema: record 'Create' transaction,
598 # create links and only then fire create transaction's scrips.
600 # Ideal variant: add all links without firing scrips, record create
601 # transaction and only then fire scrips on the other ends of links.
605 foreach my $type ( keys %LINKTYPEMAP ) {
606 next unless ( defined $args{$type} );
608 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
610 # Check rights on the other end of the link if we must
611 # then run _AddLink that doesn't check for ACLs
612 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
613 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
615 push @non_fatal_errors, $msg;
618 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
619 push @non_fatal_errors, $self->loc('Linking. Permission denied');
624 #don't show transactions for reminders
625 my $silent = ( !$args{'_RecordTransaction'}
626 || $self->Type eq 'reminder'
629 my ( $wval, $wmsg ) = $self->_AddLink(
630 Type => $LINKTYPEMAP{$type}->{'Type'},
631 $LINKTYPEMAP{$type}->{'Mode'} => $link,
633 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
637 push @non_fatal_errors, $wmsg unless ($wval);
643 # {{{ Deal with auto-customer association
645 #unless we already have (a) customer(s)...
646 unless ( $self->Customers->Count ) {
648 #first find any requestors with emails but *without* customer targets
649 my @NoCust_Requestors =
650 grep { $_->EmailAddress && ! $_->Customers->Count }
651 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
653 for my $Requestor (@NoCust_Requestors) {
655 #perhaps the stuff in here should be in a User method??
657 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
659 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
661 ## false laziness w/RT/Interface/Web_Vendor.pm
662 my @link = ( 'Type' => 'MemberOf',
663 'Target' => "freeside://freeside/cust_main/$custnum",
666 my( $val, $msg ) = $Requestor->_AddLink(@link);
667 #XXX should do something with $msg# push @non_fatal_errors, $msg;
673 #find any requestors with customer targets
675 my %cust_target = ();
678 grep { $_->Customers->Count }
679 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
681 foreach my $Requestor ( @Requestors ) {
682 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
683 $cust_target{ $cust_link->Target } = 1;
687 #and then auto-associate this ticket with those customers
689 foreach my $cust_target ( keys %cust_target ) {
691 my @link = ( 'Type' => 'MemberOf',
692 #'Target' => "freeside://freeside/cust_main/$custnum",
693 'Target' => $cust_target,
696 my( $val, $msg ) = $self->_AddLink(@link);
697 push @non_fatal_errors, $msg;
705 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
706 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
708 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
710 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
711 . ") was proposed as a ticket owner but has no rights to own "
712 . "tickets in " . $QueueObj->Name );
713 push @non_fatal_errors, $self->loc(
714 "Owner '[_1]' does not have rights to own this ticket.",
718 $Owner = $DeferOwner;
719 $self->__Set(Field => 'Owner', Value => $Owner->id);
721 $self->OwnerGroup->_AddMember(
722 PrincipalId => $Owner->PrincipalId,
723 InsideTransaction => 1
727 #don't make a transaction or fire off any scrips for reminders either
728 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
730 # {{{ Add a transaction for the create
731 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
733 TimeTaken => $args{'TimeWorked'},
734 MIMEObj => $args{'MIMEObj'},
735 CommitScrips => !$args{'DryRun'},
738 if ( $self->Id && $Trans ) {
740 #$TransObj->UpdateCustomFields(ARGSRef => \%args);
741 $TransObj->UpdateCustomFields(%args);
743 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
744 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
745 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
748 $RT::Handle->Rollback();
750 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
751 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
752 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
755 if ( $args{'DryRun'} ) {
756 $RT::Handle->Rollback();
757 return ($self->id, $TransObj, $ErrStr);
759 $RT::Handle->Commit();
760 return ( $self->Id, $TransObj->Id, $ErrStr );
766 # Not going to record a transaction
767 $RT::Handle->Commit();
768 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
769 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
770 return ( $self->Id, 0, $ErrStr );
778 # {{{ _Parse822HeadersForAttributes Content
780 =head2 _Parse822HeadersForAttributes Content
782 Takes an RFC822 style message and parses its attributes into a hash.
786 sub _Parse822HeadersForAttributes {
791 my @lines = ( split ( /\n/, $content ) );
792 while ( defined( my $line = shift @lines ) ) {
793 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
798 if ( defined( $args{$tag} ) )
799 { #if we're about to get a second value, make it an array
800 $args{$tag} = [ $args{$tag} ];
802 if ( ref( $args{$tag} ) )
803 { #If it's an array, we want to push the value
804 push @{ $args{$tag} }, $value;
806 else { #if there's nothing there, just set the value
807 $args{$tag} = $value;
809 } elsif ($line =~ /^$/) {
811 #TODO: this won't work, since "" isn't of the form "foo:value"
813 while ( defined( my $l = shift @lines ) ) {
814 push @{ $args{'content'} }, $l;
820 foreach my $date (qw(due starts started resolved)) {
821 my $dateobj = RT::Date->new($RT::SystemUser);
822 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
823 $dateobj->Set( Format => 'unix', Value => $args{$date} );
826 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
828 $args{$date} = $dateobj->ISO;
830 $args{'mimeobj'} = MIME::Entity->new();
831 $args{'mimeobj'}->build(
832 Type => ( $args{'contenttype'} || 'text/plain' ),
833 Data => ($args{'content'} || '')
843 =head2 Import PARAMHASH
846 Doesn\'t create a transaction.
847 Doesn\'t supply queue defaults, etc.
855 my ( $ErrStr, $QueueObj, $Owner );
859 EffectiveId => undef,
863 Owner => $RT::Nobody->Id,
864 Subject => '[no subject]',
865 InitialPriority => undef,
866 FinalPriority => undef,
877 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
878 $QueueObj = RT::Queue->new($RT::SystemUser);
879 $QueueObj->Load( $args{'Queue'} );
881 #TODO error check this and return 0 if it\'s not loading properly +++
883 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
884 $QueueObj = RT::Queue->new($RT::SystemUser);
885 $QueueObj->Load( $args{'Queue'}->Id );
889 "$self " . $args{'Queue'} . " not a recognised queue object." );
892 #Can't create a ticket without a queue.
893 unless ( defined($QueueObj) and $QueueObj->Id ) {
894 $RT::Logger->debug("$self No queue given for ticket creation.");
895 return ( 0, $self->loc('Could not create ticket. Queue not set') );
898 #Now that we have a queue, Check the ACLS
900 $self->CurrentUser->HasRight(
901 Right => 'CreateTicket',
907 $self->loc("No permission to create tickets in the queue '[_1]'"
911 # {{{ Deal with setting the owner
913 # Attempt to take user object, user name or user id.
914 # Assign to nobody if lookup fails.
915 if ( defined( $args{'Owner'} ) ) {
916 if ( ref( $args{'Owner'} ) ) {
917 $Owner = $args{'Owner'};
920 $Owner = new RT::User( $self->CurrentUser );
921 $Owner->Load( $args{'Owner'} );
922 if ( !defined( $Owner->id ) ) {
923 $Owner->Load( $RT::Nobody->id );
928 #If we have a proposed owner and they don't have the right
929 #to own a ticket, scream about it and make them not the owner
932 and ( $Owner->Id != $RT::Nobody->Id )
942 $RT::Logger->warning( "$self user "
946 . "as a ticket owner but has no rights to own "
948 . $QueueObj->Name . "'" );
953 #If we haven't been handed a valid owner, make it nobody.
954 unless ( defined($Owner) ) {
955 $Owner = new RT::User( $self->CurrentUser );
956 $Owner->Load( $RT::Nobody->UserObj->Id );
961 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
962 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
965 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
966 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
967 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
968 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
970 # If we're coming in with an id, set that now.
971 my $EffectiveId = undef;
973 $EffectiveId = $args{'id'};
977 my $id = $self->SUPER::Create(
979 EffectiveId => $EffectiveId,
980 Queue => $QueueObj->Id,
982 Subject => $args{'Subject'}, # loc
983 InitialPriority => $args{'InitialPriority'}, # loc
984 FinalPriority => $args{'FinalPriority'}, # loc
985 Priority => $args{'InitialPriority'}, # loc
986 Status => $args{'Status'}, # loc
987 TimeWorked => $args{'TimeWorked'}, # loc
988 Type => $args{'Type'}, # loc
989 Created => $args{'Created'}, # loc
990 Told => $args{'Told'}, # loc
991 LastUpdated => $args{'Updated'}, # loc
992 Resolved => $args{'Resolved'}, # loc
993 Due => $args{'Due'}, # loc
996 # If the ticket didn't have an id
997 # Set the ticket's effective ID now that we've created it.
999 $self->Load( $args{'id'} );
1003 $self->__Set( Field => 'EffectiveId', Value => $id );
1007 $self . "->Import couldn't set EffectiveId: $msg" );
1011 my $create_groups_ret = $self->_CreateTicketGroups();
1012 unless ($create_groups_ret) {
1014 "Couldn't create ticket groups for ticket " . $self->Id );
1017 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1019 foreach my $watcher ( @{ $args{'Cc'} } ) {
1020 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1022 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
1023 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1026 foreach my $watcher ( @{ $args{'Requestor'} } ) {
1027 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1031 return ( $self->Id, $ErrStr );
1036 # {{{ Routines dealing with watchers.
1038 # {{{ _CreateTicketGroups
1040 =head2 _CreateTicketGroups
1042 Create the ticket groups and links for this ticket.
1043 This routine expects to be called from Ticket->Create _inside of a transaction_
1045 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1047 It will return true on success and undef on failure.
1053 sub _CreateTicketGroups {
1056 my @types = qw(Requestor Owner Cc AdminCc);
1058 foreach my $type (@types) {
1059 my $type_obj = RT::Group->new($self->CurrentUser);
1060 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1061 Instance => $self->Id,
1064 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1065 $self->Id.": ".$msg);
1075 # {{{ sub OwnerGroup
1079 A constructor which returns an RT::Group object containing the owner of this ticket.
1085 my $owner_obj = RT::Group->new($self->CurrentUser);
1086 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1087 return ($owner_obj);
1093 # {{{ sub AddWatcher
1097 AddWatcher takes a parameter hash. The keys are as follows:
1099 Type One of Requestor, Cc, AdminCc
1101 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1103 Email The email address of the new watcher. If a user with this
1104 email address can't be found, a new nonprivileged user will be created.
1106 If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1114 PrincipalId => undef,
1119 # ModifyTicket works in any case
1120 return $self->_AddWatcher( %args )
1121 if $self->CurrentUserHasRight('ModifyTicket');
1122 if ( $args{'Email'} ) {
1123 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1124 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1127 if ( lc $self->CurrentUser->UserObj->EmailAddress
1128 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1130 $args{'PrincipalId'} = $self->CurrentUser->id;
1131 delete $args{'Email'};
1135 # If the watcher isn't the current user then the current user has no right
1137 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1138 return ( 0, $self->loc("Permission Denied") );
1141 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1142 if ( $args{'Type'} eq 'AdminCc' ) {
1143 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1144 return ( 0, $self->loc('Permission Denied') );
1148 # If it's a Requestor or Cc and they don't have 'Watch', bail
1149 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1150 unless ( $self->CurrentUserHasRight('Watch') ) {
1151 return ( 0, $self->loc('Permission Denied') );
1155 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1156 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1159 return $self->_AddWatcher( %args );
1162 #This contains the meat of AddWatcher. but can be called from a routine like
1163 # Create, which doesn't need the additional acl check
1169 PrincipalId => undef,
1175 my $principal = RT::Principal->new($self->CurrentUser);
1176 if ($args{'Email'}) {
1177 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1178 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
1180 my $user = RT::User->new($RT::SystemUser);
1181 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1182 $args{'PrincipalId'} = $pid if $pid;
1184 if ($args{'PrincipalId'}) {
1185 $principal->Load($args{'PrincipalId'});
1186 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1187 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
1188 if RT::EmailParser->IsRTAddress( $email );
1194 # If we can't find this watcher, we need to bail.
1195 unless ($principal->Id) {
1196 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1197 return(0, $self->loc("Could not find or create that user"));
1201 my $group = RT::Group->new($self->CurrentUser);
1202 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1203 unless ($group->id) {
1204 return(0,$self->loc("Group not found"));
1207 if ( $group->HasMember( $principal)) {
1209 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1213 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1214 InsideTransaction => 1 );
1216 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1218 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1221 unless ( $args{'Silent'} ) {
1222 $self->_NewTransaction(
1223 Type => 'AddWatcher',
1224 NewValue => $principal->Id,
1225 Field => $args{'Type'}
1229 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1235 # {{{ sub DeleteWatcher
1237 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1240 Deletes a Ticket watcher. Takes two arguments:
1242 Type (one of Requestor,Cc,AdminCc)
1246 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1248 Email (the email address of an existing wathcer)
1257 my %args = ( Type => undef,
1258 PrincipalId => undef,
1262 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1263 return ( 0, $self->loc("No principal specified") );
1265 my $principal = RT::Principal->new( $self->CurrentUser );
1266 if ( $args{'PrincipalId'} ) {
1268 $principal->Load( $args{'PrincipalId'} );
1271 my $user = RT::User->new( $self->CurrentUser );
1272 $user->LoadByEmail( $args{'Email'} );
1273 $principal->Load( $user->Id );
1276 # If we can't find this watcher, we need to bail.
1277 unless ( $principal->Id ) {
1278 return ( 0, $self->loc("Could not find that principal") );
1281 my $group = RT::Group->new( $self->CurrentUser );
1282 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1283 unless ( $group->id ) {
1284 return ( 0, $self->loc("Group not found") );
1288 #If the watcher we're trying to add is for the current user
1289 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1291 # If it's an AdminCc and they don't have
1292 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1293 if ( $args{'Type'} eq 'AdminCc' ) {
1294 unless ( $self->CurrentUserHasRight('ModifyTicket')
1295 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1296 return ( 0, $self->loc('Permission Denied') );
1300 # If it's a Requestor or Cc and they don't have
1301 # 'Watch' or 'ModifyTicket', bail
1302 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1304 unless ( $self->CurrentUserHasRight('ModifyTicket')
1305 or $self->CurrentUserHasRight('Watch') ) {
1306 return ( 0, $self->loc('Permission Denied') );
1310 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1312 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1316 # If the watcher isn't the current user
1317 # and the current user doesn't have 'ModifyTicket' bail
1319 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1320 return ( 0, $self->loc("Permission Denied") );
1326 # see if this user is already a watcher.
1328 unless ( $group->HasMember($principal) ) {
1330 $self->loc( 'That principal is not a [_1] for this ticket',
1334 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1336 $RT::Logger->error( "Failed to delete "
1338 . " as a member of group "
1344 'Could not remove that principal as a [_1] for this ticket',
1348 unless ( $args{'Silent'} ) {
1349 $self->_NewTransaction( Type => 'DelWatcher',
1350 OldValue => $principal->Id,
1351 Field => $args{'Type'} );
1355 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1356 $principal->Object->Name,
1365 =head2 SquelchMailTo [EMAIL]
1367 Takes an optional email address to never email about updates to this ticket.
1370 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1378 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1382 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1387 return $self->_SquelchMailTo(@_);
1390 sub _SquelchMailTo {
1394 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1395 unless grep { $_->Content eq $attr }
1396 $self->Attributes->Named('SquelchMailTo');
1398 my @attributes = $self->Attributes->Named('SquelchMailTo');
1399 return (@attributes);
1403 =head2 UnsquelchMailTo ADDRESS
1405 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1407 Returns a tuple of (status, message)
1411 sub UnsquelchMailTo {
1414 my $address = shift;
1415 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1416 return ( 0, $self->loc("Permission Denied") );
1419 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1420 return ($val, $msg);
1424 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1426 =head2 RequestorAddresses
1428 B<Returns> String: All Ticket Requestor email addresses as a string.
1432 sub RequestorAddresses {
1435 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1439 return ( $self->Requestors->MemberEmailAddressesAsString );
1443 =head2 AdminCcAddresses
1445 returns String: All Ticket AdminCc email addresses as a string
1449 sub AdminCcAddresses {
1452 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1456 return ( $self->AdminCc->MemberEmailAddressesAsString )
1462 returns String: All Ticket Ccs as a string of email addresses
1469 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1472 return ( $self->Cc->MemberEmailAddressesAsString);
1478 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1480 # {{{ sub Requestors
1485 Returns this ticket's Requestors as an RT::Group object
1492 my $group = RT::Group->new($self->CurrentUser);
1493 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1494 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1502 # {{{ sub _Requestors
1506 Private non-ACLed variant of Reqeustors so that we can look them up for the
1507 purposes of customer auto-association during create.
1514 my $group = RT::Group->new($RT::SystemUser);
1515 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1526 Returns an RT::Group object which contains this ticket's Ccs.
1527 If the user doesn't have "ShowTicket" permission, returns an empty group
1534 my $group = RT::Group->new($self->CurrentUser);
1535 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1536 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1549 Returns an RT::Group object which contains this ticket's AdminCcs.
1550 If the user doesn't have "ShowTicket" permission, returns an empty group
1557 my $group = RT::Group->new($self->CurrentUser);
1558 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1559 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1569 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1572 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1574 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1576 Takes a param hash with the attributes Type and either PrincipalId or Email
1578 Type is one of Requestor, Cc, AdminCc and Owner
1580 PrincipalId is an RT::Principal id, and Email is an email address.
1582 Returns true if the specified principal (or the one corresponding to the
1583 specified address) is a member of the group Type for this ticket.
1585 XX TODO: This should be Memoized.
1592 my %args = ( Type => 'Requestor',
1593 PrincipalId => undef,
1598 # Load the relevant group.
1599 my $group = RT::Group->new($self->CurrentUser);
1600 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1602 # Find the relevant principal.
1603 if (!$args{PrincipalId} && $args{Email}) {
1604 # Look up the specified user.
1605 my $user = RT::User->new($self->CurrentUser);
1606 $user->LoadByEmail($args{Email});
1608 $args{PrincipalId} = $user->PrincipalId;
1611 # A non-existent user can't be a group member.
1616 # Ask if it has the member in question
1617 return $group->HasMember( $args{'PrincipalId'} );
1622 # {{{ sub IsRequestor
1624 =head2 IsRequestor PRINCIPAL_ID
1626 Takes an L<RT::Principal> id.
1628 Returns true if the principal is a requestor of the current ticket.
1636 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1644 =head2 IsCc PRINCIPAL_ID
1646 Takes an RT::Principal id.
1647 Returns true if the principal is a Cc of the current ticket.
1656 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1664 =head2 IsAdminCc PRINCIPAL_ID
1666 Takes an RT::Principal id.
1667 Returns true if the principal is an AdminCc of the current ticket.
1675 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1685 Takes an RT::User object. Returns true if that user is this ticket's owner.
1686 returns undef otherwise
1694 # no ACL check since this is used in acl decisions
1695 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1699 #Tickets won't yet have owners when they're being created.
1700 unless ( $self->OwnerObj->id ) {
1704 if ( $person->id == $self->OwnerObj->id ) {
1719 =head2 TransactionAddresses
1721 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1722 all this ticket's Create, Comment or Correspond transactions. The keys are
1723 stringified email addresses. Each value is an L<Email::Address> object.
1725 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.
1730 sub TransactionAddresses {
1732 my $txns = $self->Transactions;
1735 foreach my $type (qw(Create Comment Correspond)) {
1736 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1739 while (my $txn = $txns->Next) {
1740 my $txnaddrs = $txn->Addresses;
1741 foreach my $addrlist ( values %$txnaddrs ) {
1742 foreach my $addr (@$addrlist) {
1743 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1744 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1745 # skips "comment-only" addresses
1746 next unless ($addr->address);
1747 $addresses{$addr->address} = $addr;
1759 # {{{ Routines dealing with queues
1761 # {{{ sub ValidateQueue
1768 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1772 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1773 my $id = $QueueObj->Load($Value);
1789 my $NewQueue = shift;
1791 #Redundant. ACL gets checked in _Set;
1792 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1793 return ( 0, $self->loc("Permission Denied") );
1796 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1797 $NewQueueObj->Load($NewQueue);
1799 unless ( $NewQueueObj->Id() ) {
1800 return ( 0, $self->loc("That queue does not exist") );
1803 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1804 return ( 0, $self->loc('That is the same value') );
1807 $self->CurrentUser->HasRight(
1808 Right => 'CreateTicket',
1809 Object => $NewQueueObj
1813 return ( 0, $self->loc("You may not create requests in that queue.") );
1817 $self->OwnerObj->HasRight(
1818 Right => 'OwnTicket',
1819 Object => $NewQueueObj
1823 my $clone = RT::Ticket->new( $RT::SystemUser );
1824 $clone->Load( $self->Id );
1825 unless ( $clone->Id ) {
1826 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1828 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1829 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1832 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1835 # On queue change, change queue for reminders too
1836 my $reminder_collection = $self->Reminders->Collection;
1837 while ( my $reminder = $reminder_collection->Next ) {
1838 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1839 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1843 return ($status, $msg);
1852 Takes nothing. returns this ticket's queue object
1859 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1861 #We call __Value so that we can avoid the ACL decision and some deep recursion
1862 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1863 return ($queue_obj);
1870 return $self->_Set( Field => 'Subject', Value => $value );
1877 # {{{ Date printing routines
1883 Returns an RT::Date object containing this ticket's due date
1890 my $time = new RT::Date( $self->CurrentUser );
1892 # -1 is RT::Date slang for never
1893 if ( my $due = $self->Due ) {
1894 $time->Set( Format => 'sql', Value => $due );
1897 $time->Set( Format => 'unix', Value => -1 );
1905 # {{{ sub DueAsString
1909 Returns this ticket's due date as a human readable string
1915 return $self->DueObj->AsString();
1920 # {{{ sub ResolvedObj
1924 Returns an RT::Date object of this ticket's 'resolved' time.
1931 my $time = new RT::Date( $self->CurrentUser );
1932 $time->Set( Format => 'sql', Value => $self->Resolved );
1938 # {{{ sub SetStarted
1942 Takes a date in ISO format or undef
1943 Returns a transaction id and a message
1944 The client calls "Start" to note that the project was started on the date in $date.
1945 A null date means "now"
1951 my $time = shift || 0;
1953 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1954 return ( 0, $self->loc("Permission Denied") );
1957 #We create a date object to catch date weirdness
1958 my $time_obj = new RT::Date( $self->CurrentUser() );
1960 $time_obj->Set( Format => 'ISO', Value => $time );
1963 $time_obj->SetToNow();
1966 #Now that we're starting, open this ticket
1967 #TODO do we really want to force this as policy? it should be a scrip
1969 #We need $TicketAsSystem, in case the current user doesn't have
1972 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1973 $TicketAsSystem->Load( $self->Id );
1974 if ( $TicketAsSystem->Status eq 'new' ) {
1975 $TicketAsSystem->Open();
1978 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1984 # {{{ sub StartedObj
1988 Returns an RT::Date object which contains this ticket's
1996 my $time = new RT::Date( $self->CurrentUser );
1997 $time->Set( Format => 'sql', Value => $self->Started );
2007 Returns an RT::Date object which contains this ticket's
2015 my $time = new RT::Date( $self->CurrentUser );
2016 $time->Set( Format => 'sql', Value => $self->Starts );
2026 Returns an RT::Date object which contains this ticket's
2034 my $time = new RT::Date( $self->CurrentUser );
2035 $time->Set( Format => 'sql', Value => $self->Told );
2041 # {{{ sub ToldAsString
2045 A convenience method that returns ToldObj->AsString
2047 TODO: This should be deprecated
2053 if ( $self->Told ) {
2054 return $self->ToldObj->AsString();
2063 # {{{ sub TimeWorkedAsString
2065 =head2 TimeWorkedAsString
2067 Returns the amount of time worked on this ticket as a Text String
2071 sub TimeWorkedAsString {
2073 my $value = $self->TimeWorked;
2075 # return the # of minutes worked turned into seconds and written as
2076 # a simple text string, this is not really a date object, but if we
2077 # diff a number of seconds vs the epoch, we'll get a nice description
2079 return "" unless $value;
2080 return RT::Date->new( $self->CurrentUser )
2081 ->DurationAsString( $value * 60 );
2086 # {{{ sub TimeLeftAsString
2088 =head2 TimeLeftAsString
2090 Returns the amount of time left on this ticket as a Text String
2094 sub TimeLeftAsString {
2096 my $value = $self->TimeLeft;
2097 return "" unless $value;
2098 return RT::Date->new( $self->CurrentUser )
2099 ->DurationAsString( $value * 60 );
2104 # {{{ Routines dealing with correspondence/comments
2110 Comment on this ticket.
2111 Takes a hash with the following attributes:
2112 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2115 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2117 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2118 They will, however, be prepared and you'll be able to access them through the TransactionObj
2120 Returns: Transaction id, Error Message, Transaction Object
2121 (note the different order from Create()!)
2128 my %args = ( CcMessageTo => undef,
2129 BccMessageTo => undef,
2136 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2137 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2138 return ( 0, $self->loc("Permission Denied"), undef );
2140 $args{'NoteType'} = 'Comment';
2142 if ($args{'DryRun'}) {
2143 $RT::Handle->BeginTransaction();
2144 $args{'CommitScrips'} = 0;
2147 my @results = $self->_RecordNote(%args);
2148 if ($args{'DryRun'}) {
2149 $RT::Handle->Rollback();
2156 # {{{ sub Correspond
2160 Correspond on this ticket.
2161 Takes a hashref with the following attributes:
2164 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2166 if there's no MIMEObj, Content is used to build a MIME::Entity object
2168 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2169 They will, however, be prepared and you'll be able to access them through the TransactionObj
2171 Returns: Transaction id, Error Message, Transaction Object
2172 (note the different order from Create()!)
2179 my %args = ( CcMessageTo => undef,
2180 BccMessageTo => undef,
2186 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2187 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2188 return ( 0, $self->loc("Permission Denied"), undef );
2191 $args{'NoteType'} = 'Correspond';
2192 if ($args{'DryRun'}) {
2193 $RT::Handle->BeginTransaction();
2194 $args{'CommitScrips'} = 0;
2197 my @results = $self->_RecordNote(%args);
2199 #Set the last told date to now if this isn't mail from the requestor.
2200 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2201 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2203 if ($args{'DryRun'}) {
2204 $RT::Handle->Rollback();
2213 # {{{ sub _RecordNote
2217 the meat of both comment and correspond.
2219 Performs no access control checks. hence, dangerous.
2226 CcMessageTo => undef,
2227 BccMessageTo => undef,
2232 NoteType => 'Correspond',
2239 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2240 return ( 0, $self->loc("No message attached"), undef );
2243 unless ( $args{'MIMEObj'} ) {
2244 $args{'MIMEObj'} = MIME::Entity->build(
2245 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2249 # convert text parts into utf-8
2250 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2252 # If we've been passed in CcMessageTo and BccMessageTo fields,
2253 # add them to the mime object for passing on to the transaction handler
2254 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2255 # RT-Send-Bcc: headers
2258 foreach my $type (qw/Cc Bcc/) {
2259 if ( defined $args{ $type . 'MessageTo' } ) {
2261 my $addresses = join ', ', (
2262 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2263 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2264 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2268 foreach my $argument (qw(Encrypt Sign)) {
2269 $args{'MIMEObj'}->head->add(
2270 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2271 ) if defined $args{ $argument };
2274 # If this is from an external source, we need to come up with its
2275 # internal Message-ID now, so all emails sent because of this
2276 # message have a common Message-ID
2277 my $org = RT->Config->Get('Organization');
2278 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2279 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2280 $args{'MIMEObj'}->head->set(
2281 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2285 #Record the correspondence (write the transaction)
2286 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2287 Type => $args{'NoteType'},
2288 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2289 TimeTaken => $args{'TimeTaken'},
2290 MIMEObj => $args{'MIMEObj'},
2291 CommitScrips => $args{'CommitScrips'},
2292 CustomFields => $args{'CustomFields'},
2296 $RT::Logger->err("$self couldn't init a transaction $msg");
2297 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2300 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2312 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2315 my $type = shift || "";
2317 my $cache_key = "$field$type";
2318 return $self->{ $cache_key } if $self->{ $cache_key };
2320 my $links = $self->{ $cache_key }
2321 = RT::Links->new( $self->CurrentUser );
2322 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2323 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2327 # Maybe this ticket is a merge ticket
2328 #my $limit_on = 'Local'. $field;
2329 # at least to myself
2331 FIELD => $field, #$limit_on,
2332 OPERATOR => 'MATCHES',
2333 VALUE => 'fsck.com-rt://%/ticket/'. $self->id,
2334 ENTRYAGGREGATOR => 'OR',
2337 FIELD => $field, #$limit_on,
2338 OPERATOR => 'MATCHES',
2339 VALUE => 'fsck.com-rt://%/ticket/'. $_,
2340 ENTRYAGGREGATOR => 'OR',
2341 ) foreach $self->Merged;
2352 # {{{ sub DeleteLink
2356 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2357 SilentBase and SilentTarget. Either Base or Target must be null.
2358 The null value will be replaced with this ticket\'s id.
2360 If Silent is true then no transaction would be recorded, in other
2361 case you can control creation of transactions on both base and
2362 target with SilentBase and SilentTarget respectively. By default
2363 both transactions are created.
2374 SilentBase => undef,
2375 SilentTarget => undef,
2379 unless ( $args{'Target'} || $args{'Base'} ) {
2380 $RT::Logger->error("Base or Target must be specified");
2381 return ( 0, $self->loc('Either base or target must be specified') );
2386 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2387 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2388 return ( 0, $self->loc("Permission Denied") );
2391 # If the other URI is an RT::Ticket, we want to make sure the user
2392 # can modify it too...
2393 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2394 return (0, $msg) unless $status;
2395 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2398 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2399 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2401 return ( 0, $self->loc("Permission Denied") );
2404 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2405 return ( 0, $Msg ) unless $val;
2407 return ( $val, $Msg ) if $args{'Silent'};
2409 my ($direction, $remote_link);
2411 if ( $args{'Base'} ) {
2412 $remote_link = $args{'Base'};
2413 $direction = 'Target';
2415 elsif ( $args{'Target'} ) {
2416 $remote_link = $args{'Target'};
2417 $direction = 'Base';
2420 my $remote_uri = RT::URI->new( $self->CurrentUser );
2421 $remote_uri->FromURI( $remote_link );
2423 unless ( $args{ 'Silent'. $direction } ) {
2424 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2425 Type => 'DeleteLink',
2426 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2427 OldValue => $remote_uri->URI || $remote_link,
2430 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2433 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2434 my $OtherObj = $remote_uri->Object;
2435 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2436 Type => 'DeleteLink',
2437 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2438 : $LINKDIRMAP{$args{'Type'}}->{Target},
2439 OldValue => $self->URI,
2440 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2443 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2446 return ( $val, $Msg );
2455 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2457 If Silent is true then no transaction would be recorded, in other
2458 case you can control creation of transactions on both base and
2459 target with SilentBase and SilentTarget respectively. By default
2460 both transactions are created.
2466 my %args = ( Target => '',
2470 SilentBase => undef,
2471 SilentTarget => undef,
2474 unless ( $args{'Target'} || $args{'Base'} ) {
2475 $RT::Logger->error("Base or Target must be specified");
2476 return ( 0, $self->loc('Either base or target must be specified') );
2480 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2481 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2482 return ( 0, $self->loc("Permission Denied") );
2485 # If the other URI is an RT::Ticket, we want to make sure the user
2486 # can modify it too...
2487 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2488 return (0, $msg) unless $status;
2489 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2492 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2493 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2495 return ( 0, $self->loc("Permission Denied") );
2498 return $self->_AddLink(%args);
2501 sub __GetTicketFromURI {
2503 my %args = ( URI => '', @_ );
2505 # If the other URI is an RT::Ticket, we want to make sure the user
2506 # can modify it too...
2507 my $uri_obj = RT::URI->new( $self->CurrentUser );
2508 $uri_obj->FromURI( $args{'URI'} );
2510 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2511 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2512 $RT::Logger->warning( $msg );
2515 my $obj = $uri_obj->Resolver->Object;
2516 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2517 return (1, 'Found not a ticket', undef);
2519 return (1, 'Found ticket', $obj);
2524 Private non-acled variant of AddLink so that links can be added during create.
2530 my %args = ( Target => '',
2534 SilentBase => undef,
2535 SilentTarget => undef,
2538 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2539 return ($val, $msg) if !$val || $exist;
2540 return ($val, $msg) if $args{'Silent'};
2542 my ($direction, $remote_link);
2543 if ( $args{'Target'} ) {
2544 $remote_link = $args{'Target'};
2545 $direction = 'Base';
2546 } elsif ( $args{'Base'} ) {
2547 $remote_link = $args{'Base'};
2548 $direction = 'Target';
2551 my $remote_uri = RT::URI->new( $self->CurrentUser );
2552 $remote_uri->FromURI( $remote_link );
2554 unless ( $args{ 'Silent'. $direction } ) {
2555 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2557 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2558 NewValue => $remote_uri->URI || $remote_link,
2561 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2564 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2565 my $OtherObj = $remote_uri->Object;
2566 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2568 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2569 : $LINKDIRMAP{$args{'Type'}}->{Target},
2570 NewValue => $self->URI,
2571 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2574 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2577 return ( $val, $msg );
2587 MergeInto take the id of the ticket to merge this ticket into.
2593 my $ticket_id = shift;
2595 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2596 return ( 0, $self->loc("Permission Denied") );
2599 # Load up the new ticket.
2600 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2601 $MergeInto->Load($ticket_id);
2603 # make sure it exists.
2604 unless ( $MergeInto->Id ) {
2605 return ( 0, $self->loc("New ticket doesn't exist") );
2608 # Make sure the current user can modify the new ticket.
2609 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2610 return ( 0, $self->loc("Permission Denied") );
2613 delete $MERGE_CACHE{'effective'}{ $self->id };
2614 delete @{ $MERGE_CACHE{'merged'} }{
2615 $ticket_id, $MergeInto->id, $self->id
2618 $RT::Handle->BeginTransaction();
2620 # We use EffectiveId here even though it duplicates information from
2621 # the links table becasue of the massive performance hit we'd take
2622 # by trying to do a separate database query for merge info everytime
2625 #update this ticket's effective id to the new ticket's id.
2626 my ( $id_val, $id_msg ) = $self->__Set(
2627 Field => 'EffectiveId',
2628 Value => $MergeInto->Id()
2632 $RT::Handle->Rollback();
2633 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2637 if ( $self->__Value('Status') ne 'resolved' ) {
2639 my ( $status_val, $status_msg )
2640 = $self->__Set( Field => 'Status', Value => 'resolved' );
2642 unless ($status_val) {
2643 $RT::Handle->Rollback();
2646 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2650 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2654 # update all the links that point to that old ticket
2655 my $old_links_to = RT::Links->new($self->CurrentUser);
2656 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2659 while (my $link = $old_links_to->Next) {
2660 if (exists $old_seen{$link->Base."-".$link->Type}) {
2663 elsif ($link->Base eq $MergeInto->URI) {
2666 # First, make sure the link doesn't already exist. then move it over.
2667 my $tmp = RT::Link->new($RT::SystemUser);
2668 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2672 $link->SetTarget($MergeInto->URI);
2673 $link->SetLocalTarget($MergeInto->id);
2675 $old_seen{$link->Base."-".$link->Type} =1;
2680 my $old_links_from = RT::Links->new($self->CurrentUser);
2681 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2683 while (my $link = $old_links_from->Next) {
2684 if (exists $old_seen{$link->Type."-".$link->Target}) {
2687 if ($link->Target eq $MergeInto->URI) {
2690 # First, make sure the link doesn't already exist. then move it over.
2691 my $tmp = RT::Link->new($RT::SystemUser);
2692 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2696 $link->SetBase($MergeInto->URI);
2697 $link->SetLocalBase($MergeInto->id);
2698 $old_seen{$link->Type."-".$link->Target} =1;
2704 # Update time fields
2705 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2707 my $mutator = "Set$type";
2708 $MergeInto->$mutator(
2709 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2712 #add all of this ticket's watchers to that ticket.
2713 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2715 my $people = $self->$watcher_type->MembersObj;
2716 my $addwatcher_type = $watcher_type;
2717 $addwatcher_type =~ s/s$//;
2719 while ( my $watcher = $people->Next ) {
2721 my ($val, $msg) = $MergeInto->_AddWatcher(
2722 Type => $addwatcher_type,
2724 PrincipalId => $watcher->MemberId
2727 $RT::Logger->warning($msg);
2733 #find all of the tickets that were merged into this ticket.
2734 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2735 $old_mergees->Limit(
2736 FIELD => 'EffectiveId',
2741 # update their EffectiveId fields to the new ticket's id
2742 while ( my $ticket = $old_mergees->Next() ) {
2743 my ( $val, $msg ) = $ticket->__Set(
2744 Field => 'EffectiveId',
2745 Value => $MergeInto->Id()
2749 #make a new link: this ticket is merged into that other ticket.
2750 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2752 $MergeInto->_SetLastUpdated;
2754 $RT::Handle->Commit();
2755 return ( 1, $self->loc("Merge Successful") );
2760 Returns list of tickets' ids that's been merged into this ticket.
2768 return @{ $MERGE_CACHE{'merged'}{ $id } }
2769 if $MERGE_CACHE{'merged'}{ $id };
2771 my $mergees = RT::Tickets->new( $self->CurrentUser );
2773 FIELD => 'EffectiveId',
2781 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2782 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2789 # {{{ Routines dealing with ownership
2795 Takes nothing and returns an RT::User object of
2803 #If this gets ACLed, we lose on a rights check in User.pm and
2804 #get deep recursion. if we need ACLs here, we need
2805 #an equiv without ACLs
2807 my $owner = new RT::User( $self->CurrentUser );
2808 $owner->Load( $self->__Value('Owner') );
2810 #Return the owner object
2816 # {{{ sub OwnerAsString
2818 =head2 OwnerAsString
2820 Returns the owner's email address
2826 return ( $self->OwnerObj->EmailAddress );
2836 Takes two arguments:
2837 the Id or Name of the owner
2838 and (optionally) the type of the SetOwner Transaction. It defaults
2839 to 'Give'. 'Steal' is also a valid option.
2846 my $NewOwner = shift;
2847 my $Type = shift || "Give";
2849 $RT::Handle->BeginTransaction();
2851 $self->_SetLastUpdated(); # lock the ticket
2852 $self->Load( $self->id ); # in case $self changed while waiting for lock
2854 my $OldOwnerObj = $self->OwnerObj;
2856 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2857 $NewOwnerObj->Load( $NewOwner );
2858 unless ( $NewOwnerObj->Id ) {
2859 $RT::Handle->Rollback();
2860 return ( 0, $self->loc("That user does not exist") );
2864 # must have ModifyTicket rights
2865 # or TakeTicket/StealTicket and $NewOwner is self
2866 # see if it's a take
2867 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2868 unless ( $self->CurrentUserHasRight('ModifyTicket')
2869 || $self->CurrentUserHasRight('TakeTicket') ) {
2870 $RT::Handle->Rollback();
2871 return ( 0, $self->loc("Permission Denied") );
2875 # see if it's a steal
2876 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2877 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2879 unless ( $self->CurrentUserHasRight('ModifyTicket')
2880 || $self->CurrentUserHasRight('StealTicket') ) {
2881 $RT::Handle->Rollback();
2882 return ( 0, $self->loc("Permission Denied") );
2886 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2887 $RT::Handle->Rollback();
2888 return ( 0, $self->loc("Permission Denied") );
2892 # If we're not stealing and the ticket has an owner and it's not
2894 if ( $Type ne 'Steal' and $Type ne 'Force'
2895 and $OldOwnerObj->Id != $RT::Nobody->Id
2896 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2898 $RT::Handle->Rollback();
2899 return ( 0, $self->loc("You can only take tickets that are unowned") )
2900 if $NewOwnerObj->id == $self->CurrentUser->id;
2903 $self->loc("You can only reassign tickets that you own or that are unowned" )
2907 #If we've specified a new owner and that user can't modify the ticket
2908 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2909 $RT::Handle->Rollback();
2910 return ( 0, $self->loc("That user may not own tickets in that queue") );
2913 # If the ticket has an owner and it's the new owner, we don't need
2915 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2916 $RT::Handle->Rollback();
2917 return ( 0, $self->loc("That user already owns that ticket") );
2920 # Delete the owner in the owner group, then add a new one
2921 # TODO: is this safe? it's not how we really want the API to work
2922 # for most things, but it's fast.
2923 my ( $del_id, $del_msg );
2924 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2925 ($del_id, $del_msg) = $owner->Delete();
2926 last unless ($del_id);
2930 $RT::Handle->Rollback();
2931 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2934 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2935 PrincipalId => $NewOwnerObj->PrincipalId,
2936 InsideTransaction => 1 );
2938 $RT::Handle->Rollback();
2939 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2942 # We call set twice with slightly different arguments, so
2943 # as to not have an SQL transaction span two RT transactions
2945 my ( $val, $msg ) = $self->_Set(
2947 RecordTransaction => 0,
2948 Value => $NewOwnerObj->Id,
2950 TransactionType => $Type,
2951 CheckACL => 0, # don't check acl
2955 $RT::Handle->Rollback;
2956 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2959 ($val, $msg) = $self->_NewTransaction(
2962 NewValue => $NewOwnerObj->Id,
2963 OldValue => $OldOwnerObj->Id,
2968 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2969 $OldOwnerObj->Name, $NewOwnerObj->Name );
2972 $RT::Handle->Rollback();
2976 $RT::Handle->Commit();
2978 return ( $val, $msg );
2987 A convenince method to set the ticket's owner to the current user
2993 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3002 Convenience method to set the owner to 'nobody' if the current user is the owner.
3008 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3017 A convenience method to change the owner of the current ticket to the
3018 current user. Even if it's owned by another user.
3025 if ( $self->IsOwner( $self->CurrentUser ) ) {
3026 return ( 0, $self->loc("You already own this ticket") );
3029 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3039 # {{{ Routines dealing with status
3041 # {{{ sub ValidateStatus
3043 =head2 ValidateStatus STATUS
3045 Takes a string. Returns true if that status is a valid status for this ticket.
3046 Returns false otherwise.
3050 sub ValidateStatus {
3054 #Make sure the status passed in is valid
3055 unless ( $self->QueueObj->IsValidStatus($status) ) {
3067 =head2 SetStatus STATUS
3069 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3071 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.
3082 $args{Status} = shift;
3089 if ( $args{Status} eq 'deleted') {
3090 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3091 return ( 0, $self->loc('Permission Denied') );
3094 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3095 return ( 0, $self->loc('Permission Denied') );
3099 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3100 return (0, $self->loc('That ticket has unresolved dependencies'));
3103 my $now = RT::Date->new( $self->CurrentUser );
3106 #If we're changing the status from new, record that we've started
3107 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3109 #Set the Started time to "now"
3110 $self->_Set( Field => 'Started',
3112 RecordTransaction => 0 );
3115 #When we close a ticket, set the 'Resolved' attribute to now.
3116 # It's misnamed, but that's just historical.
3117 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3118 $self->_Set( Field => 'Resolved',
3120 RecordTransaction => 0 );
3123 #Actually update the status
3124 my ($val, $msg)= $self->_Set( Field => 'Status',
3125 Value => $args{Status},
3128 TransactionType => 'Status' );
3139 Takes no arguments. Marks this ticket for garbage collection
3145 return ( $self->SetStatus('deleted') );
3147 # TODO: garbage collection
3156 Sets this ticket's status to stalled
3162 return ( $self->SetStatus('stalled') );
3171 Sets this ticket's status to rejected
3177 return ( $self->SetStatus('rejected') );
3186 Sets this ticket\'s status to Open
3192 return ( $self->SetStatus('open') );
3201 Sets this ticket\'s status to Resolved
3207 return ( $self->SetStatus('resolved') );
3215 # {{{ Actions + Routines dealing with transactions
3217 # {{{ sub SetTold and _SetTold
3219 =head2 SetTold ISO [TIMETAKEN]
3221 Updates the told and records a transaction
3228 $told = shift if (@_);
3229 my $timetaken = shift || 0;
3231 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3232 return ( 0, $self->loc("Permission Denied") );
3235 my $datetold = new RT::Date( $self->CurrentUser );
3237 $datetold->Set( Format => 'iso',
3241 $datetold->SetToNow();
3244 return ( $self->_Set( Field => 'Told',
3245 Value => $datetold->ISO,
3246 TimeTaken => $timetaken,
3247 TransactionType => 'Told' ) );
3252 Updates the told without a transaction or acl check. Useful when we're sending replies.
3259 my $now = new RT::Date( $self->CurrentUser );
3262 #use __Set to get no ACLs ;)
3263 return ( $self->__Set( Field => 'Told',
3264 Value => $now->ISO ) );
3274 my $uid = $self->CurrentUser->id;
3275 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3276 return if $attr && $attr->Content gt $self->LastUpdated;
3278 my $txns = $self->Transactions;
3279 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3280 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3281 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3285 VALUE => $attr->Content
3287 $txns->RowsPerPage(1);
3288 return $txns->First;
3293 =head2 TransactionBatch
3295 Returns an array reference of all transactions created on this ticket during
3296 this ticket object's lifetime or since last application of a batch, or undef
3299 Only works when the C<UseTransactionBatch> config option is set to true.
3303 sub TransactionBatch {
3305 return $self->{_TransactionBatch};
3308 =head2 ApplyTransactionBatch
3310 Applies scrips on the current batch of transactions and shinks it. Usually
3311 batch is applied when object is destroyed, but in some cases it's too late.
3315 sub ApplyTransactionBatch {
3318 my $batch = $self->TransactionBatch;
3319 return unless $batch && @$batch;
3321 $self->_ApplyTransactionBatch;
3323 $self->{_TransactionBatch} = [];
3326 sub _ApplyTransactionBatch {
3328 my $batch = $self->TransactionBatch;
3331 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3334 RT::Scrips->new($RT::SystemUser)->Apply(
3335 Stage => 'TransactionBatch',
3337 TransactionObj => $batch->[0],
3341 # Entry point of the rule system
3342 my $rules = RT::Ruleset->FindAllRules(
3343 Stage => 'TransactionBatch',
3345 TransactionObj => $batch->[0],
3348 RT::Ruleset->CommitRules($rules);
3354 # DESTROY methods need to localize $@, or it may unset it. This
3355 # causes $m->abort to not bubble all of the way up. See perlbug
3356 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3359 # The following line eliminates reentrancy.
3360 # It protects against the fact that perl doesn't deal gracefully
3361 # when an object's refcount is changed in its destructor.
3362 return if $self->{_Destroyed}++;
3364 my $batch = $self->TransactionBatch;
3365 return unless $batch && @$batch;
3367 return $self->_ApplyTransactionBatch;
3372 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3374 # {{{ sub _OverlayAccessible
3376 sub _OverlayAccessible {
3378 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3379 Queue => { 'read' => 1, 'write' => 1 },
3380 Requestors => { 'read' => 1, 'write' => 1 },
3381 Owner => { 'read' => 1, 'write' => 1 },
3382 Subject => { 'read' => 1, 'write' => 1 },
3383 InitialPriority => { 'read' => 1, 'write' => 1 },
3384 FinalPriority => { 'read' => 1, 'write' => 1 },
3385 Priority => { 'read' => 1, 'write' => 1 },
3386 Status => { 'read' => 1, 'write' => 1 },
3387 TimeEstimated => { 'read' => 1, 'write' => 1 },
3388 TimeWorked => { 'read' => 1, 'write' => 1 },
3389 TimeLeft => { 'read' => 1, 'write' => 1 },
3390 Told => { 'read' => 1, 'write' => 1 },
3391 Resolved => { 'read' => 1 },
3392 Type => { 'read' => 1 },
3393 Starts => { 'read' => 1, 'write' => 1 },
3394 Started => { 'read' => 1, 'write' => 1 },
3395 Due => { 'read' => 1, 'write' => 1 },
3396 Creator => { 'read' => 1, 'auto' => 1 },
3397 Created => { 'read' => 1, 'auto' => 1 },
3398 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3399 LastUpdated => { 'read' => 1, 'auto' => 1 }
3411 my %args = ( Field => undef,
3414 RecordTransaction => 1,
3417 TransactionType => 'Set',
3420 if ($args{'CheckACL'}) {
3421 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3422 return ( 0, $self->loc("Permission Denied"));
3426 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3427 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3428 return(0, $self->loc("Internal Error"));
3431 #if the user is trying to modify the record
3433 #Take care of the old value we really don't want to get in an ACL loop.
3434 # so ask the super::_Value
3435 my $Old = $self->SUPER::_Value("$args{'Field'}");
3438 if ( $args{'UpdateTicket'} ) {
3441 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3442 Value => $args{'Value'} );
3444 #If we can't actually set the field to the value, don't record
3445 # a transaction. instead, get out of here.
3446 return ( 0, $msg ) unless $ret;
3449 if ( $args{'RecordTransaction'} == 1 ) {
3451 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3452 Type => $args{'TransactionType'},
3453 Field => $args{'Field'},
3454 NewValue => $args{'Value'},
3456 TimeTaken => $args{'TimeTaken'},
3458 return ( $Trans, scalar $TransObj->BriefDescription );
3461 return ( $ret, $msg );
3471 Takes the name of a table column.
3472 Returns its value as a string, if the user passes an ACL check
3481 #if the field is public, return it.
3482 if ( $self->_Accessible( $field, 'public' ) ) {
3484 #$RT::Logger->debug("Skipping ACL check for $field");
3485 return ( $self->SUPER::_Value($field) );
3489 #If the current user doesn't have ACLs, don't let em at it.
3491 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3494 return ( $self->SUPER::_Value($field) );
3500 # {{{ sub _UpdateTimeTaken
3502 =head2 _UpdateTimeTaken
3504 This routine will increment the timeworked counter. it should
3505 only be called from _NewTransaction
3509 sub _UpdateTimeTaken {
3511 my $Minutes = shift;
3514 $Total = $self->SUPER::_Value("TimeWorked");
3515 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3517 Field => "TimeWorked",
3528 # {{{ Routines dealing with ACCESS CONTROL
3530 # {{{ sub CurrentUserHasRight
3532 =head2 CurrentUserHasRight
3534 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3535 1 if the user has that right. It returns 0 if the user doesn't have that right.
3539 sub CurrentUserHasRight {
3543 return $self->CurrentUser->PrincipalObj->HasRight(
3551 =head2 CurrentUserCanSee
3553 Returns true if the current user can see the ticket, using ShowTicket
3557 sub CurrentUserCanSee {
3559 return $self->CurrentUserHasRight('ShowTicket');
3566 Takes a paramhash with the attributes 'Right' and 'Principal'
3567 'Right' is a ticket-scoped textual right from RT::ACE
3568 'Principal' is an RT::User object
3570 Returns 1 if the principal has the right. Returns undef if not.
3582 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3584 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3585 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3590 $args{'Principal'}->HasRight(
3592 Right => $args{'Right'}
3603 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3604 It isn't acutally a searchbuilder collection itself.
3611 unless ($self->{'__reminders'}) {
3612 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3613 $self->{'__reminders'}->Ticket($self->id);
3615 return $self->{'__reminders'};
3621 # {{{ sub Transactions
3625 Returns an RT::Transactions object of all transactions on this ticket
3632 my $transactions = RT::Transactions->new( $self->CurrentUser );
3634 #If the user has no rights, return an empty object
3635 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3636 $transactions->LimitToTicket($self->id);
3638 # if the user may not see comments do not return them
3639 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3640 $transactions->Limit(
3646 $transactions->Limit(
3650 VALUE => "CommentEmailRecord",
3651 ENTRYAGGREGATOR => 'AND'
3656 $transactions->Limit(
3660 ENTRYAGGREGATOR => 'AND'
3664 return ($transactions);
3670 # {{{ TransactionCustomFields
3672 =head2 TransactionCustomFields
3674 Returns the custom fields that transactions on tickets will have.
3678 sub TransactionCustomFields {
3680 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3681 $cfs->SetContextObject( $self );
3687 # {{{ sub CustomFieldValues
3689 =head2 CustomFieldValues
3691 # Do name => id mapping (if needed) before falling back to
3692 # RT::Record's CustomFieldValues
3698 sub CustomFieldValues {
3702 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3704 my $cf = RT::CustomField->new( $self->CurrentUser );
3705 $cf->SetContextObject( $self );
3706 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3707 unless ( $cf->id ) {
3708 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3711 # If we didn't find a valid cfid, give up.
3712 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3714 return $self->SUPER::CustomFieldValues( $cf->id );
3719 # {{{ sub CustomFieldLookupType
3721 =head2 CustomFieldLookupType
3723 Returns the RT::Ticket lookup type, which can be passed to
3724 RT::CustomField->Create() via the 'LookupType' hash key.
3730 sub CustomFieldLookupType {
3731 "RT::Queue-RT::Ticket";
3734 =head2 ACLEquivalenceObjects
3736 This method returns a list of objects for which a user's rights also apply
3737 to this ticket. Generally, this is only the ticket's queue, but some RT
3738 extensions may make other objects available too.
3740 This method is called from L<RT::Principal/HasRight>.
3744 sub ACLEquivalenceObjects {
3746 return $self->QueueObj;
3755 Jesse Vincent, jesse@bestpractical.com