1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 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 $RT::Handle->BeginTransaction();
457 Queue => $QueueObj->Id,
459 Subject => $args{'Subject'},
460 InitialPriority => $args{'InitialPriority'},
461 FinalPriority => $args{'FinalPriority'},
462 Priority => $args{'Priority'},
463 Status => $args{'Status'},
464 TimeWorked => $args{'TimeWorked'},
465 TimeEstimated => $args{'TimeEstimated'},
466 TimeLeft => $args{'TimeLeft'},
467 Type => $args{'Type'},
468 Starts => $Starts->ISO,
469 Started => $Started->ISO,
470 Resolved => $Resolved->ISO,
474 # Parameters passed in during an import that we probably don't want to touch, otherwise
475 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
476 $params{$attr} = $args{$attr} if $args{$attr};
479 # Delete null integer parameters
481 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
483 delete $params{$attr}
484 unless ( exists $params{$attr} && $params{$attr} );
487 # Delete the time worked if we're counting it in the transaction
488 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
490 my ($id,$ticket_message) = $self->SUPER::Create( %params );
492 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
493 $RT::Handle->Rollback();
495 $self->loc("Ticket could not be created due to an internal error")
499 #Set the ticket's effective ID now that we've created it.
500 my ( $val, $msg ) = $self->__Set(
501 Field => 'EffectiveId',
502 Value => ( $args{'EffectiveId'} || $id )
505 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
506 $RT::Handle->Rollback;
508 $self->loc("Ticket could not be created due to an internal error")
512 my $create_groups_ret = $self->_CreateTicketGroups();
513 unless ($create_groups_ret) {
514 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
516 . ". aborting Ticket creation." );
517 $RT::Handle->Rollback();
519 $self->loc("Ticket could not be created due to an internal error")
523 # Set the owner in the Groups table
524 # We denormalize it into the Ticket table too because doing otherwise would
525 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
526 $self->OwnerGroup->_AddMember(
527 PrincipalId => $Owner->PrincipalId,
528 InsideTransaction => 1
529 ) unless $DeferOwner;
533 # {{{ Deal with setting up watchers
535 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
536 # we know it's an array ref
537 foreach my $watcher ( @{ $args{$type} } ) {
539 # Note that we're using AddWatcher, rather than _AddWatcher, as we
540 # actually _want_ that ACL check. Otherwise, random ticket creators
541 # could make themselves adminccs and maybe get ticket rights. that would
543 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
545 my ($val, $msg) = $self->$method(
547 PrincipalId => $watcher,
550 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
555 if ($args{'SquelchMailTo'}) {
556 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
557 : $args{'SquelchMailTo'};
558 $self->_SquelchMailTo( @squelch );
564 # {{{ Add all the custom fields
566 foreach my $arg ( keys %args ) {
567 next unless $arg =~ /^CustomField-(\d+)$/i;
571 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
573 next unless defined $value && length $value;
575 # Allow passing in uploaded LargeContent etc by hash reference
576 my ($status, $msg) = $self->_AddCustomFieldValue(
577 (UNIVERSAL::isa( $value => 'HASH' )
582 RecordTransaction => 0,
584 push @non_fatal_errors, $msg unless $status;
590 # {{{ Deal with setting up links
592 # TODO: Adding link may fire scrips on other end and those scrips
593 # could create transactions on this ticket before 'Create' transaction.
595 # We should implement different schema: record 'Create' transaction,
596 # create links and only then fire create transaction's scrips.
598 # Ideal variant: add all links without firing scrips, record create
599 # transaction and only then fire scrips on the other ends of links.
603 foreach my $type ( keys %LINKTYPEMAP ) {
604 next unless ( defined $args{$type} );
606 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
608 # Check rights on the other end of the link if we must
609 # then run _AddLink that doesn't check for ACLs
610 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
611 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
613 push @non_fatal_errors, $msg;
616 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
617 push @non_fatal_errors, $self->loc('Linking. Permission denied');
622 #don't show transactions for reminders
623 my $silent = ( !$args{'_RecordTransaction'}
624 || $self->Type eq 'reminder'
627 my ( $wval, $wmsg ) = $self->_AddLink(
628 Type => $LINKTYPEMAP{$type}->{'Type'},
629 $LINKTYPEMAP{$type}->{'Mode'} => $link,
631 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
635 push @non_fatal_errors, $wmsg unless ($wval);
641 # {{{ Deal with auto-customer association
643 #unless we already have (a) customer(s)...
644 unless ( $self->Customers->Count ) {
646 #first find any requestors with emails but *without* customer targets
647 my @NoCust_Requestors =
648 grep { $_->EmailAddress && ! $_->Customers->Count }
649 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
651 for my $Requestor (@NoCust_Requestors) {
653 #perhaps the stuff in here should be in a User method??
655 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
657 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
659 ## false laziness w/RT/Interface/Web_Vendor.pm
660 my @link = ( 'Type' => 'MemberOf',
661 'Target' => "freeside://freeside/cust_main/$custnum",
664 my( $val, $msg ) = $Requestor->_AddLink(@link);
665 #XXX should do something with $msg# push @non_fatal_errors, $msg;
671 #find any requestors with customer targets
673 my %cust_target = ();
676 grep { $_->Customers->Count }
677 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
679 foreach my $Requestor ( @Requestors ) {
680 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
681 $cust_target{ $cust_link->Target } = 1;
685 #and then auto-associate this ticket with those customers
687 foreach my $cust_target ( keys %cust_target ) {
689 my @link = ( 'Type' => 'MemberOf',
690 #'Target' => "freeside://freeside/cust_main/$custnum",
691 'Target' => $cust_target,
694 my( $val, $msg ) = $self->_AddLink(@link);
695 push @non_fatal_errors, $msg;
703 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
704 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
706 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
708 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
709 . ") was proposed as a ticket owner but has no rights to own "
710 . "tickets in " . $QueueObj->Name );
711 push @non_fatal_errors, $self->loc(
712 "Owner '[_1]' does not have rights to own this ticket.",
716 $Owner = $DeferOwner;
717 $self->__Set(Field => 'Owner', Value => $Owner->id);
719 $self->OwnerGroup->_AddMember(
720 PrincipalId => $Owner->PrincipalId,
721 InsideTransaction => 1
725 #don't make a transaction or fire off any scrips for reminders either
726 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
728 # {{{ Add a transaction for the create
729 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
731 TimeTaken => $args{'TimeWorked'},
732 MIMEObj => $args{'MIMEObj'},
733 CommitScrips => !$args{'DryRun'},
736 if ( $self->Id && $Trans ) {
738 #$TransObj->UpdateCustomFields(ARGSRef => \%args);
739 $TransObj->UpdateCustomFields(%args);
741 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
742 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
743 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
746 $RT::Handle->Rollback();
748 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
749 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
750 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
753 if ( $args{'DryRun'} ) {
754 $RT::Handle->Rollback();
755 return ($self->id, $TransObj, $ErrStr);
757 $RT::Handle->Commit();
758 return ( $self->Id, $TransObj->Id, $ErrStr );
764 # Not going to record a transaction
765 $RT::Handle->Commit();
766 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
767 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
768 return ( $self->Id, 0, $ErrStr );
776 # {{{ _Parse822HeadersForAttributes Content
778 =head2 _Parse822HeadersForAttributes Content
780 Takes an RFC822 style message and parses its attributes into a hash.
784 sub _Parse822HeadersForAttributes {
789 my @lines = ( split ( /\n/, $content ) );
790 while ( defined( my $line = shift @lines ) ) {
791 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
796 if ( defined( $args{$tag} ) )
797 { #if we're about to get a second value, make it an array
798 $args{$tag} = [ $args{$tag} ];
800 if ( ref( $args{$tag} ) )
801 { #If it's an array, we want to push the value
802 push @{ $args{$tag} }, $value;
804 else { #if there's nothing there, just set the value
805 $args{$tag} = $value;
807 } elsif ($line =~ /^$/) {
809 #TODO: this won't work, since "" isn't of the form "foo:value"
811 while ( defined( my $l = shift @lines ) ) {
812 push @{ $args{'content'} }, $l;
818 foreach my $date (qw(due starts started resolved)) {
819 my $dateobj = RT::Date->new($RT::SystemUser);
820 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
821 $dateobj->Set( Format => 'unix', Value => $args{$date} );
824 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
826 $args{$date} = $dateobj->ISO;
828 $args{'mimeobj'} = MIME::Entity->new();
829 $args{'mimeobj'}->build(
830 Type => ( $args{'contenttype'} || 'text/plain' ),
831 Data => ($args{'content'} || '')
841 =head2 Import PARAMHASH
844 Doesn\'t create a transaction.
845 Doesn\'t supply queue defaults, etc.
853 my ( $ErrStr, $QueueObj, $Owner );
857 EffectiveId => undef,
861 Owner => $RT::Nobody->Id,
862 Subject => '[no subject]',
863 InitialPriority => undef,
864 FinalPriority => undef,
875 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
876 $QueueObj = RT::Queue->new($RT::SystemUser);
877 $QueueObj->Load( $args{'Queue'} );
879 #TODO error check this and return 0 if it\'s not loading properly +++
881 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
882 $QueueObj = RT::Queue->new($RT::SystemUser);
883 $QueueObj->Load( $args{'Queue'}->Id );
887 "$self " . $args{'Queue'} . " not a recognised queue object." );
890 #Can't create a ticket without a queue.
891 unless ( defined($QueueObj) and $QueueObj->Id ) {
892 $RT::Logger->debug("$self No queue given for ticket creation.");
893 return ( 0, $self->loc('Could not create ticket. Queue not set') );
896 #Now that we have a queue, Check the ACLS
898 $self->CurrentUser->HasRight(
899 Right => 'CreateTicket',
905 $self->loc("No permission to create tickets in the queue '[_1]'"
909 # {{{ Deal with setting the owner
911 # Attempt to take user object, user name or user id.
912 # Assign to nobody if lookup fails.
913 if ( defined( $args{'Owner'} ) ) {
914 if ( ref( $args{'Owner'} ) ) {
915 $Owner = $args{'Owner'};
918 $Owner = new RT::User( $self->CurrentUser );
919 $Owner->Load( $args{'Owner'} );
920 if ( !defined( $Owner->id ) ) {
921 $Owner->Load( $RT::Nobody->id );
926 #If we have a proposed owner and they don't have the right
927 #to own a ticket, scream about it and make them not the owner
930 and ( $Owner->Id != $RT::Nobody->Id )
940 $RT::Logger->warning( "$self user "
944 . "as a ticket owner but has no rights to own "
946 . $QueueObj->Name . "'" );
951 #If we haven't been handed a valid owner, make it nobody.
952 unless ( defined($Owner) ) {
953 $Owner = new RT::User( $self->CurrentUser );
954 $Owner->Load( $RT::Nobody->UserObj->Id );
959 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
960 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
963 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
964 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
965 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
966 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
968 # If we're coming in with an id, set that now.
969 my $EffectiveId = undef;
971 $EffectiveId = $args{'id'};
975 my $id = $self->SUPER::Create(
977 EffectiveId => $EffectiveId,
978 Queue => $QueueObj->Id,
980 Subject => $args{'Subject'}, # loc
981 InitialPriority => $args{'InitialPriority'}, # loc
982 FinalPriority => $args{'FinalPriority'}, # loc
983 Priority => $args{'InitialPriority'}, # loc
984 Status => $args{'Status'}, # loc
985 TimeWorked => $args{'TimeWorked'}, # loc
986 Type => $args{'Type'}, # loc
987 Created => $args{'Created'}, # loc
988 Told => $args{'Told'}, # loc
989 LastUpdated => $args{'Updated'}, # loc
990 Resolved => $args{'Resolved'}, # loc
991 Due => $args{'Due'}, # loc
994 # If the ticket didn't have an id
995 # Set the ticket's effective ID now that we've created it.
997 $self->Load( $args{'id'} );
1001 $self->__Set( Field => 'EffectiveId', Value => $id );
1005 $self . "->Import couldn't set EffectiveId: $msg" );
1009 my $create_groups_ret = $self->_CreateTicketGroups();
1010 unless ($create_groups_ret) {
1012 "Couldn't create ticket groups for ticket " . $self->Id );
1015 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1017 foreach my $watcher ( @{ $args{'Cc'} } ) {
1018 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1020 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
1021 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1024 foreach my $watcher ( @{ $args{'Requestor'} } ) {
1025 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1029 return ( $self->Id, $ErrStr );
1034 # {{{ Routines dealing with watchers.
1036 # {{{ _CreateTicketGroups
1038 =head2 _CreateTicketGroups
1040 Create the ticket groups and links for this ticket.
1041 This routine expects to be called from Ticket->Create _inside of a transaction_
1043 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1045 It will return true on success and undef on failure.
1051 sub _CreateTicketGroups {
1054 my @types = qw(Requestor Owner Cc AdminCc);
1056 foreach my $type (@types) {
1057 my $type_obj = RT::Group->new($self->CurrentUser);
1058 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1059 Instance => $self->Id,
1062 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1063 $self->Id.": ".$msg);
1073 # {{{ sub OwnerGroup
1077 A constructor which returns an RT::Group object containing the owner of this ticket.
1083 my $owner_obj = RT::Group->new($self->CurrentUser);
1084 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1085 return ($owner_obj);
1091 # {{{ sub AddWatcher
1095 AddWatcher takes a parameter hash. The keys are as follows:
1097 Type One of Requestor, Cc, AdminCc
1099 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1101 Email The email address of the new watcher. If a user with this
1102 email address can't be found, a new nonprivileged user will be created.
1104 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.
1112 PrincipalId => undef,
1117 # ModifyTicket works in any case
1118 return $self->_AddWatcher( %args )
1119 if $self->CurrentUserHasRight('ModifyTicket');
1120 if ( $args{'Email'} ) {
1121 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1122 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1125 if ( lc $self->CurrentUser->UserObj->EmailAddress
1126 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1128 $args{'PrincipalId'} = $self->CurrentUser->id;
1129 delete $args{'Email'};
1133 # If the watcher isn't the current user then the current user has no right
1135 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1136 return ( 0, $self->loc("Permission Denied") );
1139 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1140 if ( $args{'Type'} eq 'AdminCc' ) {
1141 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1142 return ( 0, $self->loc('Permission Denied') );
1146 # If it's a Requestor or Cc and they don't have 'Watch', bail
1147 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1148 unless ( $self->CurrentUserHasRight('Watch') ) {
1149 return ( 0, $self->loc('Permission Denied') );
1153 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1154 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1157 return $self->_AddWatcher( %args );
1160 #This contains the meat of AddWatcher. but can be called from a routine like
1161 # Create, which doesn't need the additional acl check
1167 PrincipalId => undef,
1173 my $principal = RT::Principal->new($self->CurrentUser);
1174 if ($args{'Email'}) {
1175 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1176 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'})));
1178 my $user = RT::User->new($RT::SystemUser);
1179 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1180 $args{'PrincipalId'} = $pid if $pid;
1182 if ($args{'PrincipalId'}) {
1183 $principal->Load($args{'PrincipalId'});
1184 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1185 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'})))
1186 if RT::EmailParser->IsRTAddress( $email );
1192 # If we can't find this watcher, we need to bail.
1193 unless ($principal->Id) {
1194 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1195 return(0, $self->loc("Could not find or create that user"));
1199 my $group = RT::Group->new($self->CurrentUser);
1200 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1201 unless ($group->id) {
1202 return(0,$self->loc("Group not found"));
1205 if ( $group->HasMember( $principal)) {
1207 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1211 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1212 InsideTransaction => 1 );
1214 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1216 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1219 unless ( $args{'Silent'} ) {
1220 $self->_NewTransaction(
1221 Type => 'AddWatcher',
1222 NewValue => $principal->Id,
1223 Field => $args{'Type'}
1227 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1233 # {{{ sub DeleteWatcher
1235 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1238 Deletes a Ticket watcher. Takes two arguments:
1240 Type (one of Requestor,Cc,AdminCc)
1244 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1246 Email (the email address of an existing wathcer)
1255 my %args = ( Type => undef,
1256 PrincipalId => undef,
1260 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1261 return ( 0, $self->loc("No principal specified") );
1263 my $principal = RT::Principal->new( $self->CurrentUser );
1264 if ( $args{'PrincipalId'} ) {
1266 $principal->Load( $args{'PrincipalId'} );
1269 my $user = RT::User->new( $self->CurrentUser );
1270 $user->LoadByEmail( $args{'Email'} );
1271 $principal->Load( $user->Id );
1274 # If we can't find this watcher, we need to bail.
1275 unless ( $principal->Id ) {
1276 return ( 0, $self->loc("Could not find that principal") );
1279 my $group = RT::Group->new( $self->CurrentUser );
1280 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1281 unless ( $group->id ) {
1282 return ( 0, $self->loc("Group not found") );
1286 #If the watcher we're trying to add is for the current user
1287 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1289 # If it's an AdminCc and they don't have
1290 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1291 if ( $args{'Type'} eq 'AdminCc' ) {
1292 unless ( $self->CurrentUserHasRight('ModifyTicket')
1293 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1294 return ( 0, $self->loc('Permission Denied') );
1298 # If it's a Requestor or Cc and they don't have
1299 # 'Watch' or 'ModifyTicket', bail
1300 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1302 unless ( $self->CurrentUserHasRight('ModifyTicket')
1303 or $self->CurrentUserHasRight('Watch') ) {
1304 return ( 0, $self->loc('Permission Denied') );
1308 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1310 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1314 # If the watcher isn't the current user
1315 # and the current user doesn't have 'ModifyTicket' bail
1317 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1318 return ( 0, $self->loc("Permission Denied") );
1324 # see if this user is already a watcher.
1326 unless ( $group->HasMember($principal) ) {
1328 $self->loc( 'That principal is not a [_1] for this ticket',
1332 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1334 $RT::Logger->error( "Failed to delete "
1336 . " as a member of group "
1342 'Could not remove that principal as a [_1] for this ticket',
1346 unless ( $args{'Silent'} ) {
1347 $self->_NewTransaction( Type => 'DelWatcher',
1348 OldValue => $principal->Id,
1349 Field => $args{'Type'} );
1353 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1354 $principal->Object->Name,
1363 =head2 SquelchMailTo [EMAIL]
1365 Takes an optional email address to never email about updates to this ticket.
1368 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1376 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1380 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1385 return $self->_SquelchMailTo(@_);
1388 sub _SquelchMailTo {
1392 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1393 unless grep { $_->Content eq $attr }
1394 $self->Attributes->Named('SquelchMailTo');
1396 my @attributes = $self->Attributes->Named('SquelchMailTo');
1397 return (@attributes);
1401 =head2 UnsquelchMailTo ADDRESS
1403 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1405 Returns a tuple of (status, message)
1409 sub UnsquelchMailTo {
1412 my $address = shift;
1413 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1414 return ( 0, $self->loc("Permission Denied") );
1417 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1418 return ($val, $msg);
1422 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1424 =head2 RequestorAddresses
1426 B<Returns> String: All Ticket Requestor email addresses as a string.
1430 sub RequestorAddresses {
1433 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1437 return ( $self->Requestors->MemberEmailAddressesAsString );
1441 =head2 AdminCcAddresses
1443 returns String: All Ticket AdminCc email addresses as a string
1447 sub AdminCcAddresses {
1450 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1454 return ( $self->AdminCc->MemberEmailAddressesAsString )
1460 returns String: All Ticket Ccs as a string of email addresses
1467 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1470 return ( $self->Cc->MemberEmailAddressesAsString);
1476 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1478 # {{{ sub Requestors
1483 Returns this ticket's Requestors as an RT::Group object
1490 my $group = RT::Group->new($self->CurrentUser);
1491 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1492 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1500 # {{{ sub _Requestors
1504 Private non-ACLed variant of Reqeustors so that we can look them up for the
1505 purposes of customer auto-association during create.
1512 my $group = RT::Group->new($RT::SystemUser);
1513 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1524 Returns an RT::Group object which contains this ticket's Ccs.
1525 If the user doesn't have "ShowTicket" permission, returns an empty group
1532 my $group = RT::Group->new($self->CurrentUser);
1533 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1534 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1547 Returns an RT::Group object which contains this ticket's AdminCcs.
1548 If the user doesn't have "ShowTicket" permission, returns an empty group
1555 my $group = RT::Group->new($self->CurrentUser);
1556 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1557 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1567 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1570 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1572 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1574 Takes a param hash with the attributes Type and either PrincipalId or Email
1576 Type is one of Requestor, Cc, AdminCc and Owner
1578 PrincipalId is an RT::Principal id, and Email is an email address.
1580 Returns true if the specified principal (or the one corresponding to the
1581 specified address) is a member of the group Type for this ticket.
1583 XX TODO: This should be Memoized.
1590 my %args = ( Type => 'Requestor',
1591 PrincipalId => undef,
1596 # Load the relevant group.
1597 my $group = RT::Group->new($self->CurrentUser);
1598 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1600 # Find the relevant principal.
1601 if (!$args{PrincipalId} && $args{Email}) {
1602 # Look up the specified user.
1603 my $user = RT::User->new($self->CurrentUser);
1604 $user->LoadByEmail($args{Email});
1606 $args{PrincipalId} = $user->PrincipalId;
1609 # A non-existent user can't be a group member.
1614 # Ask if it has the member in question
1615 return $group->HasMember( $args{'PrincipalId'} );
1620 # {{{ sub IsRequestor
1622 =head2 IsRequestor PRINCIPAL_ID
1624 Takes an L<RT::Principal> id.
1626 Returns true if the principal is a requestor of the current ticket.
1634 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1642 =head2 IsCc PRINCIPAL_ID
1644 Takes an RT::Principal id.
1645 Returns true if the principal is a Cc of the current ticket.
1654 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1662 =head2 IsAdminCc PRINCIPAL_ID
1664 Takes an RT::Principal id.
1665 Returns true if the principal is an AdminCc of the current ticket.
1673 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1683 Takes an RT::User object. Returns true if that user is this ticket's owner.
1684 returns undef otherwise
1692 # no ACL check since this is used in acl decisions
1693 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1697 #Tickets won't yet have owners when they're being created.
1698 unless ( $self->OwnerObj->id ) {
1702 if ( $person->id == $self->OwnerObj->id ) {
1717 =head2 TransactionAddresses
1719 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1720 all this ticket's Create, Comment or Correspond transactions. The keys are
1721 stringified email addresses. Each value is an L<Email::Address> object.
1723 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.
1728 sub TransactionAddresses {
1730 my $txns = $self->Transactions;
1733 foreach my $type (qw(Create Comment Correspond)) {
1734 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1737 while (my $txn = $txns->Next) {
1738 my $txnaddrs = $txn->Addresses;
1739 foreach my $addrlist ( values %$txnaddrs ) {
1740 foreach my $addr (@$addrlist) {
1741 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1742 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1743 # skips "comment-only" addresses
1744 next unless ($addr->address);
1745 $addresses{$addr->address} = $addr;
1757 # {{{ Routines dealing with queues
1759 # {{{ sub ValidateQueue
1766 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1770 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1771 my $id = $QueueObj->Load($Value);
1787 my $NewQueue = shift;
1789 #Redundant. ACL gets checked in _Set;
1790 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1791 return ( 0, $self->loc("Permission Denied") );
1794 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1795 $NewQueueObj->Load($NewQueue);
1797 unless ( $NewQueueObj->Id() ) {
1798 return ( 0, $self->loc("That queue does not exist") );
1801 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1802 return ( 0, $self->loc('That is the same value') );
1805 $self->CurrentUser->HasRight(
1806 Right => 'CreateTicket',
1807 Object => $NewQueueObj
1811 return ( 0, $self->loc("You may not create requests in that queue.") );
1815 $self->OwnerObj->HasRight(
1816 Right => 'OwnTicket',
1817 Object => $NewQueueObj
1821 my $clone = RT::Ticket->new( $RT::SystemUser );
1822 $clone->Load( $self->Id );
1823 unless ( $clone->Id ) {
1824 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1826 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1827 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1830 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1833 # On queue change, change queue for reminders too
1834 my $reminder_collection = $self->Reminders->Collection;
1835 while ( my $reminder = $reminder_collection->Next ) {
1836 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1837 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1841 return ($status, $msg);
1850 Takes nothing. returns this ticket's queue object
1857 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1859 #We call __Value so that we can avoid the ACL decision and some deep recursion
1860 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1861 return ($queue_obj);
1868 # {{{ Date printing routines
1874 Returns an RT::Date object containing this ticket's due date
1881 my $time = new RT::Date( $self->CurrentUser );
1883 # -1 is RT::Date slang for never
1884 if ( my $due = $self->Due ) {
1885 $time->Set( Format => 'sql', Value => $due );
1888 $time->Set( Format => 'unix', Value => -1 );
1896 # {{{ sub DueAsString
1900 Returns this ticket's due date as a human readable string
1906 return $self->DueObj->AsString();
1911 # {{{ sub ResolvedObj
1915 Returns an RT::Date object of this ticket's 'resolved' time.
1922 my $time = new RT::Date( $self->CurrentUser );
1923 $time->Set( Format => 'sql', Value => $self->Resolved );
1929 # {{{ sub SetStarted
1933 Takes a date in ISO format or undef
1934 Returns a transaction id and a message
1935 The client calls "Start" to note that the project was started on the date in $date.
1936 A null date means "now"
1942 my $time = shift || 0;
1944 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1945 return ( 0, $self->loc("Permission Denied") );
1948 #We create a date object to catch date weirdness
1949 my $time_obj = new RT::Date( $self->CurrentUser() );
1951 $time_obj->Set( Format => 'ISO', Value => $time );
1954 $time_obj->SetToNow();
1957 #Now that we're starting, open this ticket
1958 #TODO do we really want to force this as policy? it should be a scrip
1960 #We need $TicketAsSystem, in case the current user doesn't have
1963 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1964 $TicketAsSystem->Load( $self->Id );
1965 if ( $TicketAsSystem->Status eq 'new' ) {
1966 $TicketAsSystem->Open();
1969 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1975 # {{{ sub StartedObj
1979 Returns an RT::Date object which contains this ticket's
1987 my $time = new RT::Date( $self->CurrentUser );
1988 $time->Set( Format => 'sql', Value => $self->Started );
1998 Returns an RT::Date object which contains this ticket's
2006 my $time = new RT::Date( $self->CurrentUser );
2007 $time->Set( Format => 'sql', Value => $self->Starts );
2017 Returns an RT::Date object which contains this ticket's
2025 my $time = new RT::Date( $self->CurrentUser );
2026 $time->Set( Format => 'sql', Value => $self->Told );
2032 # {{{ sub ToldAsString
2036 A convenience method that returns ToldObj->AsString
2038 TODO: This should be deprecated
2044 if ( $self->Told ) {
2045 return $self->ToldObj->AsString();
2054 # {{{ sub TimeWorkedAsString
2056 =head2 TimeWorkedAsString
2058 Returns the amount of time worked on this ticket as a Text String
2062 sub TimeWorkedAsString {
2064 my $value = $self->TimeWorked;
2066 # return the # of minutes worked turned into seconds and written as
2067 # a simple text string, this is not really a date object, but if we
2068 # diff a number of seconds vs the epoch, we'll get a nice description
2070 return "" unless $value;
2071 return RT::Date->new( $self->CurrentUser )
2072 ->DurationAsString( $value * 60 );
2077 # {{{ sub TimeLeftAsString
2079 =head2 TimeLeftAsString
2081 Returns the amount of time left on this ticket as a Text String
2085 sub TimeLeftAsString {
2087 my $value = $self->TimeLeft;
2088 return "" unless $value;
2089 return RT::Date->new( $self->CurrentUser )
2090 ->DurationAsString( $value * 60 );
2095 # {{{ Routines dealing with correspondence/comments
2101 Comment on this ticket.
2102 Takes a hash with the following attributes:
2103 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2106 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2108 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2109 They will, however, be prepared and you'll be able to access them through the TransactionObj
2111 Returns: Transaction id, Error Message, Transaction Object
2112 (note the different order from Create()!)
2119 my %args = ( CcMessageTo => undef,
2120 BccMessageTo => undef,
2127 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2128 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2129 return ( 0, $self->loc("Permission Denied"), undef );
2131 $args{'NoteType'} = 'Comment';
2133 if ($args{'DryRun'}) {
2134 $RT::Handle->BeginTransaction();
2135 $args{'CommitScrips'} = 0;
2138 my @results = $self->_RecordNote(%args);
2139 if ($args{'DryRun'}) {
2140 $RT::Handle->Rollback();
2147 # {{{ sub Correspond
2151 Correspond on this ticket.
2152 Takes a hashref with the following attributes:
2155 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2157 if there's no MIMEObj, Content is used to build a MIME::Entity object
2159 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2160 They will, however, be prepared and you'll be able to access them through the TransactionObj
2162 Returns: Transaction id, Error Message, Transaction Object
2163 (note the different order from Create()!)
2170 my %args = ( CcMessageTo => undef,
2171 BccMessageTo => undef,
2177 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2178 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2179 return ( 0, $self->loc("Permission Denied"), undef );
2182 $args{'NoteType'} = 'Correspond';
2183 if ($args{'DryRun'}) {
2184 $RT::Handle->BeginTransaction();
2185 $args{'CommitScrips'} = 0;
2188 my @results = $self->_RecordNote(%args);
2190 #Set the last told date to now if this isn't mail from the requestor.
2191 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2192 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2194 if ($args{'DryRun'}) {
2195 $RT::Handle->Rollback();
2204 # {{{ sub _RecordNote
2208 the meat of both comment and correspond.
2210 Performs no access control checks. hence, dangerous.
2217 CcMessageTo => undef,
2218 BccMessageTo => undef,
2223 NoteType => 'Correspond',
2230 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2231 return ( 0, $self->loc("No message attached"), undef );
2234 unless ( $args{'MIMEObj'} ) {
2235 $args{'MIMEObj'} = MIME::Entity->build(
2236 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2240 # convert text parts into utf-8
2241 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2243 # If we've been passed in CcMessageTo and BccMessageTo fields,
2244 # add them to the mime object for passing on to the transaction handler
2245 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2246 # RT-Send-Bcc: headers
2249 foreach my $type (qw/Cc Bcc/) {
2250 if ( defined $args{ $type . 'MessageTo' } ) {
2252 my $addresses = join ', ', (
2253 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2254 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2255 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2259 foreach my $argument (qw(Encrypt Sign)) {
2260 $args{'MIMEObj'}->head->add(
2261 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2262 ) if defined $args{ $argument };
2265 # If this is from an external source, we need to come up with its
2266 # internal Message-ID now, so all emails sent because of this
2267 # message have a common Message-ID
2268 my $org = RT->Config->Get('Organization');
2269 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2270 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2271 $args{'MIMEObj'}->head->set(
2272 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2276 #Record the correspondence (write the transaction)
2277 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2278 Type => $args{'NoteType'},
2279 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2280 TimeTaken => $args{'TimeTaken'},
2281 MIMEObj => $args{'MIMEObj'},
2282 CommitScrips => $args{'CommitScrips'},
2283 CustomFields => $args{'CustomFields'},
2287 $RT::Logger->err("$self couldn't init a transaction $msg");
2288 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2291 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2303 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2306 my $type = shift || "";
2308 my $cache_key = "$field$type";
2309 return $self->{ $cache_key } if $self->{ $cache_key };
2311 my $links = $self->{ $cache_key }
2312 = RT::Links->new( $self->CurrentUser );
2313 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2314 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2318 # Maybe this ticket is a merge ticket
2319 #my $limit_on = 'Local'. $field;
2320 # at least to myself
2322 FIELD => $field, #$limit_on,
2324 VALUE => 'fsck.com-rt://%/ticket/'. $self->id,
2325 ENTRYAGGREGATOR => 'OR',
2328 FIELD => $field, #$limit_on,
2330 VALUE => 'fsck.com-rt://%/ticket/'. $_,
2331 ENTRYAGGREGATOR => 'OR',
2332 ) foreach $self->Merged;
2343 # {{{ sub DeleteLink
2347 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2348 SilentBase and SilentTarget. Either Base or Target must be null.
2349 The null value will be replaced with this ticket\'s id.
2351 If Silent is true then no transaction would be recorded, in other
2352 case you can control creation of transactions on both base and
2353 target with SilentBase and SilentTarget respectively. By default
2354 both transactions are created.
2365 SilentBase => undef,
2366 SilentTarget => undef,
2370 unless ( $args{'Target'} || $args{'Base'} ) {
2371 $RT::Logger->error("Base or Target must be specified");
2372 return ( 0, $self->loc('Either base or target must be specified') );
2377 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2378 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2379 return ( 0, $self->loc("Permission Denied") );
2382 # If the other URI is an RT::Ticket, we want to make sure the user
2383 # can modify it too...
2384 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2385 return (0, $msg) unless $status;
2386 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2389 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2390 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2392 return ( 0, $self->loc("Permission Denied") );
2395 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2396 return ( 0, $Msg ) unless $val;
2398 return ( $val, $Msg ) if $args{'Silent'};
2400 my ($direction, $remote_link);
2402 if ( $args{'Base'} ) {
2403 $remote_link = $args{'Base'};
2404 $direction = 'Target';
2406 elsif ( $args{'Target'} ) {
2407 $remote_link = $args{'Target'};
2408 $direction = 'Base';
2411 my $remote_uri = RT::URI->new( $self->CurrentUser );
2412 $remote_uri->FromURI( $remote_link );
2414 unless ( $args{ 'Silent'. $direction } ) {
2415 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2416 Type => 'DeleteLink',
2417 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2418 OldValue => $remote_uri->URI || $remote_link,
2421 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2424 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2425 my $OtherObj = $remote_uri->Object;
2426 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2427 Type => 'DeleteLink',
2428 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2429 : $LINKDIRMAP{$args{'Type'}}->{Target},
2430 OldValue => $self->URI,
2431 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2434 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2437 return ( $val, $Msg );
2446 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2448 If Silent is true then no transaction would be recorded, in other
2449 case you can control creation of transactions on both base and
2450 target with SilentBase and SilentTarget respectively. By default
2451 both transactions are created.
2457 my %args = ( Target => '',
2461 SilentBase => undef,
2462 SilentTarget => undef,
2465 unless ( $args{'Target'} || $args{'Base'} ) {
2466 $RT::Logger->error("Base or Target must be specified");
2467 return ( 0, $self->loc('Either base or target must be specified') );
2471 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2472 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2473 return ( 0, $self->loc("Permission Denied") );
2476 # If the other URI is an RT::Ticket, we want to make sure the user
2477 # can modify it too...
2478 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2479 return (0, $msg) unless $status;
2480 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2483 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2484 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2486 return ( 0, $self->loc("Permission Denied") );
2489 return $self->_AddLink(%args);
2492 sub __GetTicketFromURI {
2494 my %args = ( URI => '', @_ );
2496 # If the other URI is an RT::Ticket, we want to make sure the user
2497 # can modify it too...
2498 my $uri_obj = RT::URI->new( $self->CurrentUser );
2499 $uri_obj->FromURI( $args{'URI'} );
2501 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2502 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2503 $RT::Logger->warning( $msg );
2506 my $obj = $uri_obj->Resolver->Object;
2507 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2508 return (1, 'Found not a ticket', undef);
2510 return (1, 'Found ticket', $obj);
2515 Private non-acled variant of AddLink so that links can be added during create.
2521 my %args = ( Target => '',
2525 SilentBase => undef,
2526 SilentTarget => undef,
2529 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2530 return ($val, $msg) if !$val || $exist;
2531 return ($val, $msg) if $args{'Silent'};
2533 my ($direction, $remote_link);
2534 if ( $args{'Target'} ) {
2535 $remote_link = $args{'Target'};
2536 $direction = 'Base';
2537 } elsif ( $args{'Base'} ) {
2538 $remote_link = $args{'Base'};
2539 $direction = 'Target';
2542 my $remote_uri = RT::URI->new( $self->CurrentUser );
2543 $remote_uri->FromURI( $remote_link );
2545 unless ( $args{ 'Silent'. $direction } ) {
2546 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2548 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2549 NewValue => $remote_uri->URI || $remote_link,
2552 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2555 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2556 my $OtherObj = $remote_uri->Object;
2557 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2559 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2560 : $LINKDIRMAP{$args{'Type'}}->{Target},
2561 NewValue => $self->URI,
2562 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2565 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2568 return ( $val, $msg );
2578 MergeInto take the id of the ticket to merge this ticket into.
2584 my $ticket_id = shift;
2586 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2587 return ( 0, $self->loc("Permission Denied") );
2590 # Load up the new ticket.
2591 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2592 $MergeInto->Load($ticket_id);
2594 # make sure it exists.
2595 unless ( $MergeInto->Id ) {
2596 return ( 0, $self->loc("New ticket doesn't exist") );
2599 # Make sure the current user can modify the new ticket.
2600 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2601 return ( 0, $self->loc("Permission Denied") );
2604 delete $MERGE_CACHE{'effective'}{ $self->id };
2605 delete @{ $MERGE_CACHE{'merged'} }{
2606 $ticket_id, $MergeInto->id, $self->id
2609 $RT::Handle->BeginTransaction();
2611 # We use EffectiveId here even though it duplicates information from
2612 # the links table becasue of the massive performance hit we'd take
2613 # by trying to do a separate database query for merge info everytime
2616 #update this ticket's effective id to the new ticket's id.
2617 my ( $id_val, $id_msg ) = $self->__Set(
2618 Field => 'EffectiveId',
2619 Value => $MergeInto->Id()
2623 $RT::Handle->Rollback();
2624 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2628 if ( $self->__Value('Status') ne 'resolved' ) {
2630 my ( $status_val, $status_msg )
2631 = $self->__Set( Field => 'Status', Value => 'resolved' );
2633 unless ($status_val) {
2634 $RT::Handle->Rollback();
2637 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2641 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2645 # update all the links that point to that old ticket
2646 my $old_links_to = RT::Links->new($self->CurrentUser);
2647 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2650 while (my $link = $old_links_to->Next) {
2651 if (exists $old_seen{$link->Base."-".$link->Type}) {
2654 elsif ($link->Base eq $MergeInto->URI) {
2657 # First, make sure the link doesn't already exist. then move it over.
2658 my $tmp = RT::Link->new($RT::SystemUser);
2659 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2663 $link->SetTarget($MergeInto->URI);
2664 $link->SetLocalTarget($MergeInto->id);
2666 $old_seen{$link->Base."-".$link->Type} =1;
2671 my $old_links_from = RT::Links->new($self->CurrentUser);
2672 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2674 while (my $link = $old_links_from->Next) {
2675 if (exists $old_seen{$link->Type."-".$link->Target}) {
2678 if ($link->Target eq $MergeInto->URI) {
2681 # First, make sure the link doesn't already exist. then move it over.
2682 my $tmp = RT::Link->new($RT::SystemUser);
2683 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2687 $link->SetBase($MergeInto->URI);
2688 $link->SetLocalBase($MergeInto->id);
2689 $old_seen{$link->Type."-".$link->Target} =1;
2695 # Update time fields
2696 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2698 my $mutator = "Set$type";
2699 $MergeInto->$mutator(
2700 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2703 #add all of this ticket's watchers to that ticket.
2704 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2706 my $people = $self->$watcher_type->MembersObj;
2707 my $addwatcher_type = $watcher_type;
2708 $addwatcher_type =~ s/s$//;
2710 while ( my $watcher = $people->Next ) {
2712 my ($val, $msg) = $MergeInto->_AddWatcher(
2713 Type => $addwatcher_type,
2715 PrincipalId => $watcher->MemberId
2718 $RT::Logger->warning($msg);
2724 #find all of the tickets that were merged into this ticket.
2725 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2726 $old_mergees->Limit(
2727 FIELD => 'EffectiveId',
2732 # update their EffectiveId fields to the new ticket's id
2733 while ( my $ticket = $old_mergees->Next() ) {
2734 my ( $val, $msg ) = $ticket->__Set(
2735 Field => 'EffectiveId',
2736 Value => $MergeInto->Id()
2740 #make a new link: this ticket is merged into that other ticket.
2741 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2743 $MergeInto->_SetLastUpdated;
2745 $RT::Handle->Commit();
2746 return ( 1, $self->loc("Merge Successful") );
2751 Returns list of tickets' ids that's been merged into this ticket.
2759 return @{ $MERGE_CACHE{'merged'}{ $id } }
2760 if $MERGE_CACHE{'merged'}{ $id };
2762 my $mergees = RT::Tickets->new( $self->CurrentUser );
2764 FIELD => 'EffectiveId',
2772 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2773 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2780 # {{{ Routines dealing with ownership
2786 Takes nothing and returns an RT::User object of
2794 #If this gets ACLed, we lose on a rights check in User.pm and
2795 #get deep recursion. if we need ACLs here, we need
2796 #an equiv without ACLs
2798 my $owner = new RT::User( $self->CurrentUser );
2799 $owner->Load( $self->__Value('Owner') );
2801 #Return the owner object
2807 # {{{ sub OwnerAsString
2809 =head2 OwnerAsString
2811 Returns the owner's email address
2817 return ( $self->OwnerObj->EmailAddress );
2827 Takes two arguments:
2828 the Id or Name of the owner
2829 and (optionally) the type of the SetOwner Transaction. It defaults
2830 to 'Give'. 'Steal' is also a valid option.
2837 my $NewOwner = shift;
2838 my $Type = shift || "Give";
2840 $RT::Handle->BeginTransaction();
2842 $self->_SetLastUpdated(); # lock the ticket
2843 $self->Load( $self->id ); # in case $self changed while waiting for lock
2845 my $OldOwnerObj = $self->OwnerObj;
2847 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2848 $NewOwnerObj->Load( $NewOwner );
2849 unless ( $NewOwnerObj->Id ) {
2850 $RT::Handle->Rollback();
2851 return ( 0, $self->loc("That user does not exist") );
2855 # must have ModifyTicket rights
2856 # or TakeTicket/StealTicket and $NewOwner is self
2857 # see if it's a take
2858 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2859 unless ( $self->CurrentUserHasRight('ModifyTicket')
2860 || $self->CurrentUserHasRight('TakeTicket') ) {
2861 $RT::Handle->Rollback();
2862 return ( 0, $self->loc("Permission Denied") );
2866 # see if it's a steal
2867 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2868 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2870 unless ( $self->CurrentUserHasRight('ModifyTicket')
2871 || $self->CurrentUserHasRight('StealTicket') ) {
2872 $RT::Handle->Rollback();
2873 return ( 0, $self->loc("Permission Denied") );
2877 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2878 $RT::Handle->Rollback();
2879 return ( 0, $self->loc("Permission Denied") );
2883 # If we're not stealing and the ticket has an owner and it's not
2885 if ( $Type ne 'Steal' and $Type ne 'Force'
2886 and $OldOwnerObj->Id != $RT::Nobody->Id
2887 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2889 $RT::Handle->Rollback();
2890 return ( 0, $self->loc("You can only take tickets that are unowned") )
2891 if $NewOwnerObj->id == $self->CurrentUser->id;
2894 $self->loc("You can only reassign tickets that you own or that are unowned" )
2898 #If we've specified a new owner and that user can't modify the ticket
2899 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2900 $RT::Handle->Rollback();
2901 return ( 0, $self->loc("That user may not own tickets in that queue") );
2904 # If the ticket has an owner and it's the new owner, we don't need
2906 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2907 $RT::Handle->Rollback();
2908 return ( 0, $self->loc("That user already owns that ticket") );
2911 # Delete the owner in the owner group, then add a new one
2912 # TODO: is this safe? it's not how we really want the API to work
2913 # for most things, but it's fast.
2914 my ( $del_id, $del_msg );
2915 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2916 ($del_id, $del_msg) = $owner->Delete();
2917 last unless ($del_id);
2921 $RT::Handle->Rollback();
2922 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2925 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2926 PrincipalId => $NewOwnerObj->PrincipalId,
2927 InsideTransaction => 1 );
2929 $RT::Handle->Rollback();
2930 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2933 # We call set twice with slightly different arguments, so
2934 # as to not have an SQL transaction span two RT transactions
2936 my ( $val, $msg ) = $self->_Set(
2938 RecordTransaction => 0,
2939 Value => $NewOwnerObj->Id,
2941 TransactionType => $Type,
2942 CheckACL => 0, # don't check acl
2946 $RT::Handle->Rollback;
2947 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2950 ($val, $msg) = $self->_NewTransaction(
2953 NewValue => $NewOwnerObj->Id,
2954 OldValue => $OldOwnerObj->Id,
2959 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2960 $OldOwnerObj->Name, $NewOwnerObj->Name );
2963 $RT::Handle->Rollback();
2967 $RT::Handle->Commit();
2969 return ( $val, $msg );
2978 A convenince method to set the ticket's owner to the current user
2984 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2993 Convenience method to set the owner to 'nobody' if the current user is the owner.
2999 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3008 A convenience method to change the owner of the current ticket to the
3009 current user. Even if it's owned by another user.
3016 if ( $self->IsOwner( $self->CurrentUser ) ) {
3017 return ( 0, $self->loc("You already own this ticket") );
3020 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3030 # {{{ Routines dealing with status
3032 # {{{ sub ValidateStatus
3034 =head2 ValidateStatus STATUS
3036 Takes a string. Returns true if that status is a valid status for this ticket.
3037 Returns false otherwise.
3041 sub ValidateStatus {
3045 #Make sure the status passed in is valid
3046 unless ( $self->QueueObj->IsValidStatus($status) ) {
3058 =head2 SetStatus STATUS
3060 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3062 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.
3073 $args{Status} = shift;
3080 if ( $args{Status} eq 'deleted') {
3081 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3082 return ( 0, $self->loc('Permission Denied') );
3085 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3086 return ( 0, $self->loc('Permission Denied') );
3090 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3091 return (0, $self->loc('That ticket has unresolved dependencies'));
3094 my $now = RT::Date->new( $self->CurrentUser );
3097 #If we're changing the status from new, record that we've started
3098 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3100 #Set the Started time to "now"
3101 $self->_Set( Field => 'Started',
3103 RecordTransaction => 0 );
3106 #When we close a ticket, set the 'Resolved' attribute to now.
3107 # It's misnamed, but that's just historical.
3108 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3109 $self->_Set( Field => 'Resolved',
3111 RecordTransaction => 0 );
3114 #Actually update the status
3115 my ($val, $msg)= $self->_Set( Field => 'Status',
3116 Value => $args{Status},
3119 TransactionType => 'Status' );
3130 Takes no arguments. Marks this ticket for garbage collection
3136 return ( $self->SetStatus('deleted') );
3138 # TODO: garbage collection
3147 Sets this ticket's status to stalled
3153 return ( $self->SetStatus('stalled') );
3162 Sets this ticket's status to rejected
3168 return ( $self->SetStatus('rejected') );
3177 Sets this ticket\'s status to Open
3183 return ( $self->SetStatus('open') );
3192 Sets this ticket\'s status to Resolved
3198 return ( $self->SetStatus('resolved') );
3206 # {{{ Actions + Routines dealing with transactions
3208 # {{{ sub SetTold and _SetTold
3210 =head2 SetTold ISO [TIMETAKEN]
3212 Updates the told and records a transaction
3219 $told = shift if (@_);
3220 my $timetaken = shift || 0;
3222 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3223 return ( 0, $self->loc("Permission Denied") );
3226 my $datetold = new RT::Date( $self->CurrentUser );
3228 $datetold->Set( Format => 'iso',
3232 $datetold->SetToNow();
3235 return ( $self->_Set( Field => 'Told',
3236 Value => $datetold->ISO,
3237 TimeTaken => $timetaken,
3238 TransactionType => 'Told' ) );
3243 Updates the told without a transaction or acl check. Useful when we're sending replies.
3250 my $now = new RT::Date( $self->CurrentUser );
3253 #use __Set to get no ACLs ;)
3254 return ( $self->__Set( Field => 'Told',
3255 Value => $now->ISO ) );
3265 my $uid = $self->CurrentUser->id;
3266 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3267 return if $attr && $attr->Content gt $self->LastUpdated;
3269 my $txns = $self->Transactions;
3270 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3271 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3272 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3276 VALUE => $attr->Content
3278 $txns->RowsPerPage(1);
3279 return $txns->First;
3284 =head2 TransactionBatch
3286 Returns an array reference of all transactions created on this ticket during
3287 this ticket object's lifetime or since last application of a batch, or undef
3290 Only works when the C<UseTransactionBatch> config option is set to true.
3294 sub TransactionBatch {
3296 return $self->{_TransactionBatch};
3299 =head2 ApplyTransactionBatch
3301 Applies scrips on the current batch of transactions and shinks it. Usually
3302 batch is applied when object is destroyed, but in some cases it's too late.
3306 sub ApplyTransactionBatch {
3309 my $batch = $self->TransactionBatch;
3310 return unless $batch && @$batch;
3312 $self->_ApplyTransactionBatch;
3314 $self->{_TransactionBatch} = [];
3317 sub _ApplyTransactionBatch {
3319 my $batch = $self->TransactionBatch;
3322 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3325 RT::Scrips->new($RT::SystemUser)->Apply(
3326 Stage => 'TransactionBatch',
3328 TransactionObj => $batch->[0],
3332 # Entry point of the rule system
3333 my $rules = RT::Ruleset->FindAllRules(
3334 Stage => 'TransactionBatch',
3336 TransactionObj => $batch->[0],
3339 RT::Ruleset->CommitRules($rules);
3345 # DESTROY methods need to localize $@, or it may unset it. This
3346 # causes $m->abort to not bubble all of the way up. See perlbug
3347 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3350 # The following line eliminates reentrancy.
3351 # It protects against the fact that perl doesn't deal gracefully
3352 # when an object's refcount is changed in its destructor.
3353 return if $self->{_Destroyed}++;
3355 my $batch = $self->TransactionBatch;
3356 return unless $batch && @$batch;
3358 return $self->_ApplyTransactionBatch;
3363 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3365 # {{{ sub _OverlayAccessible
3367 sub _OverlayAccessible {
3369 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3370 Queue => { 'read' => 1, 'write' => 1 },
3371 Requestors => { 'read' => 1, 'write' => 1 },
3372 Owner => { 'read' => 1, 'write' => 1 },
3373 Subject => { 'read' => 1, 'write' => 1 },
3374 InitialPriority => { 'read' => 1, 'write' => 1 },
3375 FinalPriority => { 'read' => 1, 'write' => 1 },
3376 Priority => { 'read' => 1, 'write' => 1 },
3377 Status => { 'read' => 1, 'write' => 1 },
3378 TimeEstimated => { 'read' => 1, 'write' => 1 },
3379 TimeWorked => { 'read' => 1, 'write' => 1 },
3380 TimeLeft => { 'read' => 1, 'write' => 1 },
3381 Told => { 'read' => 1, 'write' => 1 },
3382 Resolved => { 'read' => 1 },
3383 Type => { 'read' => 1 },
3384 Starts => { 'read' => 1, 'write' => 1 },
3385 Started => { 'read' => 1, 'write' => 1 },
3386 Due => { 'read' => 1, 'write' => 1 },
3387 Creator => { 'read' => 1, 'auto' => 1 },
3388 Created => { 'read' => 1, 'auto' => 1 },
3389 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3390 LastUpdated => { 'read' => 1, 'auto' => 1 }
3402 my %args = ( Field => undef,
3405 RecordTransaction => 1,
3408 TransactionType => 'Set',
3411 if ($args{'CheckACL'}) {
3412 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3413 return ( 0, $self->loc("Permission Denied"));
3417 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3418 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3419 return(0, $self->loc("Internal Error"));
3422 #if the user is trying to modify the record
3424 #Take care of the old value we really don't want to get in an ACL loop.
3425 # so ask the super::_Value
3426 my $Old = $self->SUPER::_Value("$args{'Field'}");
3429 if ( $args{'UpdateTicket'} ) {
3432 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3433 Value => $args{'Value'} );
3435 #If we can't actually set the field to the value, don't record
3436 # a transaction. instead, get out of here.
3437 return ( 0, $msg ) unless $ret;
3440 if ( $args{'RecordTransaction'} == 1 ) {
3442 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3443 Type => $args{'TransactionType'},
3444 Field => $args{'Field'},
3445 NewValue => $args{'Value'},
3447 TimeTaken => $args{'TimeTaken'},
3449 return ( $Trans, scalar $TransObj->BriefDescription );
3452 return ( $ret, $msg );
3462 Takes the name of a table column.
3463 Returns its value as a string, if the user passes an ACL check
3472 #if the field is public, return it.
3473 if ( $self->_Accessible( $field, 'public' ) ) {
3475 #$RT::Logger->debug("Skipping ACL check for $field");
3476 return ( $self->SUPER::_Value($field) );
3480 #If the current user doesn't have ACLs, don't let em at it.
3482 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3485 return ( $self->SUPER::_Value($field) );
3491 # {{{ sub _UpdateTimeTaken
3493 =head2 _UpdateTimeTaken
3495 This routine will increment the timeworked counter. it should
3496 only be called from _NewTransaction
3500 sub _UpdateTimeTaken {
3502 my $Minutes = shift;
3505 $Total = $self->SUPER::_Value("TimeWorked");
3506 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3508 Field => "TimeWorked",
3519 # {{{ Routines dealing with ACCESS CONTROL
3521 # {{{ sub CurrentUserHasRight
3523 =head2 CurrentUserHasRight
3525 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3526 1 if the user has that right. It returns 0 if the user doesn't have that right.
3530 sub CurrentUserHasRight {
3534 return $self->CurrentUser->PrincipalObj->HasRight(
3542 =head2 CurrentUserCanSee
3544 Returns true if the current user can see the ticket, using ShowTicket
3548 sub CurrentUserCanSee {
3550 return $self->CurrentUserHasRight('ShowTicket');
3557 Takes a paramhash with the attributes 'Right' and 'Principal'
3558 'Right' is a ticket-scoped textual right from RT::ACE
3559 'Principal' is an RT::User object
3561 Returns 1 if the principal has the right. Returns undef if not.
3573 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3575 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3576 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3581 $args{'Principal'}->HasRight(
3583 Right => $args{'Right'}
3594 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3595 It isn't acutally a searchbuilder collection itself.
3602 unless ($self->{'__reminders'}) {
3603 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3604 $self->{'__reminders'}->Ticket($self->id);
3606 return $self->{'__reminders'};
3612 # {{{ sub Transactions
3616 Returns an RT::Transactions object of all transactions on this ticket
3623 my $transactions = RT::Transactions->new( $self->CurrentUser );
3625 #If the user has no rights, return an empty object
3626 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3627 $transactions->LimitToTicket($self->id);
3629 # if the user may not see comments do not return them
3630 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3631 $transactions->Limit(
3637 $transactions->Limit(
3641 VALUE => "CommentEmailRecord",
3642 ENTRYAGGREGATOR => 'AND'
3647 $transactions->Limit(
3651 ENTRYAGGREGATOR => 'AND'
3655 return ($transactions);
3661 # {{{ TransactionCustomFields
3663 =head2 TransactionCustomFields
3665 Returns the custom fields that transactions on tickets will have.
3669 sub TransactionCustomFields {
3671 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3672 $cfs->SetContextObject( $self );
3678 # {{{ sub CustomFieldValues
3680 =head2 CustomFieldValues
3682 # Do name => id mapping (if needed) before falling back to
3683 # RT::Record's CustomFieldValues
3689 sub CustomFieldValues {
3693 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3695 my $cf = RT::CustomField->new( $self->CurrentUser );
3696 $cf->SetContextObject( $self );
3697 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3698 unless ( $cf->id ) {
3699 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3702 # If we didn't find a valid cfid, give up.
3703 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3705 return $self->SUPER::CustomFieldValues( $cf->id );
3710 # {{{ sub CustomFieldLookupType
3712 =head2 CustomFieldLookupType
3714 Returns the RT::Ticket lookup type, which can be passed to
3715 RT::CustomField->Create() via the 'LookupType' hash key.
3721 sub CustomFieldLookupType {
3722 "RT::Queue-RT::Ticket";
3725 =head2 ACLEquivalenceObjects
3727 This method returns a list of objects for which a user's rights also apply
3728 to this ticket. Generally, this is only the ticket's queue, but some RT
3729 extensions may make other objects available too.
3731 This method is called from L<RT::Principal/HasRight>.
3735 sub ACLEquivalenceObjects {
3737 return $self->QueueObj;
3746 Jesse Vincent, jesse@bestpractical.com