1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
54 my $ticket = new RT::Ticket($CurrentUser);
55 $ticket->Load($ticket_id);
59 This module lets you manipulate RT\'s ticket object.
71 no warnings qw(redefine);
82 use RT::URI::fsck_com_rt;
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
92 MemberOf => { Type => 'MemberOf',
94 Parents => { Type => 'MemberOf',
96 Members => { Type => 'MemberOf',
98 Children => { Type => 'MemberOf',
100 HasMember => { Type => 'MemberOf',
102 RefersTo => { Type => 'RefersTo',
104 ReferredToBy => { Type => 'RefersTo',
106 DependsOn => { Type => 'DependsOn',
108 DependedOnBy => { Type => 'DependsOn',
110 MergedInto => { Type => 'MergedInto',
118 # A helper table for links mapping to make it easier
119 # to build and parse links between tickets
122 MemberOf => { Base => 'MemberOf',
123 Target => 'HasMember', },
124 RefersTo => { Base => 'RefersTo',
125 Target => 'ReferredToBy', },
126 DependsOn => { Base => 'DependsOn',
127 Target => 'DependedOnBy', },
128 MergedInto => { Base => 'MergedInto',
129 Target => 'MergedInto', },
135 sub LINKTYPEMAP { return \%LINKTYPEMAP }
136 sub LINKDIRMAP { return \%LINKDIRMAP }
147 Takes a single argument. This can be a ticket id, ticket alias or
148 local ticket uri. If the ticket can't be loaded, returns undef.
149 Otherwise, returns the ticket id.
156 $id = '' unless defined $id;
158 # TODO: modify this routine to look at EffectiveId and
159 # do the recursive load thing. be careful to cache all
160 # the interim tickets we try so we don't loop forever.
162 # FIXME: there is no TicketBaseURI option in config
163 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
164 #If it's a local URI, turn it into a ticket id
165 if ( $base_uri && $id =~ /^$base_uri(\d+)$/ ) {
169 unless ( $id =~ /^\d+$/ ) {
170 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
174 $id = $MERGE_CACHE{'effective'}{ $id }
175 if $MERGE_CACHE{'effective'}{ $id };
177 my ($ticketid, $msg) = $self->LoadById( $id );
178 unless ( $self->Id ) {
179 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
183 #If we're merged, resolve the merge.
184 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
186 "We found a merged ticket. "
187 . $self->id ."/". $self->EffectiveId
189 my $real_id = $self->Load( $self->EffectiveId );
190 $MERGE_CACHE{'effective'}{ $id } = $real_id;
194 #Ok. we're loaded. lets get outa here.
204 Arguments: ARGS is a hash of named parameters. Valid parameters are:
207 Queue - Either a Queue object or a Queue Name
208 Requestor - A reference to a list of email addresses or RT user Names
209 Cc - A reference to a list of email addresses or Names
210 AdminCc - A reference to a list of email addresses or Names
211 SquelchMailTo - A reference to a list of email addresses -
212 who should this ticket not mail
213 Type -- The ticket\'s type. ignore this for now
214 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
215 Subject -- A string describing the subject of the ticket
216 Priority -- an integer from 0 to 99
217 InitialPriority -- an integer from 0 to 99
218 FinalPriority -- an integer from 0 to 99
219 Status -- any valid status (Defined in RT::Queue)
220 TimeEstimated -- an integer. estimated time for this task in minutes
221 TimeWorked -- an integer. time worked so far in minutes
222 TimeLeft -- an integer. time remaining in minutes
223 Starts -- an ISO date describing the ticket\'s start date and time in GMT
224 Due -- an ISO date describing the ticket\'s due date and time in GMT
225 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
226 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
228 Ticket links can be set up during create by passing the link type as a hask key and
229 the ticket id to be linked to as a value (or a URI when linking to other objects).
230 Multiple links of the same type can be created by passing an array ref. For example:
233 DependsOn => [ 15, 22 ],
234 RefersTo => 'http://www.bestpractical.com',
236 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
237 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
238 C<Members> and C<Children> are aliases for C<HasMember>.
240 Returns: TICKETID, Transaction Object, Error Message
250 EffectiveId => undef,
255 SquelchMailTo => undef,
259 InitialPriority => undef,
260 FinalPriority => undef,
271 _RecordTransaction => 1,
276 my ($ErrStr, @non_fatal_errors);
278 my $QueueObj = RT::Queue->new( $RT::SystemUser );
279 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
280 $QueueObj->Load( $args{'Queue'}->Id );
282 elsif ( $args{'Queue'} ) {
283 $QueueObj->Load( $args{'Queue'} );
286 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
289 #Can't create a ticket without a queue.
290 unless ( $QueueObj->Id ) {
291 $RT::Logger->debug("$self No queue given for ticket creation.");
292 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
296 #Now that we have a queue, Check the ACLS
298 $self->CurrentUser->HasRight(
299 Right => 'CreateTicket',
306 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
309 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
310 return ( 0, 0, $self->loc('Invalid value for status') );
313 #Since we have a queue, we can set queue defaults
316 # If there's no queue default initial priority and it's not set, set it to 0
317 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
318 unless defined $args{'InitialPriority'};
321 # If there's no queue default final priority and it's not set, set it to 0
322 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
323 unless defined $args{'FinalPriority'};
325 # Priority may have changed from InitialPriority, for the case
326 # where we're importing tickets (eg, from an older RT version.)
327 $args{'Priority'} = $args{'InitialPriority'}
328 unless defined $args{'Priority'};
331 #TODO we should see what sort of due date we're getting, rather +
332 # than assuming it's in ISO format.
334 #Set the due date. if we didn't get fed one, use the queue default due in
335 my $Due = new RT::Date( $self->CurrentUser );
336 if ( defined $args{'Due'} ) {
337 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
339 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
341 $Due->AddDays( $due_in );
344 my $Starts = new RT::Date( $self->CurrentUser );
345 if ( defined $args{'Starts'} ) {
346 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
349 my $Started = new RT::Date( $self->CurrentUser );
350 if ( defined $args{'Started'} ) {
351 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
353 elsif ( $args{'Status'} ne 'new' ) {
357 my $Resolved = new RT::Date( $self->CurrentUser );
358 if ( defined $args{'Resolved'} ) {
359 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
362 #If the status is an inactive status, set the resolved date
363 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
365 $RT::Logger->debug( "Got a ". $args{'Status'}
366 ."(inactive) ticket with undefined resolved date. Setting to now."
373 # {{{ Dealing with time fields
375 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
376 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
377 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
381 # {{{ Deal with setting the owner
384 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
385 if ( $args{'Owner'}->id ) {
386 $Owner = $args{'Owner'};
388 $RT::Logger->error('passed not loaded owner object');
389 push @non_fatal_errors, $self->loc("Invalid owner object");
394 #If we've been handed something else, try to load the user.
395 elsif ( $args{'Owner'} ) {
396 $Owner = RT::User->new( $self->CurrentUser );
397 $Owner->Load( $args{'Owner'} );
398 $Owner->LoadByEmail( $args{'Owner'} )
400 unless ( $Owner->Id ) {
401 push @non_fatal_errors,
402 $self->loc("Owner could not be set.") . " "
403 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
408 #If we have a proposed owner and they don't have the right
409 #to own a ticket, scream about it and make them not the owner
412 if ( $Owner && $Owner->Id != $RT::Nobody->Id
413 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
415 $DeferOwner = $Owner;
417 $RT::Logger->debug('going to deffer setting owner');
421 #If we haven't been handed a valid owner, make it nobody.
422 unless ( defined($Owner) && $Owner->Id ) {
423 $Owner = new RT::User( $self->CurrentUser );
424 $Owner->Load( $RT::Nobody->Id );
429 # We attempt to load or create each of the people who might have a role for this ticket
430 # _outside_ the transaction, so we don't get into ticket creation races
431 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
432 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
433 foreach my $watcher ( splice @{ $args{$type} } ) {
434 next unless $watcher;
435 if ( $watcher =~ /^\d+$/ ) {
436 push @{ $args{$type} }, $watcher;
438 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
439 foreach my $address( @addresses ) {
440 my $user = RT::User->new( $RT::SystemUser );
441 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
443 push @non_fatal_errors,
444 $self->loc("Couldn't load or create user: [_1]", $msg);
446 push @{ $args{$type} }, $user->id;
453 $RT::Handle->BeginTransaction();
456 Queue => $QueueObj->Id,
458 Subject => $args{'Subject'},
459 InitialPriority => $args{'InitialPriority'},
460 FinalPriority => $args{'FinalPriority'},
461 Priority => $args{'Priority'},
462 Status => $args{'Status'},
463 TimeWorked => $args{'TimeWorked'},
464 TimeEstimated => $args{'TimeEstimated'},
465 TimeLeft => $args{'TimeLeft'},
466 Type => $args{'Type'},
467 Starts => $Starts->ISO,
468 Started => $Started->ISO,
469 Resolved => $Resolved->ISO,
473 # Parameters passed in during an import that we probably don't want to touch, otherwise
474 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
475 $params{$attr} = $args{$attr} if $args{$attr};
478 # Delete null integer parameters
480 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
482 delete $params{$attr}
483 unless ( exists $params{$attr} && $params{$attr} );
486 # Delete the time worked if we're counting it in the transaction
487 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
489 my ($id,$ticket_message) = $self->SUPER::Create( %params );
491 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
492 $RT::Handle->Rollback();
494 $self->loc("Ticket could not be created due to an internal error")
498 #Set the ticket's effective ID now that we've created it.
499 my ( $val, $msg ) = $self->__Set(
500 Field => 'EffectiveId',
501 Value => ( $args{'EffectiveId'} || $id )
504 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
505 $RT::Handle->Rollback;
507 $self->loc("Ticket could not be created due to an internal error")
511 my $create_groups_ret = $self->_CreateTicketGroups();
512 unless ($create_groups_ret) {
513 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
515 . ". aborting Ticket creation." );
516 $RT::Handle->Rollback();
518 $self->loc("Ticket could not be created due to an internal error")
522 # Set the owner in the Groups table
523 # We denormalize it into the Ticket table too because doing otherwise would
524 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
525 $self->OwnerGroup->_AddMember(
526 PrincipalId => $Owner->PrincipalId,
527 InsideTransaction => 1
528 ) unless $DeferOwner;
532 # {{{ Deal with setting up watchers
534 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
535 # we know it's an array ref
536 foreach my $watcher ( @{ $args{$type} } ) {
538 # Note that we're using AddWatcher, rather than _AddWatcher, as we
539 # actually _want_ that ACL check. Otherwise, random ticket creators
540 # could make themselves adminccs and maybe get ticket rights. that would
542 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
544 my ($val, $msg) = $self->$method(
546 PrincipalId => $watcher,
549 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
554 if ($args{'SquelchMailTo'}) {
555 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
556 : $args{'SquelchMailTo'};
557 $self->_SquelchMailTo( @squelch );
563 # {{{ Add all the custom fields
565 foreach my $arg ( keys %args ) {
566 next unless $arg =~ /^CustomField-(\d+)$/i;
570 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
572 next unless defined $value && length $value;
574 # Allow passing in uploaded LargeContent etc by hash reference
575 my ($status, $msg) = $self->_AddCustomFieldValue(
576 (UNIVERSAL::isa( $value => 'HASH' )
581 RecordTransaction => 0,
583 push @non_fatal_errors, $msg unless $status;
589 # {{{ Deal with setting up links
591 # TODO: Adding link may fire scrips on other end and those scrips
592 # could create transactions on this ticket before 'Create' transaction.
594 # We should implement different schema: record 'Create' transaction,
595 # create links and only then fire create transaction's scrips.
597 # Ideal variant: add all links without firing scrips, record create
598 # transaction and only then fire scrips on the other ends of links.
602 foreach my $type ( keys %LINKTYPEMAP ) {
603 next unless ( defined $args{$type} );
605 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
607 # Check rights on the other end of the link if we must
608 # then run _AddLink that doesn't check for ACLs
609 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
610 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
612 push @non_fatal_errors, $msg;
615 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
616 push @non_fatal_errors, $self->loc('Linking. Permission denied');
621 #don't show transactions for reminders
622 my $silent = ( !$args{'_RecordTransaction'}
623 || $self->Type eq 'reminder'
626 my ( $wval, $wmsg ) = $self->_AddLink(
627 Type => $LINKTYPEMAP{$type}->{'Type'},
628 $LINKTYPEMAP{$type}->{'Mode'} => $link,
630 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
634 push @non_fatal_errors, $wmsg unless ($wval);
640 # {{{ Deal with auto-customer association
642 #unless we already have (a) customer(s)...
643 unless ( $self->Customers->Count ) {
645 #first find any requestors with emails but *without* customer targets
646 my @NoCust_Requestors =
647 grep { $_->EmailAddress && ! $_->Customers->Count }
648 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
650 for my $Requestor (@NoCust_Requestors) {
652 #perhaps the stuff in here should be in a User method??
654 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
656 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
658 ## false laziness w/RT/Interface/Web_Vendor.pm
659 my @link = ( 'Type' => 'MemberOf',
660 'Target' => "freeside://freeside/cust_main/$custnum",
663 my( $val, $msg ) = $Requestor->_AddLink(@link);
664 #XXX should do something with $msg# push @non_fatal_errors, $msg;
670 #find any requestors with customer targets
672 my %cust_target = ();
675 grep { $_->Customers->Count }
676 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
678 foreach my $Requestor ( @Requestors ) {
679 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
680 $cust_target{ $cust_link->Target } = 1;
684 #and then auto-associate this ticket with those customers
686 foreach my $cust_target ( keys %cust_target ) {
688 my @link = ( 'Type' => 'MemberOf',
689 #'Target' => "freeside://freeside/cust_main/$custnum",
690 'Target' => $cust_target,
693 my( $val, $msg ) = $self->_AddLink(@link);
694 push @non_fatal_errors, $msg;
702 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
703 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
705 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
707 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
708 . ") was proposed as a ticket owner but has no rights to own "
709 . "tickets in " . $QueueObj->Name );
710 push @non_fatal_errors, $self->loc(
711 "Owner '[_1]' does not have rights to own this ticket.",
715 $Owner = $DeferOwner;
716 $self->__Set(Field => 'Owner', Value => $Owner->id);
718 $self->OwnerGroup->_AddMember(
719 PrincipalId => $Owner->PrincipalId,
720 InsideTransaction => 1
724 #don't make a transaction or fire off any scrips for reminders either
725 if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
727 # {{{ Add a transaction for the create
728 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
730 TimeTaken => $args{'TimeWorked'},
731 MIMEObj => $args{'MIMEObj'},
732 CommitScrips => !$args{'DryRun'},
735 if ( $self->Id && $Trans ) {
737 $TransObj->UpdateCustomFields(ARGSRef => \%args);
739 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
740 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
741 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
744 $RT::Handle->Rollback();
746 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
747 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
748 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
751 if ( $args{'DryRun'} ) {
752 $RT::Handle->Rollback();
753 return ($self->id, $TransObj, $ErrStr);
755 $RT::Handle->Commit();
756 return ( $self->Id, $TransObj->Id, $ErrStr );
762 # Not going to record a transaction
763 $RT::Handle->Commit();
764 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
765 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
766 return ( $self->Id, 0, $ErrStr );
774 # {{{ _Parse822HeadersForAttributes Content
776 =head2 _Parse822HeadersForAttributes Content
778 Takes an RFC822 style message and parses its attributes into a hash.
782 sub _Parse822HeadersForAttributes {
787 my @lines = ( split ( /\n/, $content ) );
788 while ( defined( my $line = shift @lines ) ) {
789 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
794 if ( defined( $args{$tag} ) )
795 { #if we're about to get a second value, make it an array
796 $args{$tag} = [ $args{$tag} ];
798 if ( ref( $args{$tag} ) )
799 { #If it's an array, we want to push the value
800 push @{ $args{$tag} }, $value;
802 else { #if there's nothing there, just set the value
803 $args{$tag} = $value;
805 } elsif ($line =~ /^$/) {
807 #TODO: this won't work, since "" isn't of the form "foo:value"
809 while ( defined( my $l = shift @lines ) ) {
810 push @{ $args{'content'} }, $l;
816 foreach my $date qw(due starts started resolved) {
817 my $dateobj = RT::Date->new($RT::SystemUser);
818 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
819 $dateobj->Set( Format => 'unix', Value => $args{$date} );
822 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
824 $args{$date} = $dateobj->ISO;
826 $args{'mimeobj'} = MIME::Entity->new();
827 $args{'mimeobj'}->build(
828 Type => ( $args{'contenttype'} || 'text/plain' ),
829 Data => ($args{'content'} || '')
839 =head2 Import PARAMHASH
842 Doesn\'t create a transaction.
843 Doesn\'t supply queue defaults, etc.
851 my ( $ErrStr, $QueueObj, $Owner );
855 EffectiveId => undef,
859 Owner => $RT::Nobody->Id,
860 Subject => '[no subject]',
861 InitialPriority => undef,
862 FinalPriority => undef,
873 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
874 $QueueObj = RT::Queue->new($RT::SystemUser);
875 $QueueObj->Load( $args{'Queue'} );
877 #TODO error check this and return 0 if it\'s not loading properly +++
879 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
880 $QueueObj = RT::Queue->new($RT::SystemUser);
881 $QueueObj->Load( $args{'Queue'}->Id );
885 "$self " . $args{'Queue'} . " not a recognised queue object." );
888 #Can't create a ticket without a queue.
889 unless ( defined($QueueObj) and $QueueObj->Id ) {
890 $RT::Logger->debug("$self No queue given for ticket creation.");
891 return ( 0, $self->loc('Could not create ticket. Queue not set') );
894 #Now that we have a queue, Check the ACLS
896 $self->CurrentUser->HasRight(
897 Right => 'CreateTicket',
903 $self->loc("No permission to create tickets in the queue '[_1]'"
907 # {{{ Deal with setting the owner
909 # Attempt to take user object, user name or user id.
910 # Assign to nobody if lookup fails.
911 if ( defined( $args{'Owner'} ) ) {
912 if ( ref( $args{'Owner'} ) ) {
913 $Owner = $args{'Owner'};
916 $Owner = new RT::User( $self->CurrentUser );
917 $Owner->Load( $args{'Owner'} );
918 if ( !defined( $Owner->id ) ) {
919 $Owner->Load( $RT::Nobody->id );
924 #If we have a proposed owner and they don't have the right
925 #to own a ticket, scream about it and make them not the owner
928 and ( $Owner->Id != $RT::Nobody->Id )
938 $RT::Logger->warning( "$self user "
942 . "as a ticket owner but has no rights to own "
944 . $QueueObj->Name . "'" );
949 #If we haven't been handed a valid owner, make it nobody.
950 unless ( defined($Owner) ) {
951 $Owner = new RT::User( $self->CurrentUser );
952 $Owner->Load( $RT::Nobody->UserObj->Id );
957 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
958 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
961 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
962 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
963 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
964 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
966 # If we're coming in with an id, set that now.
967 my $EffectiveId = undef;
969 $EffectiveId = $args{'id'};
973 my $id = $self->SUPER::Create(
975 EffectiveId => $EffectiveId,
976 Queue => $QueueObj->Id,
978 Subject => $args{'Subject'}, # loc
979 InitialPriority => $args{'InitialPriority'}, # loc
980 FinalPriority => $args{'FinalPriority'}, # loc
981 Priority => $args{'InitialPriority'}, # loc
982 Status => $args{'Status'}, # loc
983 TimeWorked => $args{'TimeWorked'}, # loc
984 Type => $args{'Type'}, # loc
985 Created => $args{'Created'}, # loc
986 Told => $args{'Told'}, # loc
987 LastUpdated => $args{'Updated'}, # loc
988 Resolved => $args{'Resolved'}, # loc
989 Due => $args{'Due'}, # loc
992 # If the ticket didn't have an id
993 # Set the ticket's effective ID now that we've created it.
995 $self->Load( $args{'id'} );
999 $self->__Set( Field => 'EffectiveId', Value => $id );
1003 $self . "->Import couldn't set EffectiveId: $msg" );
1007 my $create_groups_ret = $self->_CreateTicketGroups();
1008 unless ($create_groups_ret) {
1010 "Couldn't create ticket groups for ticket " . $self->Id );
1013 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1016 foreach $watcher ( @{ $args{'Cc'} } ) {
1017 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1019 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1020 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1023 foreach $watcher ( @{ $args{'Requestor'} } ) {
1024 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1028 return ( $self->Id, $ErrStr );
1033 # {{{ Routines dealing with watchers.
1035 # {{{ _CreateTicketGroups
1037 =head2 _CreateTicketGroups
1039 Create the ticket groups and links for this ticket.
1040 This routine expects to be called from Ticket->Create _inside of a transaction_
1042 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1044 It will return true on success and undef on failure.
1050 sub _CreateTicketGroups {
1053 my @types = qw(Requestor Owner Cc AdminCc);
1055 foreach my $type (@types) {
1056 my $type_obj = RT::Group->new($self->CurrentUser);
1057 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1058 Instance => $self->Id,
1061 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1062 $self->Id.": ".$msg);
1072 # {{{ sub OwnerGroup
1076 A constructor which returns an RT::Group object containing the owner of this ticket.
1082 my $owner_obj = RT::Group->new($self->CurrentUser);
1083 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1084 return ($owner_obj);
1090 # {{{ sub AddWatcher
1094 AddWatcher takes a parameter hash. The keys are as follows:
1096 Type One of Requestor, Cc, AdminCc
1098 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1100 Email The email address of the new watcher. If a user with this
1101 email address can't be found, a new nonprivileged user will be created.
1103 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.
1111 PrincipalId => undef,
1116 # ModifyTicket works in any case
1117 return $self->_AddWatcher( %args )
1118 if $self->CurrentUserHasRight('ModifyTicket');
1119 if ( $args{'Email'} ) {
1120 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1121 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1124 if ( lc $self->CurrentUser->UserObj->EmailAddress
1125 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1127 $args{'PrincipalId'} = $self->CurrentUser->id;
1128 delete $args{'Email'};
1132 # If the watcher isn't the current user then the current user has no right
1134 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1135 return ( 0, $self->loc("Permission Denied") );
1138 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1139 if ( $args{'Type'} eq 'AdminCc' ) {
1140 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1141 return ( 0, $self->loc('Permission Denied') );
1145 # If it's a Requestor or Cc and they don't have 'Watch', bail
1146 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1147 unless ( $self->CurrentUserHasRight('Watch') ) {
1148 return ( 0, $self->loc('Permission Denied') );
1152 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1153 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1156 return $self->_AddWatcher( %args );
1159 #This contains the meat of AddWatcher. but can be called from a routine like
1160 # Create, which doesn't need the additional acl check
1166 PrincipalId => undef,
1172 my $principal = RT::Principal->new($self->CurrentUser);
1173 if ($args{'Email'}) {
1174 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1175 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'})));
1177 my $user = RT::User->new($RT::SystemUser);
1178 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1179 $args{'PrincipalId'} = $pid if $pid;
1181 if ($args{'PrincipalId'}) {
1182 $principal->Load($args{'PrincipalId'});
1183 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1184 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'})))
1185 if RT::EmailParser->IsRTAddress( $email );
1191 # If we can't find this watcher, we need to bail.
1192 unless ($principal->Id) {
1193 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1194 return(0, $self->loc("Could not find or create that user"));
1198 my $group = RT::Group->new($self->CurrentUser);
1199 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1200 unless ($group->id) {
1201 return(0,$self->loc("Group not found"));
1204 if ( $group->HasMember( $principal)) {
1206 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1210 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1211 InsideTransaction => 1 );
1213 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1215 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1218 unless ( $args{'Silent'} ) {
1219 $self->_NewTransaction(
1220 Type => 'AddWatcher',
1221 NewValue => $principal->Id,
1222 Field => $args{'Type'}
1226 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1232 # {{{ sub DeleteWatcher
1234 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1237 Deletes a Ticket watcher. Takes two arguments:
1239 Type (one of Requestor,Cc,AdminCc)
1243 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1245 Email (the email address of an existing wathcer)
1254 my %args = ( Type => undef,
1255 PrincipalId => undef,
1259 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1260 return ( 0, $self->loc("No principal specified") );
1262 my $principal = RT::Principal->new( $self->CurrentUser );
1263 if ( $args{'PrincipalId'} ) {
1265 $principal->Load( $args{'PrincipalId'} );
1268 my $user = RT::User->new( $self->CurrentUser );
1269 $user->LoadByEmail( $args{'Email'} );
1270 $principal->Load( $user->Id );
1273 # If we can't find this watcher, we need to bail.
1274 unless ( $principal->Id ) {
1275 return ( 0, $self->loc("Could not find that principal") );
1278 my $group = RT::Group->new( $self->CurrentUser );
1279 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1280 unless ( $group->id ) {
1281 return ( 0, $self->loc("Group not found") );
1285 #If the watcher we're trying to add is for the current user
1286 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1288 # If it's an AdminCc and they don't have
1289 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1290 if ( $args{'Type'} eq 'AdminCc' ) {
1291 unless ( $self->CurrentUserHasRight('ModifyTicket')
1292 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1293 return ( 0, $self->loc('Permission Denied') );
1297 # If it's a Requestor or Cc and they don't have
1298 # 'Watch' or 'ModifyTicket', bail
1299 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1301 unless ( $self->CurrentUserHasRight('ModifyTicket')
1302 or $self->CurrentUserHasRight('Watch') ) {
1303 return ( 0, $self->loc('Permission Denied') );
1307 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1309 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1313 # If the watcher isn't the current user
1314 # and the current user doesn't have 'ModifyTicket' bail
1316 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1317 return ( 0, $self->loc("Permission Denied") );
1323 # see if this user is already a watcher.
1325 unless ( $group->HasMember($principal) ) {
1327 $self->loc( 'That principal is not a [_1] for this ticket',
1331 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1333 $RT::Logger->error( "Failed to delete "
1335 . " as a member of group "
1341 'Could not remove that principal as a [_1] for this ticket',
1345 unless ( $args{'Silent'} ) {
1346 $self->_NewTransaction( Type => 'DelWatcher',
1347 OldValue => $principal->Id,
1348 Field => $args{'Type'} );
1352 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1353 $principal->Object->Name,
1362 =head2 SquelchMailTo [EMAIL]
1364 Takes an optional email address to never email about updates to this ticket.
1367 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1375 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1379 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1384 return $self->_SquelchMailTo(@_);
1387 sub _SquelchMailTo {
1391 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1392 unless grep { $_->Content eq $attr }
1393 $self->Attributes->Named('SquelchMailTo');
1395 my @attributes = $self->Attributes->Named('SquelchMailTo');
1396 return (@attributes);
1400 =head2 UnsquelchMailTo ADDRESS
1402 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1404 Returns a tuple of (status, message)
1408 sub UnsquelchMailTo {
1411 my $address = shift;
1412 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1413 return ( 0, $self->loc("Permission Denied") );
1416 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1417 return ($val, $msg);
1421 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1423 =head2 RequestorAddresses
1425 B<Returns> String: All Ticket Requestor email addresses as a string.
1429 sub RequestorAddresses {
1432 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1436 return ( $self->Requestors->MemberEmailAddressesAsString );
1440 =head2 AdminCcAddresses
1442 returns String: All Ticket AdminCc email addresses as a string
1446 sub AdminCcAddresses {
1449 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1453 return ( $self->AdminCc->MemberEmailAddressesAsString )
1459 returns String: All Ticket Ccs as a string of email addresses
1466 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1469 return ( $self->Cc->MemberEmailAddressesAsString);
1475 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1477 # {{{ sub Requestors
1482 Returns this ticket's Requestors as an RT::Group object
1489 my $group = RT::Group->new($self->CurrentUser);
1490 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1491 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1499 # {{{ sub _Requestors
1503 Private non-ACLed variant of Reqeustors so that we can look them up for the
1504 purposes of customer auto-association during create.
1511 my $group = RT::Group->new($RT::SystemUser);
1512 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1523 Returns an RT::Group object which contains this ticket's Ccs.
1524 If the user doesn't have "ShowTicket" permission, returns an empty group
1531 my $group = RT::Group->new($self->CurrentUser);
1532 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1533 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1546 Returns an RT::Group object which contains this ticket's AdminCcs.
1547 If the user doesn't have "ShowTicket" permission, returns an empty group
1554 my $group = RT::Group->new($self->CurrentUser);
1555 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1556 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1566 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1569 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1571 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1573 Takes a param hash with the attributes Type and either PrincipalId or Email
1575 Type is one of Requestor, Cc, AdminCc and Owner
1577 PrincipalId is an RT::Principal id, and Email is an email address.
1579 Returns true if the specified principal (or the one corresponding to the
1580 specified address) is a member of the group Type for this ticket.
1582 XX TODO: This should be Memoized.
1589 my %args = ( Type => 'Requestor',
1590 PrincipalId => undef,
1595 # Load the relevant group.
1596 my $group = RT::Group->new($self->CurrentUser);
1597 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1599 # Find the relevant principal.
1600 if (!$args{PrincipalId} && $args{Email}) {
1601 # Look up the specified user.
1602 my $user = RT::User->new($self->CurrentUser);
1603 $user->LoadByEmail($args{Email});
1605 $args{PrincipalId} = $user->PrincipalId;
1608 # A non-existent user can't be a group member.
1613 # Ask if it has the member in question
1614 return $group->HasMember( $args{'PrincipalId'} );
1619 # {{{ sub IsRequestor
1621 =head2 IsRequestor PRINCIPAL_ID
1623 Takes an L<RT::Principal> id.
1625 Returns true if the principal is a requestor of the current ticket.
1633 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1641 =head2 IsCc PRINCIPAL_ID
1643 Takes an RT::Principal id.
1644 Returns true if the principal is a Cc of the current ticket.
1653 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1661 =head2 IsAdminCc PRINCIPAL_ID
1663 Takes an RT::Principal id.
1664 Returns true if the principal is an AdminCc of the current ticket.
1672 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1682 Takes an RT::User object. Returns true if that user is this ticket's owner.
1683 returns undef otherwise
1691 # no ACL check since this is used in acl decisions
1692 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1696 #Tickets won't yet have owners when they're being created.
1697 unless ( $self->OwnerObj->id ) {
1701 if ( $person->id == $self->OwnerObj->id ) {
1716 =head2 TransactionAddresses
1718 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1719 all this ticket's Create, Comment or Correspond transactions. The keys are
1720 stringified email addresses. Each value is an L<Email::Address> object.
1722 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.
1727 sub TransactionAddresses {
1729 my $txns = $self->Transactions;
1732 foreach my $type (qw(Create Comment Correspond)) {
1733 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1736 while (my $txn = $txns->Next) {
1737 my $txnaddrs = $txn->Addresses;
1738 foreach my $addrlist ( values %$txnaddrs ) {
1739 foreach my $addr (@$addrlist) {
1740 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1741 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1742 # skips "comment-only" addresses
1743 next unless ($addr->address);
1744 $addresses{$addr->address} = $addr;
1756 # {{{ Routines dealing with queues
1758 # {{{ sub ValidateQueue
1765 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1769 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1770 my $id = $QueueObj->Load($Value);
1786 my $NewQueue = shift;
1788 #Redundant. ACL gets checked in _Set;
1789 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1790 return ( 0, $self->loc("Permission Denied") );
1793 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1794 $NewQueueObj->Load($NewQueue);
1796 unless ( $NewQueueObj->Id() ) {
1797 return ( 0, $self->loc("That queue does not exist") );
1800 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1801 return ( 0, $self->loc('That is the same value') );
1804 $self->CurrentUser->HasRight(
1805 Right => 'CreateTicket',
1806 Object => $NewQueueObj
1810 return ( 0, $self->loc("You may not create requests in that queue.") );
1814 $self->OwnerObj->HasRight(
1815 Right => 'OwnTicket',
1816 Object => $NewQueueObj
1820 my $clone = RT::Ticket->new( $RT::SystemUser );
1821 $clone->Load( $self->Id );
1822 unless ( $clone->Id ) {
1823 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1825 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1826 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1829 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1832 # On queue change, change queue for reminders too
1833 my $reminder_collection = $self->Reminders->Collection;
1834 while ( my $reminder = $reminder_collection->Next ) {
1835 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1836 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1840 return ($status, $msg);
1849 Takes nothing. returns this ticket's queue object
1856 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1858 #We call __Value so that we can avoid the ACL decision and some deep recursion
1859 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1860 return ($queue_obj);
1867 # {{{ Date printing routines
1873 Returns an RT::Date object containing this ticket's due date
1880 my $time = new RT::Date( $self->CurrentUser );
1882 # -1 is RT::Date slang for never
1883 if ( my $due = $self->Due ) {
1884 $time->Set( Format => 'sql', Value => $due );
1887 $time->Set( Format => 'unix', Value => -1 );
1895 # {{{ sub DueAsString
1899 Returns this ticket's due date as a human readable string
1905 return $self->DueObj->AsString();
1910 # {{{ sub ResolvedObj
1914 Returns an RT::Date object of this ticket's 'resolved' time.
1921 my $time = new RT::Date( $self->CurrentUser );
1922 $time->Set( Format => 'sql', Value => $self->Resolved );
1928 # {{{ sub SetStarted
1932 Takes a date in ISO format or undef
1933 Returns a transaction id and a message
1934 The client calls "Start" to note that the project was started on the date in $date.
1935 A null date means "now"
1941 my $time = shift || 0;
1943 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1944 return ( 0, $self->loc("Permission Denied") );
1947 #We create a date object to catch date weirdness
1948 my $time_obj = new RT::Date( $self->CurrentUser() );
1950 $time_obj->Set( Format => 'ISO', Value => $time );
1953 $time_obj->SetToNow();
1956 #Now that we're starting, open this ticket
1957 #TODO do we really want to force this as policy? it should be a scrip
1959 #We need $TicketAsSystem, in case the current user doesn't have
1962 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1963 $TicketAsSystem->Load( $self->Id );
1964 if ( $TicketAsSystem->Status eq 'new' ) {
1965 $TicketAsSystem->Open();
1968 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1974 # {{{ sub StartedObj
1978 Returns an RT::Date object which contains this ticket's
1986 my $time = new RT::Date( $self->CurrentUser );
1987 $time->Set( Format => 'sql', Value => $self->Started );
1997 Returns an RT::Date object which contains this ticket's
2005 my $time = new RT::Date( $self->CurrentUser );
2006 $time->Set( Format => 'sql', Value => $self->Starts );
2016 Returns an RT::Date object which contains this ticket's
2024 my $time = new RT::Date( $self->CurrentUser );
2025 $time->Set( Format => 'sql', Value => $self->Told );
2031 # {{{ sub ToldAsString
2035 A convenience method that returns ToldObj->AsString
2037 TODO: This should be deprecated
2043 if ( $self->Told ) {
2044 return $self->ToldObj->AsString();
2053 # {{{ sub TimeWorkedAsString
2055 =head2 TimeWorkedAsString
2057 Returns the amount of time worked on this ticket as a Text String
2061 sub TimeWorkedAsString {
2063 my $value = $self->TimeWorked;
2065 # return the # of minutes worked turned into seconds and written as
2066 # a simple text string, this is not really a date object, but if we
2067 # diff a number of seconds vs the epoch, we'll get a nice description
2069 return "" unless $value;
2070 return RT::Date->new( $self->CurrentUser )
2071 ->DurationAsString( $value * 60 );
2076 # {{{ sub TimeLeftAsString
2078 =head2 TimeLeftAsString
2080 Returns the amount of time left on this ticket as a Text String
2084 sub TimeLeftAsString {
2086 my $value = $self->TimeLeft;
2087 return "" unless $value;
2088 return RT::Date->new( $self->CurrentUser )
2089 ->DurationAsString( $value * 60 );
2094 # {{{ Routines dealing with correspondence/comments
2100 Comment on this ticket.
2101 Takes a hash with the following attributes:
2102 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2105 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2107 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2108 They will, however, be prepared and you'll be able to access them through the TransactionObj
2110 Returns: Transaction id, Error Message, Transaction Object
2111 (note the different order from Create()!)
2118 my %args = ( CcMessageTo => undef,
2119 BccMessageTo => undef,
2126 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2127 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2128 return ( 0, $self->loc("Permission Denied"), undef );
2130 $args{'NoteType'} = 'Comment';
2132 if ($args{'DryRun'}) {
2133 $RT::Handle->BeginTransaction();
2134 $args{'CommitScrips'} = 0;
2137 my @results = $self->_RecordNote(%args);
2138 if ($args{'DryRun'}) {
2139 $RT::Handle->Rollback();
2146 # {{{ sub Correspond
2150 Correspond on this ticket.
2151 Takes a hashref with the following attributes:
2154 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2156 if there's no MIMEObj, Content is used to build a MIME::Entity object
2158 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2159 They will, however, be prepared and you'll be able to access them through the TransactionObj
2161 Returns: Transaction id, Error Message, Transaction Object
2162 (note the different order from Create()!)
2169 my %args = ( CcMessageTo => undef,
2170 BccMessageTo => undef,
2176 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2177 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2178 return ( 0, $self->loc("Permission Denied"), undef );
2181 $args{'NoteType'} = 'Correspond';
2182 if ($args{'DryRun'}) {
2183 $RT::Handle->BeginTransaction();
2184 $args{'CommitScrips'} = 0;
2187 my @results = $self->_RecordNote(%args);
2189 #Set the last told date to now if this isn't mail from the requestor.
2190 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2191 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2193 if ($args{'DryRun'}) {
2194 $RT::Handle->Rollback();
2203 # {{{ sub _RecordNote
2207 the meat of both comment and correspond.
2209 Performs no access control checks. hence, dangerous.
2216 CcMessageTo => undef,
2217 BccMessageTo => undef,
2222 NoteType => 'Correspond',
2228 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2229 return ( 0, $self->loc("No message attached"), undef );
2232 unless ( $args{'MIMEObj'} ) {
2233 $args{'MIMEObj'} = MIME::Entity->build(
2234 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2238 # convert text parts into utf-8
2239 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2241 # If we've been passed in CcMessageTo and BccMessageTo fields,
2242 # add them to the mime object for passing on to the transaction handler
2243 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2244 # RT-Send-Bcc: headers
2247 foreach my $type (qw/Cc Bcc/) {
2248 if ( defined $args{ $type . 'MessageTo' } ) {
2250 my $addresses = join ', ', (
2251 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2252 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2253 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2257 foreach my $argument (qw(Encrypt Sign)) {
2258 $args{'MIMEObj'}->head->add(
2259 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2260 ) if defined $args{ $argument };
2263 # If this is from an external source, we need to come up with its
2264 # internal Message-ID now, so all emails sent because of this
2265 # message have a common Message-ID
2266 my $org = RT->Config->Get('Organization');
2267 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2268 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2269 $args{'MIMEObj'}->head->set(
2270 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2274 #Record the correspondence (write the transaction)
2275 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2276 Type => $args{'NoteType'},
2277 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2278 TimeTaken => $args{'TimeTaken'},
2279 MIMEObj => $args{'MIMEObj'},
2280 CommitScrips => $args{'CommitScrips'},
2284 $RT::Logger->err("$self couldn't init a transaction $msg");
2285 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2288 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2300 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2303 my $type = shift || "";
2305 my $cache_key = "$field$type";
2306 return $self->{ $cache_key } if $self->{ $cache_key };
2308 my $links = $self->{ $cache_key }
2309 = RT::Links->new( $self->CurrentUser );
2310 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2311 $links->Limit( FIELD => 'id', VALUE => 0 );
2315 # without this you will also get RT::User(s) instead of tickets!
2316 if ($field == 'Base' and $type == 'MemberOf') {
2317 my $rtname = RT->Config->Get('rtname');
2320 OPERATOR => 'STARTSWITH',
2321 VALUE => "fsck.com-rt://$rtname/ticket/",
2325 # Maybe this ticket is a merge ticket
2326 my $limit_on = 'Local'. $field;
2327 # at least to myself
2331 ENTRYAGGREGATOR => 'OR',
2336 ENTRYAGGREGATOR => 'OR',
2337 ) foreach $self->Merged;
2348 # {{{ sub DeleteLink
2352 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2353 SilentBase and SilentTarget. Either Base or Target must be null.
2354 The null value will be replaced with this ticket\'s id.
2356 If Silent is true then no transaction would be recorded, in other
2357 case you can control creation of transactions on both base and
2358 target with SilentBase and SilentTarget respectively. By default
2359 both transactions are created.
2370 SilentBase => undef,
2371 SilentTarget => undef,
2375 unless ( $args{'Target'} || $args{'Base'} ) {
2376 $RT::Logger->error("Base or Target must be specified");
2377 return ( 0, $self->loc('Either base or target must be specified') );
2382 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2383 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2384 return ( 0, $self->loc("Permission Denied") );
2387 # If the other URI is an RT::Ticket, we want to make sure the user
2388 # can modify it too...
2389 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2390 return (0, $msg) unless $status;
2391 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2394 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2395 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2397 return ( 0, $self->loc("Permission Denied") );
2400 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2401 return ( 0, $Msg ) unless $val;
2403 return ( $val, $Msg ) if $args{'Silent'};
2405 my ($direction, $remote_link);
2407 if ( $args{'Base'} ) {
2408 $remote_link = $args{'Base'};
2409 $direction = 'Target';
2411 elsif ( $args{'Target'} ) {
2412 $remote_link = $args{'Target'};
2413 $direction = 'Base';
2416 my $remote_uri = RT::URI->new( $self->CurrentUser );
2417 $remote_uri->FromURI( $remote_link );
2419 unless ( $args{ 'Silent'. $direction } ) {
2420 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2421 Type => 'DeleteLink',
2422 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2423 OldValue => $remote_uri->URI || $remote_link,
2426 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2429 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2430 my $OtherObj = $remote_uri->Object;
2431 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2432 Type => 'DeleteLink',
2433 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2434 : $LINKDIRMAP{$args{'Type'}}->{Target},
2435 OldValue => $self->URI,
2436 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2439 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2442 return ( $val, $Msg );
2451 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2453 If Silent is true then no transaction would be recorded, in other
2454 case you can control creation of transactions on both base and
2455 target with SilentBase and SilentTarget respectively. By default
2456 both transactions are created.
2462 my %args = ( Target => '',
2466 SilentBase => undef,
2467 SilentTarget => undef,
2470 unless ( $args{'Target'} || $args{'Base'} ) {
2471 $RT::Logger->error("Base or Target must be specified");
2472 return ( 0, $self->loc('Either base or target must be specified') );
2476 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2477 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2478 return ( 0, $self->loc("Permission Denied") );
2481 # If the other URI is an RT::Ticket, we want to make sure the user
2482 # can modify it too...
2483 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2484 return (0, $msg) unless $status;
2485 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2488 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2489 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2491 return ( 0, $self->loc("Permission Denied") );
2494 return $self->_AddLink(%args);
2497 sub __GetTicketFromURI {
2499 my %args = ( URI => '', @_ );
2501 # If the other URI is an RT::Ticket, we want to make sure the user
2502 # can modify it too...
2503 my $uri_obj = RT::URI->new( $self->CurrentUser );
2504 $uri_obj->FromURI( $args{'URI'} );
2506 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2507 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2508 $RT::Logger->warning( $msg );
2511 my $obj = $uri_obj->Resolver->Object;
2512 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2513 return (1, 'Found not a ticket', undef);
2515 return (1, 'Found ticket', $obj);
2520 Private non-acled variant of AddLink so that links can be added during create.
2526 my %args = ( Target => '',
2530 SilentBase => undef,
2531 SilentTarget => undef,
2534 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2535 return ($val, $msg) if !$val || $exist;
2536 return ($val, $msg) if $args{'Silent'};
2538 my ($direction, $remote_link);
2539 if ( $args{'Target'} ) {
2540 $remote_link = $args{'Target'};
2541 $direction = 'Base';
2542 } elsif ( $args{'Base'} ) {
2543 $remote_link = $args{'Base'};
2544 $direction = 'Target';
2547 my $remote_uri = RT::URI->new( $self->CurrentUser );
2548 $remote_uri->FromURI( $remote_link );
2550 unless ( $args{ 'Silent'. $direction } ) {
2551 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2553 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2554 NewValue => $remote_uri->URI || $remote_link,
2557 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2560 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2561 my $OtherObj = $remote_uri->Object;
2562 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2564 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2565 : $LINKDIRMAP{$args{'Type'}}->{Target},
2566 NewValue => $self->URI,
2567 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2570 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2573 return ( $val, $msg );
2583 MergeInto take the id of the ticket to merge this ticket into.
2589 my $ticket_id = shift;
2591 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2592 return ( 0, $self->loc("Permission Denied") );
2595 # Load up the new ticket.
2596 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2597 $MergeInto->Load($ticket_id);
2599 # make sure it exists.
2600 unless ( $MergeInto->Id ) {
2601 return ( 0, $self->loc("New ticket doesn't exist") );
2604 # Make sure the current user can modify the new ticket.
2605 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2606 return ( 0, $self->loc("Permission Denied") );
2609 delete $MERGE_CACHE{'effective'}{ $self->id };
2610 delete @{ $MERGE_CACHE{'merged'} }{
2611 $ticket_id, $MergeInto->id, $self->id
2614 $RT::Handle->BeginTransaction();
2616 # We use EffectiveId here even though it duplicates information from
2617 # the links table becasue of the massive performance hit we'd take
2618 # by trying to do a separate database query for merge info everytime
2621 #update this ticket's effective id to the new ticket's id.
2622 my ( $id_val, $id_msg ) = $self->__Set(
2623 Field => 'EffectiveId',
2624 Value => $MergeInto->Id()
2628 $RT::Handle->Rollback();
2629 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2633 if ( $self->__Value('Status') ne 'resolved' ) {
2635 my ( $status_val, $status_msg )
2636 = $self->__Set( Field => 'Status', Value => 'resolved' );
2638 unless ($status_val) {
2639 $RT::Handle->Rollback();
2642 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2646 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2650 # update all the links that point to that old ticket
2651 my $old_links_to = RT::Links->new($self->CurrentUser);
2652 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2655 while (my $link = $old_links_to->Next) {
2656 if (exists $old_seen{$link->Base."-".$link->Type}) {
2659 elsif ($link->Base eq $MergeInto->URI) {
2662 # First, make sure the link doesn't already exist. then move it over.
2663 my $tmp = RT::Link->new($RT::SystemUser);
2664 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2668 $link->SetTarget($MergeInto->URI);
2669 $link->SetLocalTarget($MergeInto->id);
2671 $old_seen{$link->Base."-".$link->Type} =1;
2676 my $old_links_from = RT::Links->new($self->CurrentUser);
2677 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2679 while (my $link = $old_links_from->Next) {
2680 if (exists $old_seen{$link->Type."-".$link->Target}) {
2683 if ($link->Target eq $MergeInto->URI) {
2686 # First, make sure the link doesn't already exist. then move it over.
2687 my $tmp = RT::Link->new($RT::SystemUser);
2688 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2692 $link->SetBase($MergeInto->URI);
2693 $link->SetLocalBase($MergeInto->id);
2694 $old_seen{$link->Type."-".$link->Target} =1;
2700 # Update time fields
2701 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2703 my $mutator = "Set$type";
2704 $MergeInto->$mutator(
2705 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2708 #add all of this ticket's watchers to that ticket.
2709 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2711 my $people = $self->$watcher_type->MembersObj;
2712 my $addwatcher_type = $watcher_type;
2713 $addwatcher_type =~ s/s$//;
2715 while ( my $watcher = $people->Next ) {
2717 my ($val, $msg) = $MergeInto->_AddWatcher(
2718 Type => $addwatcher_type,
2720 PrincipalId => $watcher->MemberId
2723 $RT::Logger->warning($msg);
2729 #find all of the tickets that were merged into this ticket.
2730 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2731 $old_mergees->Limit(
2732 FIELD => 'EffectiveId',
2737 # update their EffectiveId fields to the new ticket's id
2738 while ( my $ticket = $old_mergees->Next() ) {
2739 my ( $val, $msg ) = $ticket->__Set(
2740 Field => 'EffectiveId',
2741 Value => $MergeInto->Id()
2745 #make a new link: this ticket is merged into that other ticket.
2746 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2748 $MergeInto->_SetLastUpdated;
2750 $RT::Handle->Commit();
2751 return ( 1, $self->loc("Merge Successful") );
2756 Returns list of tickets' ids that's been merged into this ticket.
2764 return @{ $MERGE_CACHE{'merged'}{ $id } }
2765 if $MERGE_CACHE{'merged'}{ $id };
2767 my $mergees = RT::Tickets->new( $self->CurrentUser );
2769 FIELD => 'EffectiveId',
2777 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2778 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2785 # {{{ Routines dealing with ownership
2791 Takes nothing and returns an RT::User object of
2799 #If this gets ACLed, we lose on a rights check in User.pm and
2800 #get deep recursion. if we need ACLs here, we need
2801 #an equiv without ACLs
2803 my $owner = new RT::User( $self->CurrentUser );
2804 $owner->Load( $self->__Value('Owner') );
2806 #Return the owner object
2812 # {{{ sub OwnerAsString
2814 =head2 OwnerAsString
2816 Returns the owner's email address
2822 return ( $self->OwnerObj->EmailAddress );
2832 Takes two arguments:
2833 the Id or Name of the owner
2834 and (optionally) the type of the SetOwner Transaction. It defaults
2835 to 'Give'. 'Steal' is also a valid option.
2842 my $NewOwner = shift;
2843 my $Type = shift || "Give";
2845 $RT::Handle->BeginTransaction();
2847 $self->_SetLastUpdated(); # lock the ticket
2848 $self->Load( $self->id ); # in case $self changed while waiting for lock
2850 my $OldOwnerObj = $self->OwnerObj;
2852 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2853 $NewOwnerObj->Load( $NewOwner );
2854 unless ( $NewOwnerObj->Id ) {
2855 $RT::Handle->Rollback();
2856 return ( 0, $self->loc("That user does not exist") );
2860 # must have ModifyTicket rights
2861 # or TakeTicket/StealTicket and $NewOwner is self
2862 # see if it's a take
2863 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2864 unless ( $self->CurrentUserHasRight('ModifyTicket')
2865 || $self->CurrentUserHasRight('TakeTicket') ) {
2866 $RT::Handle->Rollback();
2867 return ( 0, $self->loc("Permission Denied") );
2871 # see if it's a steal
2872 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2873 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2875 unless ( $self->CurrentUserHasRight('ModifyTicket')
2876 || $self->CurrentUserHasRight('StealTicket') ) {
2877 $RT::Handle->Rollback();
2878 return ( 0, $self->loc("Permission Denied") );
2882 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2883 $RT::Handle->Rollback();
2884 return ( 0, $self->loc("Permission Denied") );
2888 # If we're not stealing and the ticket has an owner and it's not
2890 if ( $Type ne 'Steal' and $Type ne 'Force'
2891 and $OldOwnerObj->Id != $RT::Nobody->Id
2892 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2894 $RT::Handle->Rollback();
2895 return ( 0, $self->loc("You can only take tickets that are unowned") )
2896 if $NewOwnerObj->id == $self->CurrentUser->id;
2899 $self->loc("You can only reassign tickets that you own or that are unowned" )
2903 #If we've specified a new owner and that user can't modify the ticket
2904 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2905 $RT::Handle->Rollback();
2906 return ( 0, $self->loc("That user may not own tickets in that queue") );
2909 # If the ticket has an owner and it's the new owner, we don't need
2911 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2912 $RT::Handle->Rollback();
2913 return ( 0, $self->loc("That user already owns that ticket") );
2916 # Delete the owner in the owner group, then add a new one
2917 # TODO: is this safe? it's not how we really want the API to work
2918 # for most things, but it's fast.
2919 my ( $del_id, $del_msg );
2920 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2921 ($del_id, $del_msg) = $owner->Delete();
2922 last unless ($del_id);
2926 $RT::Handle->Rollback();
2927 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2930 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2931 PrincipalId => $NewOwnerObj->PrincipalId,
2932 InsideTransaction => 1 );
2934 $RT::Handle->Rollback();
2935 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2938 # We call set twice with slightly different arguments, so
2939 # as to not have an SQL transaction span two RT transactions
2941 my ( $val, $msg ) = $self->_Set(
2943 RecordTransaction => 0,
2944 Value => $NewOwnerObj->Id,
2946 TransactionType => $Type,
2947 CheckACL => 0, # don't check acl
2951 $RT::Handle->Rollback;
2952 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2955 ($val, $msg) = $self->_NewTransaction(
2958 NewValue => $NewOwnerObj->Id,
2959 OldValue => $OldOwnerObj->Id,
2964 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2965 $OldOwnerObj->Name, $NewOwnerObj->Name );
2968 $RT::Handle->Rollback();
2972 $RT::Handle->Commit();
2974 return ( $val, $msg );
2983 A convenince method to set the ticket's owner to the current user
2989 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2998 Convenience method to set the owner to 'nobody' if the current user is the owner.
3004 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3013 A convenience method to change the owner of the current ticket to the
3014 current user. Even if it's owned by another user.
3021 if ( $self->IsOwner( $self->CurrentUser ) ) {
3022 return ( 0, $self->loc("You already own this ticket") );
3025 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3035 # {{{ Routines dealing with status
3037 # {{{ sub ValidateStatus
3039 =head2 ValidateStatus STATUS
3041 Takes a string. Returns true if that status is a valid status for this ticket.
3042 Returns false otherwise.
3046 sub ValidateStatus {
3050 #Make sure the status passed in is valid
3051 unless ( $self->QueueObj->IsValidStatus($status) ) {
3063 =head2 SetStatus STATUS
3065 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3067 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.
3078 $args{Status} = shift;
3085 if ( $args{Status} eq 'deleted') {
3086 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3087 return ( 0, $self->loc('Permission Denied') );
3090 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3091 return ( 0, $self->loc('Permission Denied') );
3095 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3096 return (0, $self->loc('That ticket has unresolved dependencies'));
3099 my $now = RT::Date->new( $self->CurrentUser );
3102 #If we're changing the status from new, record that we've started
3103 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3105 #Set the Started time to "now"
3106 $self->_Set( Field => 'Started',
3108 RecordTransaction => 0 );
3111 #When we close a ticket, set the 'Resolved' attribute to now.
3112 # It's misnamed, but that's just historical.
3113 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3114 $self->_Set( Field => 'Resolved',
3116 RecordTransaction => 0 );
3119 #Actually update the status
3120 my ($val, $msg)= $self->_Set( Field => 'Status',
3121 Value => $args{Status},
3124 TransactionType => 'Status' );
3135 Takes no arguments. Marks this ticket for garbage collection
3141 return ( $self->SetStatus('deleted') );
3143 # TODO: garbage collection
3152 Sets this ticket's status to stalled
3158 return ( $self->SetStatus('stalled') );
3167 Sets this ticket's status to rejected
3173 return ( $self->SetStatus('rejected') );
3182 Sets this ticket\'s status to Open
3188 return ( $self->SetStatus('open') );
3197 Sets this ticket\'s status to Resolved
3203 return ( $self->SetStatus('resolved') );
3211 # {{{ Actions + Routines dealing with transactions
3213 # {{{ sub SetTold and _SetTold
3215 =head2 SetTold ISO [TIMETAKEN]
3217 Updates the told and records a transaction
3224 $told = shift if (@_);
3225 my $timetaken = shift || 0;
3227 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3228 return ( 0, $self->loc("Permission Denied") );
3231 my $datetold = new RT::Date( $self->CurrentUser );
3233 $datetold->Set( Format => 'iso',
3237 $datetold->SetToNow();
3240 return ( $self->_Set( Field => 'Told',
3241 Value => $datetold->ISO,
3242 TimeTaken => $timetaken,
3243 TransactionType => 'Told' ) );
3248 Updates the told without a transaction or acl check. Useful when we're sending replies.
3255 my $now = new RT::Date( $self->CurrentUser );
3258 #use __Set to get no ACLs ;)
3259 return ( $self->__Set( Field => 'Told',
3260 Value => $now->ISO ) );
3270 my $uid = $self->CurrentUser->id;
3271 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3272 return if $attr && $attr->Content gt $self->LastUpdated;
3274 my $txns = $self->Transactions;
3275 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3276 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3277 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3281 VALUE => $attr->Content
3283 $txns->RowsPerPage(1);
3284 return $txns->First;
3289 =head2 TransactionBatch
3291 Returns an array reference of all transactions created on this ticket during
3292 this ticket object's lifetime or since last application of a batch, or undef
3295 Only works when the C<UseTransactionBatch> config option is set to true.
3299 sub TransactionBatch {
3301 return $self->{_TransactionBatch};
3304 =head2 ApplyTransactionBatch
3306 Applies scrips on the current batch of transactions and shinks it. Usually
3307 batch is applied when object is destroyed, but in some cases it's too late.
3311 sub ApplyTransactionBatch {
3314 my $batch = $self->TransactionBatch;
3315 return unless $batch && @$batch;
3317 $self->_ApplyTransactionBatch;
3319 $self->{_TransactionBatch} = [];
3322 sub _ApplyTransactionBatch {
3324 my $batch = $self->TransactionBatch;
3327 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3330 RT::Scrips->new($RT::SystemUser)->Apply(
3331 Stage => 'TransactionBatch',
3333 TransactionObj => $batch->[0],
3337 # Entry point of the rule system
3338 my $rules = RT::Ruleset->FindAllRules(
3339 Stage => 'TransactionBatch',
3341 TransactionObj => $batch->[0],
3344 RT::Ruleset->CommitRules($rules);
3350 # DESTROY methods need to localize $@, or it may unset it. This
3351 # causes $m->abort to not bubble all of the way up. See perlbug
3352 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3355 # The following line eliminates reentrancy.
3356 # It protects against the fact that perl doesn't deal gracefully
3357 # when an object's refcount is changed in its destructor.
3358 return if $self->{_Destroyed}++;
3360 my $batch = $self->TransactionBatch;
3361 return unless $batch && @$batch;
3363 return $self->_ApplyTransactionBatch;
3368 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3370 # {{{ sub _OverlayAccessible
3372 sub _OverlayAccessible {
3374 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3375 Queue => { 'read' => 1, 'write' => 1 },
3376 Requestors => { 'read' => 1, 'write' => 1 },
3377 Owner => { 'read' => 1, 'write' => 1 },
3378 Subject => { 'read' => 1, 'write' => 1 },
3379 InitialPriority => { 'read' => 1, 'write' => 1 },
3380 FinalPriority => { 'read' => 1, 'write' => 1 },
3381 Priority => { 'read' => 1, 'write' => 1 },
3382 Status => { 'read' => 1, 'write' => 1 },
3383 TimeEstimated => { 'read' => 1, 'write' => 1 },
3384 TimeWorked => { 'read' => 1, 'write' => 1 },
3385 TimeLeft => { 'read' => 1, 'write' => 1 },
3386 Told => { 'read' => 1, 'write' => 1 },
3387 Resolved => { 'read' => 1 },
3388 Type => { 'read' => 1 },
3389 Starts => { 'read' => 1, 'write' => 1 },
3390 Started => { 'read' => 1, 'write' => 1 },
3391 Due => { 'read' => 1, 'write' => 1 },
3392 Creator => { 'read' => 1, 'auto' => 1 },
3393 Created => { 'read' => 1, 'auto' => 1 },
3394 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3395 LastUpdated => { 'read' => 1, 'auto' => 1 }
3407 my %args = ( Field => undef,
3410 RecordTransaction => 1,
3413 TransactionType => 'Set',
3416 if ($args{'CheckACL'}) {
3417 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3418 return ( 0, $self->loc("Permission Denied"));
3422 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3423 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3424 return(0, $self->loc("Internal Error"));
3427 #if the user is trying to modify the record
3429 #Take care of the old value we really don't want to get in an ACL loop.
3430 # so ask the super::_Value
3431 my $Old = $self->SUPER::_Value("$args{'Field'}");
3434 if ( $args{'UpdateTicket'} ) {
3437 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3438 Value => $args{'Value'} );
3440 #If we can't actually set the field to the value, don't record
3441 # a transaction. instead, get out of here.
3442 return ( 0, $msg ) unless $ret;
3445 if ( $args{'RecordTransaction'} == 1 ) {
3447 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3448 Type => $args{'TransactionType'},
3449 Field => $args{'Field'},
3450 NewValue => $args{'Value'},
3452 TimeTaken => $args{'TimeTaken'},
3454 return ( $Trans, scalar $TransObj->BriefDescription );
3457 return ( $ret, $msg );
3467 Takes the name of a table column.
3468 Returns its value as a string, if the user passes an ACL check
3477 #if the field is public, return it.
3478 if ( $self->_Accessible( $field, 'public' ) ) {
3480 #$RT::Logger->debug("Skipping ACL check for $field");
3481 return ( $self->SUPER::_Value($field) );
3485 #If the current user doesn't have ACLs, don't let em at it.
3487 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3490 return ( $self->SUPER::_Value($field) );
3496 # {{{ sub _UpdateTimeTaken
3498 =head2 _UpdateTimeTaken
3500 This routine will increment the timeworked counter. it should
3501 only be called from _NewTransaction
3505 sub _UpdateTimeTaken {
3507 my $Minutes = shift;
3510 $Total = $self->SUPER::_Value("TimeWorked");
3511 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3513 Field => "TimeWorked",
3524 # {{{ Routines dealing with ACCESS CONTROL
3526 # {{{ sub CurrentUserHasRight
3528 =head2 CurrentUserHasRight
3530 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3531 1 if the user has that right. It returns 0 if the user doesn't have that right.
3535 sub CurrentUserHasRight {
3539 return $self->CurrentUser->PrincipalObj->HasRight(
3551 Takes a paramhash with the attributes 'Right' and 'Principal'
3552 'Right' is a ticket-scoped textual right from RT::ACE
3553 'Principal' is an RT::User object
3555 Returns 1 if the principal has the right. Returns undef if not.
3567 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3569 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3570 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3575 $args{'Principal'}->HasRight(
3577 Right => $args{'Right'}
3588 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3589 It isn't acutally a searchbuilder collection itself.
3596 unless ($self->{'__reminders'}) {
3597 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3598 $self->{'__reminders'}->Ticket($self->id);
3600 return $self->{'__reminders'};
3606 # {{{ sub Transactions
3610 Returns an RT::Transactions object of all transactions on this ticket
3617 my $transactions = RT::Transactions->new( $self->CurrentUser );
3619 #If the user has no rights, return an empty object
3620 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3621 $transactions->LimitToTicket($self->id);
3623 # if the user may not see comments do not return them
3624 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3625 $transactions->Limit(
3631 $transactions->Limit(
3635 VALUE => "CommentEmailRecord",
3636 ENTRYAGGREGATOR => 'AND'
3641 $transactions->Limit(
3645 ENTRYAGGREGATOR => 'AND'
3649 return ($transactions);
3655 # {{{ TransactionCustomFields
3657 =head2 TransactionCustomFields
3659 Returns the custom fields that transactions on tickets will have.
3663 sub TransactionCustomFields {
3665 return $self->QueueObj->TicketTransactionCustomFields;
3670 # {{{ sub CustomFieldValues
3672 =head2 CustomFieldValues
3674 # Do name => id mapping (if needed) before falling back to
3675 # RT::Record's CustomFieldValues
3681 sub CustomFieldValues {
3685 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3687 my $cf = RT::CustomField->new( $self->CurrentUser );
3688 $cf->SetContextObject( $self );
3689 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3690 unless ( $cf->id ) {
3691 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3694 # If we didn't find a valid cfid, give up.
3695 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3697 return $self->SUPER::CustomFieldValues( $cf->id );
3702 # {{{ sub CustomFieldLookupType
3704 =head2 CustomFieldLookupType
3706 Returns the RT::Ticket lookup type, which can be passed to
3707 RT::CustomField->Create() via the 'LookupType' hash key.
3713 sub CustomFieldLookupType {
3714 "RT::Queue-RT::Ticket";
3717 =head2 ACLEquivalenceObjects
3719 This method returns a list of objects for which a user's rights also apply
3720 to this ticket. Generally, this is only the ticket's queue, but some RT
3721 extensions may make other objects available too.
3723 This method is called from L<RT::Principal/HasRight>.
3727 sub ACLEquivalenceObjects {
3729 return $self->QueueObj;
3738 Jesse Vincent, jesse@bestpractical.com