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 if ( $args{'_RecordTransaction'} ) {
726 # {{{ Add a transaction for the create
727 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
729 TimeTaken => $args{'TimeWorked'},
730 MIMEObj => $args{'MIMEObj'},
731 CommitScrips => !$args{'DryRun'},
734 if ( $self->Id && $Trans ) {
736 $TransObj->UpdateCustomFields(ARGSRef => \%args);
738 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
739 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
740 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
743 $RT::Handle->Rollback();
745 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
746 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
747 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
750 if ( $args{'DryRun'} ) {
751 $RT::Handle->Rollback();
752 return ($self->id, $TransObj, $ErrStr);
754 $RT::Handle->Commit();
755 return ( $self->Id, $TransObj->Id, $ErrStr );
761 # Not going to record a transaction
762 $RT::Handle->Commit();
763 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
764 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
765 return ( $self->Id, 0, $ErrStr );
773 # {{{ _Parse822HeadersForAttributes Content
775 =head2 _Parse822HeadersForAttributes Content
777 Takes an RFC822 style message and parses its attributes into a hash.
781 sub _Parse822HeadersForAttributes {
786 my @lines = ( split ( /\n/, $content ) );
787 while ( defined( my $line = shift @lines ) ) {
788 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
793 if ( defined( $args{$tag} ) )
794 { #if we're about to get a second value, make it an array
795 $args{$tag} = [ $args{$tag} ];
797 if ( ref( $args{$tag} ) )
798 { #If it's an array, we want to push the value
799 push @{ $args{$tag} }, $value;
801 else { #if there's nothing there, just set the value
802 $args{$tag} = $value;
804 } elsif ($line =~ /^$/) {
806 #TODO: this won't work, since "" isn't of the form "foo:value"
808 while ( defined( my $l = shift @lines ) ) {
809 push @{ $args{'content'} }, $l;
815 foreach my $date qw(due starts started resolved) {
816 my $dateobj = RT::Date->new($RT::SystemUser);
817 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
818 $dateobj->Set( Format => 'unix', Value => $args{$date} );
821 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
823 $args{$date} = $dateobj->ISO;
825 $args{'mimeobj'} = MIME::Entity->new();
826 $args{'mimeobj'}->build(
827 Type => ( $args{'contenttype'} || 'text/plain' ),
828 Data => ($args{'content'} || '')
838 =head2 Import PARAMHASH
841 Doesn\'t create a transaction.
842 Doesn\'t supply queue defaults, etc.
850 my ( $ErrStr, $QueueObj, $Owner );
854 EffectiveId => undef,
858 Owner => $RT::Nobody->Id,
859 Subject => '[no subject]',
860 InitialPriority => undef,
861 FinalPriority => undef,
872 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
873 $QueueObj = RT::Queue->new($RT::SystemUser);
874 $QueueObj->Load( $args{'Queue'} );
876 #TODO error check this and return 0 if it\'s not loading properly +++
878 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
879 $QueueObj = RT::Queue->new($RT::SystemUser);
880 $QueueObj->Load( $args{'Queue'}->Id );
884 "$self " . $args{'Queue'} . " not a recognised queue object." );
887 #Can't create a ticket without a queue.
888 unless ( defined($QueueObj) and $QueueObj->Id ) {
889 $RT::Logger->debug("$self No queue given for ticket creation.");
890 return ( 0, $self->loc('Could not create ticket. Queue not set') );
893 #Now that we have a queue, Check the ACLS
895 $self->CurrentUser->HasRight(
896 Right => 'CreateTicket',
902 $self->loc("No permission to create tickets in the queue '[_1]'"
906 # {{{ Deal with setting the owner
908 # Attempt to take user object, user name or user id.
909 # Assign to nobody if lookup fails.
910 if ( defined( $args{'Owner'} ) ) {
911 if ( ref( $args{'Owner'} ) ) {
912 $Owner = $args{'Owner'};
915 $Owner = new RT::User( $self->CurrentUser );
916 $Owner->Load( $args{'Owner'} );
917 if ( !defined( $Owner->id ) ) {
918 $Owner->Load( $RT::Nobody->id );
923 #If we have a proposed owner and they don't have the right
924 #to own a ticket, scream about it and make them not the owner
927 and ( $Owner->Id != $RT::Nobody->Id )
937 $RT::Logger->warning( "$self user "
941 . "as a ticket owner but has no rights to own "
943 . $QueueObj->Name . "'" );
948 #If we haven't been handed a valid owner, make it nobody.
949 unless ( defined($Owner) ) {
950 $Owner = new RT::User( $self->CurrentUser );
951 $Owner->Load( $RT::Nobody->UserObj->Id );
956 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
957 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
960 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
961 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
962 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
963 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
965 # If we're coming in with an id, set that now.
966 my $EffectiveId = undef;
968 $EffectiveId = $args{'id'};
972 my $id = $self->SUPER::Create(
974 EffectiveId => $EffectiveId,
975 Queue => $QueueObj->Id,
977 Subject => $args{'Subject'}, # loc
978 InitialPriority => $args{'InitialPriority'}, # loc
979 FinalPriority => $args{'FinalPriority'}, # loc
980 Priority => $args{'InitialPriority'}, # loc
981 Status => $args{'Status'}, # loc
982 TimeWorked => $args{'TimeWorked'}, # loc
983 Type => $args{'Type'}, # loc
984 Created => $args{'Created'}, # loc
985 Told => $args{'Told'}, # loc
986 LastUpdated => $args{'Updated'}, # loc
987 Resolved => $args{'Resolved'}, # loc
988 Due => $args{'Due'}, # loc
991 # If the ticket didn't have an id
992 # Set the ticket's effective ID now that we've created it.
994 $self->Load( $args{'id'} );
998 $self->__Set( Field => 'EffectiveId', Value => $id );
1002 $self . "->Import couldn't set EffectiveId: $msg" );
1006 my $create_groups_ret = $self->_CreateTicketGroups();
1007 unless ($create_groups_ret) {
1009 "Couldn't create ticket groups for ticket " . $self->Id );
1012 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1015 foreach $watcher ( @{ $args{'Cc'} } ) {
1016 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1018 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1019 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1022 foreach $watcher ( @{ $args{'Requestor'} } ) {
1023 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1027 return ( $self->Id, $ErrStr );
1032 # {{{ Routines dealing with watchers.
1034 # {{{ _CreateTicketGroups
1036 =head2 _CreateTicketGroups
1038 Create the ticket groups and links for this ticket.
1039 This routine expects to be called from Ticket->Create _inside of a transaction_
1041 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1043 It will return true on success and undef on failure.
1049 sub _CreateTicketGroups {
1052 my @types = qw(Requestor Owner Cc AdminCc);
1054 foreach my $type (@types) {
1055 my $type_obj = RT::Group->new($self->CurrentUser);
1056 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1057 Instance => $self->Id,
1060 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1061 $self->Id.": ".$msg);
1071 # {{{ sub OwnerGroup
1075 A constructor which returns an RT::Group object containing the owner of this ticket.
1081 my $owner_obj = RT::Group->new($self->CurrentUser);
1082 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1083 return ($owner_obj);
1089 # {{{ sub AddWatcher
1093 AddWatcher takes a parameter hash. The keys are as follows:
1095 Type One of Requestor, Cc, AdminCc
1097 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1099 Email The email address of the new watcher. If a user with this
1100 email address can't be found, a new nonprivileged user will be created.
1102 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.
1110 PrincipalId => undef,
1115 # ModifyTicket works in any case
1116 return $self->_AddWatcher( %args )
1117 if $self->CurrentUserHasRight('ModifyTicket');
1118 if ( $args{'Email'} ) {
1119 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1120 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1123 if ( lc $self->CurrentUser->UserObj->EmailAddress
1124 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1126 $args{'PrincipalId'} = $self->CurrentUser->id;
1127 delete $args{'Email'};
1131 # If the watcher isn't the current user then the current user has no right
1133 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1134 return ( 0, $self->loc("Permission Denied") );
1137 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1138 if ( $args{'Type'} eq 'AdminCc' ) {
1139 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1140 return ( 0, $self->loc('Permission Denied') );
1144 # If it's a Requestor or Cc and they don't have 'Watch', bail
1145 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1146 unless ( $self->CurrentUserHasRight('Watch') ) {
1147 return ( 0, $self->loc('Permission Denied') );
1151 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1152 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1155 return $self->_AddWatcher( %args );
1158 #This contains the meat of AddWatcher. but can be called from a routine like
1159 # Create, which doesn't need the additional acl check
1165 PrincipalId => undef,
1171 my $principal = RT::Principal->new($self->CurrentUser);
1172 if ($args{'Email'}) {
1173 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1174 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'})));
1176 my $user = RT::User->new($RT::SystemUser);
1177 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1178 $args{'PrincipalId'} = $pid if $pid;
1180 if ($args{'PrincipalId'}) {
1181 $principal->Load($args{'PrincipalId'});
1182 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1183 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'})))
1184 if RT::EmailParser->IsRTAddress( $email );
1190 # If we can't find this watcher, we need to bail.
1191 unless ($principal->Id) {
1192 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1193 return(0, $self->loc("Could not find or create that user"));
1197 my $group = RT::Group->new($self->CurrentUser);
1198 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1199 unless ($group->id) {
1200 return(0,$self->loc("Group not found"));
1203 if ( $group->HasMember( $principal)) {
1205 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1209 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1210 InsideTransaction => 1 );
1212 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1214 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1217 unless ( $args{'Silent'} ) {
1218 $self->_NewTransaction(
1219 Type => 'AddWatcher',
1220 NewValue => $principal->Id,
1221 Field => $args{'Type'}
1225 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1231 # {{{ sub DeleteWatcher
1233 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1236 Deletes a Ticket watcher. Takes two arguments:
1238 Type (one of Requestor,Cc,AdminCc)
1242 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1244 Email (the email address of an existing wathcer)
1253 my %args = ( Type => undef,
1254 PrincipalId => undef,
1258 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1259 return ( 0, $self->loc("No principal specified") );
1261 my $principal = RT::Principal->new( $self->CurrentUser );
1262 if ( $args{'PrincipalId'} ) {
1264 $principal->Load( $args{'PrincipalId'} );
1267 my $user = RT::User->new( $self->CurrentUser );
1268 $user->LoadByEmail( $args{'Email'} );
1269 $principal->Load( $user->Id );
1272 # If we can't find this watcher, we need to bail.
1273 unless ( $principal->Id ) {
1274 return ( 0, $self->loc("Could not find that principal") );
1277 my $group = RT::Group->new( $self->CurrentUser );
1278 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1279 unless ( $group->id ) {
1280 return ( 0, $self->loc("Group not found") );
1284 #If the watcher we're trying to add is for the current user
1285 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1287 # If it's an AdminCc and they don't have
1288 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1289 if ( $args{'Type'} eq 'AdminCc' ) {
1290 unless ( $self->CurrentUserHasRight('ModifyTicket')
1291 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1292 return ( 0, $self->loc('Permission Denied') );
1296 # If it's a Requestor or Cc and they don't have
1297 # 'Watch' or 'ModifyTicket', bail
1298 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1300 unless ( $self->CurrentUserHasRight('ModifyTicket')
1301 or $self->CurrentUserHasRight('Watch') ) {
1302 return ( 0, $self->loc('Permission Denied') );
1306 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1308 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1312 # If the watcher isn't the current user
1313 # and the current user doesn't have 'ModifyTicket' bail
1315 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1316 return ( 0, $self->loc("Permission Denied") );
1322 # see if this user is already a watcher.
1324 unless ( $group->HasMember($principal) ) {
1326 $self->loc( 'That principal is not a [_1] for this ticket',
1330 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1332 $RT::Logger->error( "Failed to delete "
1334 . " as a member of group "
1340 'Could not remove that principal as a [_1] for this ticket',
1344 unless ( $args{'Silent'} ) {
1345 $self->_NewTransaction( Type => 'DelWatcher',
1346 OldValue => $principal->Id,
1347 Field => $args{'Type'} );
1351 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1352 $principal->Object->Name,
1361 =head2 SquelchMailTo [EMAIL]
1363 Takes an optional email address to never email about updates to this ticket.
1366 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1374 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1378 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1383 return $self->_SquelchMailTo(@_);
1386 sub _SquelchMailTo {
1390 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1391 unless grep { $_->Content eq $attr }
1392 $self->Attributes->Named('SquelchMailTo');
1394 my @attributes = $self->Attributes->Named('SquelchMailTo');
1395 return (@attributes);
1399 =head2 UnsquelchMailTo ADDRESS
1401 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1403 Returns a tuple of (status, message)
1407 sub UnsquelchMailTo {
1410 my $address = shift;
1411 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1412 return ( 0, $self->loc("Permission Denied") );
1415 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1416 return ($val, $msg);
1420 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1422 =head2 RequestorAddresses
1424 B<Returns> String: All Ticket Requestor email addresses as a string.
1428 sub RequestorAddresses {
1431 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1435 return ( $self->Requestors->MemberEmailAddressesAsString );
1439 =head2 AdminCcAddresses
1441 returns String: All Ticket AdminCc email addresses as a string
1445 sub AdminCcAddresses {
1448 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1452 return ( $self->AdminCc->MemberEmailAddressesAsString )
1458 returns String: All Ticket Ccs as a string of email addresses
1465 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1468 return ( $self->Cc->MemberEmailAddressesAsString);
1474 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1476 # {{{ sub Requestors
1481 Returns this ticket's Requestors as an RT::Group object
1488 my $group = RT::Group->new($self->CurrentUser);
1489 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1490 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1498 # {{{ sub _Requestors
1502 Private non-ACLed variant of Reqeustors so that we can look them up for the
1503 purposes of customer auto-association during create.
1510 my $group = RT::Group->new($RT::SystemUser);
1511 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1522 Returns an RT::Group object which contains this ticket's Ccs.
1523 If the user doesn't have "ShowTicket" permission, returns an empty group
1530 my $group = RT::Group->new($self->CurrentUser);
1531 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1532 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1545 Returns an RT::Group object which contains this ticket's AdminCcs.
1546 If the user doesn't have "ShowTicket" permission, returns an empty group
1553 my $group = RT::Group->new($self->CurrentUser);
1554 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1555 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1565 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1568 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1570 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1572 Takes a param hash with the attributes Type and either PrincipalId or Email
1574 Type is one of Requestor, Cc, AdminCc and Owner
1576 PrincipalId is an RT::Principal id, and Email is an email address.
1578 Returns true if the specified principal (or the one corresponding to the
1579 specified address) is a member of the group Type for this ticket.
1581 XX TODO: This should be Memoized.
1588 my %args = ( Type => 'Requestor',
1589 PrincipalId => undef,
1594 # Load the relevant group.
1595 my $group = RT::Group->new($self->CurrentUser);
1596 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1598 # Find the relevant principal.
1599 if (!$args{PrincipalId} && $args{Email}) {
1600 # Look up the specified user.
1601 my $user = RT::User->new($self->CurrentUser);
1602 $user->LoadByEmail($args{Email});
1604 $args{PrincipalId} = $user->PrincipalId;
1607 # A non-existent user can't be a group member.
1612 # Ask if it has the member in question
1613 return $group->HasMember( $args{'PrincipalId'} );
1618 # {{{ sub IsRequestor
1620 =head2 IsRequestor PRINCIPAL_ID
1622 Takes an L<RT::Principal> id.
1624 Returns true if the principal is a requestor of the current ticket.
1632 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1640 =head2 IsCc PRINCIPAL_ID
1642 Takes an RT::Principal id.
1643 Returns true if the principal is a Cc of the current ticket.
1652 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1660 =head2 IsAdminCc PRINCIPAL_ID
1662 Takes an RT::Principal id.
1663 Returns true if the principal is an AdminCc of the current ticket.
1671 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1681 Takes an RT::User object. Returns true if that user is this ticket's owner.
1682 returns undef otherwise
1690 # no ACL check since this is used in acl decisions
1691 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1695 #Tickets won't yet have owners when they're being created.
1696 unless ( $self->OwnerObj->id ) {
1700 if ( $person->id == $self->OwnerObj->id ) {
1715 =head2 TransactionAddresses
1717 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1718 all this ticket's Create, Comment or Correspond transactions. The keys are
1719 stringified email addresses. Each value is an L<Email::Address> object.
1721 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.
1726 sub TransactionAddresses {
1728 my $txns = $self->Transactions;
1731 foreach my $type (qw(Create Comment Correspond)) {
1732 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1735 while (my $txn = $txns->Next) {
1736 my $txnaddrs = $txn->Addresses;
1737 foreach my $addrlist ( values %$txnaddrs ) {
1738 foreach my $addr (@$addrlist) {
1739 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1740 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1741 # skips "comment-only" addresses
1742 next unless ($addr->address);
1743 $addresses{$addr->address} = $addr;
1755 # {{{ Routines dealing with queues
1757 # {{{ sub ValidateQueue
1764 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1768 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1769 my $id = $QueueObj->Load($Value);
1785 my $NewQueue = shift;
1787 #Redundant. ACL gets checked in _Set;
1788 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1789 return ( 0, $self->loc("Permission Denied") );
1792 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1793 $NewQueueObj->Load($NewQueue);
1795 unless ( $NewQueueObj->Id() ) {
1796 return ( 0, $self->loc("That queue does not exist") );
1799 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1800 return ( 0, $self->loc('That is the same value') );
1803 $self->CurrentUser->HasRight(
1804 Right => 'CreateTicket',
1805 Object => $NewQueueObj
1809 return ( 0, $self->loc("You may not create requests in that queue.") );
1813 $self->OwnerObj->HasRight(
1814 Right => 'OwnTicket',
1815 Object => $NewQueueObj
1819 my $clone = RT::Ticket->new( $RT::SystemUser );
1820 $clone->Load( $self->Id );
1821 unless ( $clone->Id ) {
1822 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1824 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1825 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1828 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1831 # On queue change, change queue for reminders too
1832 my $reminder_collection = $self->Reminders->Collection;
1833 while ( my $reminder = $reminder_collection->Next ) {
1834 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1835 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1839 return ($status, $msg);
1848 Takes nothing. returns this ticket's queue object
1855 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1857 #We call __Value so that we can avoid the ACL decision and some deep recursion
1858 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1859 return ($queue_obj);
1866 # {{{ Date printing routines
1872 Returns an RT::Date object containing this ticket's due date
1879 my $time = new RT::Date( $self->CurrentUser );
1881 # -1 is RT::Date slang for never
1882 if ( my $due = $self->Due ) {
1883 $time->Set( Format => 'sql', Value => $due );
1886 $time->Set( Format => 'unix', Value => -1 );
1894 # {{{ sub DueAsString
1898 Returns this ticket's due date as a human readable string
1904 return $self->DueObj->AsString();
1909 # {{{ sub ResolvedObj
1913 Returns an RT::Date object of this ticket's 'resolved' time.
1920 my $time = new RT::Date( $self->CurrentUser );
1921 $time->Set( Format => 'sql', Value => $self->Resolved );
1927 # {{{ sub SetStarted
1931 Takes a date in ISO format or undef
1932 Returns a transaction id and a message
1933 The client calls "Start" to note that the project was started on the date in $date.
1934 A null date means "now"
1940 my $time = shift || 0;
1942 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1943 return ( 0, $self->loc("Permission Denied") );
1946 #We create a date object to catch date weirdness
1947 my $time_obj = new RT::Date( $self->CurrentUser() );
1949 $time_obj->Set( Format => 'ISO', Value => $time );
1952 $time_obj->SetToNow();
1955 #Now that we're starting, open this ticket
1956 #TODO do we really want to force this as policy? it should be a scrip
1958 #We need $TicketAsSystem, in case the current user doesn't have
1961 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1962 $TicketAsSystem->Load( $self->Id );
1963 if ( $TicketAsSystem->Status eq 'new' ) {
1964 $TicketAsSystem->Open();
1967 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1973 # {{{ sub StartedObj
1977 Returns an RT::Date object which contains this ticket's
1985 my $time = new RT::Date( $self->CurrentUser );
1986 $time->Set( Format => 'sql', Value => $self->Started );
1996 Returns an RT::Date object which contains this ticket's
2004 my $time = new RT::Date( $self->CurrentUser );
2005 $time->Set( Format => 'sql', Value => $self->Starts );
2015 Returns an RT::Date object which contains this ticket's
2023 my $time = new RT::Date( $self->CurrentUser );
2024 $time->Set( Format => 'sql', Value => $self->Told );
2030 # {{{ sub ToldAsString
2034 A convenience method that returns ToldObj->AsString
2036 TODO: This should be deprecated
2042 if ( $self->Told ) {
2043 return $self->ToldObj->AsString();
2052 # {{{ sub TimeWorkedAsString
2054 =head2 TimeWorkedAsString
2056 Returns the amount of time worked on this ticket as a Text String
2060 sub TimeWorkedAsString {
2062 my $value = $self->TimeWorked;
2064 # return the # of minutes worked turned into seconds and written as
2065 # a simple text string, this is not really a date object, but if we
2066 # diff a number of seconds vs the epoch, we'll get a nice description
2068 return "" unless $value;
2069 return RT::Date->new( $self->CurrentUser )
2070 ->DurationAsString( $value * 60 );
2075 # {{{ sub TimeLeftAsString
2077 =head2 TimeLeftAsString
2079 Returns the amount of time left on this ticket as a Text String
2083 sub TimeLeftAsString {
2085 my $value = $self->TimeLeft;
2086 return "" unless $value;
2087 return RT::Date->new( $self->CurrentUser )
2088 ->DurationAsString( $value * 60 );
2093 # {{{ Routines dealing with correspondence/comments
2099 Comment on this ticket.
2100 Takes a hash with the following attributes:
2101 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2104 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2106 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2107 They will, however, be prepared and you'll be able to access them through the TransactionObj
2109 Returns: Transaction id, Error Message, Transaction Object
2110 (note the different order from Create()!)
2117 my %args = ( CcMessageTo => undef,
2118 BccMessageTo => undef,
2125 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2126 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2127 return ( 0, $self->loc("Permission Denied"), undef );
2129 $args{'NoteType'} = 'Comment';
2131 if ($args{'DryRun'}) {
2132 $RT::Handle->BeginTransaction();
2133 $args{'CommitScrips'} = 0;
2136 my @results = $self->_RecordNote(%args);
2137 if ($args{'DryRun'}) {
2138 $RT::Handle->Rollback();
2145 # {{{ sub Correspond
2149 Correspond on this ticket.
2150 Takes a hashref with the following attributes:
2153 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2155 if there's no MIMEObj, Content is used to build a MIME::Entity object
2157 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2158 They will, however, be prepared and you'll be able to access them through the TransactionObj
2160 Returns: Transaction id, Error Message, Transaction Object
2161 (note the different order from Create()!)
2168 my %args = ( CcMessageTo => undef,
2169 BccMessageTo => undef,
2175 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2176 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2177 return ( 0, $self->loc("Permission Denied"), undef );
2180 $args{'NoteType'} = 'Correspond';
2181 if ($args{'DryRun'}) {
2182 $RT::Handle->BeginTransaction();
2183 $args{'CommitScrips'} = 0;
2186 my @results = $self->_RecordNote(%args);
2188 #Set the last told date to now if this isn't mail from the requestor.
2189 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2190 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2192 if ($args{'DryRun'}) {
2193 $RT::Handle->Rollback();
2202 # {{{ sub _RecordNote
2206 the meat of both comment and correspond.
2208 Performs no access control checks. hence, dangerous.
2215 CcMessageTo => undef,
2216 BccMessageTo => undef,
2221 NoteType => 'Correspond',
2227 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2228 return ( 0, $self->loc("No message attached"), undef );
2231 unless ( $args{'MIMEObj'} ) {
2232 $args{'MIMEObj'} = MIME::Entity->build(
2233 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2237 # convert text parts into utf-8
2238 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2240 # If we've been passed in CcMessageTo and BccMessageTo fields,
2241 # add them to the mime object for passing on to the transaction handler
2242 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2243 # RT-Send-Bcc: headers
2246 foreach my $type (qw/Cc Bcc/) {
2247 if ( defined $args{ $type . 'MessageTo' } ) {
2249 my $addresses = join ', ', (
2250 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2251 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2252 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2256 foreach my $argument (qw(Encrypt Sign)) {
2257 $args{'MIMEObj'}->head->add(
2258 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2259 ) if defined $args{ $argument };
2262 # If this is from an external source, we need to come up with its
2263 # internal Message-ID now, so all emails sent because of this
2264 # message have a common Message-ID
2265 my $org = RT->Config->Get('Organization');
2266 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2267 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2268 $args{'MIMEObj'}->head->set(
2269 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2273 #Record the correspondence (write the transaction)
2274 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2275 Type => $args{'NoteType'},
2276 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2277 TimeTaken => $args{'TimeTaken'},
2278 MIMEObj => $args{'MIMEObj'},
2279 CommitScrips => $args{'CommitScrips'},
2283 $RT::Logger->err("$self couldn't init a transaction $msg");
2284 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2287 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2299 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2302 my $type = shift || "";
2304 my $cache_key = "$field$type";
2305 return $self->{ $cache_key } if $self->{ $cache_key };
2307 my $links = $self->{ $cache_key }
2308 = RT::Links->new( $self->CurrentUser );
2309 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2310 $links->Limit( FIELD => 'id', VALUE => 0 );
2314 # Maybe this ticket is a merge ticket
2315 my $limit_on = 'Local'. $field;
2316 # at least to myself
2320 ENTRYAGGREGATOR => 'OR',
2325 ENTRYAGGREGATOR => 'OR',
2326 ) foreach $self->Merged;
2337 # {{{ sub DeleteLink
2341 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2342 SilentBase and SilentTarget. Either Base or Target must be null.
2343 The null value will be replaced with this ticket\'s id.
2345 If Silent is true then no transaction would be recorded, in other
2346 case you can control creation of transactions on both base and
2347 target with SilentBase and SilentTarget respectively. By default
2348 both transactions are created.
2359 SilentBase => undef,
2360 SilentTarget => undef,
2364 unless ( $args{'Target'} || $args{'Base'} ) {
2365 $RT::Logger->error("Base or Target must be specified");
2366 return ( 0, $self->loc('Either base or target must be specified') );
2371 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2372 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2373 return ( 0, $self->loc("Permission Denied") );
2376 # If the other URI is an RT::Ticket, we want to make sure the user
2377 # can modify it too...
2378 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2379 return (0, $msg) unless $status;
2380 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2383 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2384 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2386 return ( 0, $self->loc("Permission Denied") );
2389 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2390 return ( 0, $Msg ) unless $val;
2392 return ( $val, $Msg ) if $args{'Silent'};
2394 my ($direction, $remote_link);
2396 if ( $args{'Base'} ) {
2397 $remote_link = $args{'Base'};
2398 $direction = 'Target';
2400 elsif ( $args{'Target'} ) {
2401 $remote_link = $args{'Target'};
2402 $direction = 'Base';
2405 my $remote_uri = RT::URI->new( $self->CurrentUser );
2406 $remote_uri->FromURI( $remote_link );
2408 unless ( $args{ 'Silent'. $direction } ) {
2409 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2410 Type => 'DeleteLink',
2411 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2412 OldValue => $remote_uri->URI || $remote_link,
2415 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2418 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2419 my $OtherObj = $remote_uri->Object;
2420 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2421 Type => 'DeleteLink',
2422 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2423 : $LINKDIRMAP{$args{'Type'}}->{Target},
2424 OldValue => $self->URI,
2425 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2428 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2431 return ( $val, $Msg );
2440 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2442 If Silent is true then no transaction would be recorded, in other
2443 case you can control creation of transactions on both base and
2444 target with SilentBase and SilentTarget respectively. By default
2445 both transactions are created.
2451 my %args = ( Target => '',
2455 SilentBase => undef,
2456 SilentTarget => undef,
2459 unless ( $args{'Target'} || $args{'Base'} ) {
2460 $RT::Logger->error("Base or Target must be specified");
2461 return ( 0, $self->loc('Either base or target must be specified') );
2465 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2466 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2467 return ( 0, $self->loc("Permission Denied") );
2470 # If the other URI is an RT::Ticket, we want to make sure the user
2471 # can modify it too...
2472 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2473 return (0, $msg) unless $status;
2474 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2477 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2478 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2480 return ( 0, $self->loc("Permission Denied") );
2483 return $self->_AddLink(%args);
2486 sub __GetTicketFromURI {
2488 my %args = ( URI => '', @_ );
2490 # If the other URI is an RT::Ticket, we want to make sure the user
2491 # can modify it too...
2492 my $uri_obj = RT::URI->new( $self->CurrentUser );
2493 $uri_obj->FromURI( $args{'URI'} );
2495 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2496 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2497 $RT::Logger->warning( $msg );
2500 my $obj = $uri_obj->Resolver->Object;
2501 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2502 return (1, 'Found not a ticket', undef);
2504 return (1, 'Found ticket', $obj);
2509 Private non-acled variant of AddLink so that links can be added during create.
2515 my %args = ( Target => '',
2519 SilentBase => undef,
2520 SilentTarget => undef,
2523 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2524 return ($val, $msg) if !$val || $exist;
2525 return ($val, $msg) if $args{'Silent'};
2527 my ($direction, $remote_link);
2528 if ( $args{'Target'} ) {
2529 $remote_link = $args{'Target'};
2530 $direction = 'Base';
2531 } elsif ( $args{'Base'} ) {
2532 $remote_link = $args{'Base'};
2533 $direction = 'Target';
2536 my $remote_uri = RT::URI->new( $self->CurrentUser );
2537 $remote_uri->FromURI( $remote_link );
2539 unless ( $args{ 'Silent'. $direction } ) {
2540 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2542 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2543 NewValue => $remote_uri->URI || $remote_link,
2546 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2549 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2550 my $OtherObj = $remote_uri->Object;
2551 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2553 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2554 : $LINKDIRMAP{$args{'Type'}}->{Target},
2555 NewValue => $self->URI,
2556 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2559 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2562 return ( $val, $msg );
2572 MergeInto take the id of the ticket to merge this ticket into.
2578 my $ticket_id = shift;
2580 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2581 return ( 0, $self->loc("Permission Denied") );
2584 # Load up the new ticket.
2585 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2586 $MergeInto->Load($ticket_id);
2588 # make sure it exists.
2589 unless ( $MergeInto->Id ) {
2590 return ( 0, $self->loc("New ticket doesn't exist") );
2593 # Make sure the current user can modify the new ticket.
2594 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2595 return ( 0, $self->loc("Permission Denied") );
2598 delete $MERGE_CACHE{'effective'}{ $self->id };
2599 delete @{ $MERGE_CACHE{'merged'} }{
2600 $ticket_id, $MergeInto->id, $self->id
2603 $RT::Handle->BeginTransaction();
2605 # We use EffectiveId here even though it duplicates information from
2606 # the links table becasue of the massive performance hit we'd take
2607 # by trying to do a separate database query for merge info everytime
2610 #update this ticket's effective id to the new ticket's id.
2611 my ( $id_val, $id_msg ) = $self->__Set(
2612 Field => 'EffectiveId',
2613 Value => $MergeInto->Id()
2617 $RT::Handle->Rollback();
2618 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2622 if ( $self->__Value('Status') ne 'resolved' ) {
2624 my ( $status_val, $status_msg )
2625 = $self->__Set( Field => 'Status', Value => 'resolved' );
2627 unless ($status_val) {
2628 $RT::Handle->Rollback();
2631 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2635 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2639 # update all the links that point to that old ticket
2640 my $old_links_to = RT::Links->new($self->CurrentUser);
2641 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2644 while (my $link = $old_links_to->Next) {
2645 if (exists $old_seen{$link->Base."-".$link->Type}) {
2648 elsif ($link->Base eq $MergeInto->URI) {
2651 # First, make sure the link doesn't already exist. then move it over.
2652 my $tmp = RT::Link->new($RT::SystemUser);
2653 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2657 $link->SetTarget($MergeInto->URI);
2658 $link->SetLocalTarget($MergeInto->id);
2660 $old_seen{$link->Base."-".$link->Type} =1;
2665 my $old_links_from = RT::Links->new($self->CurrentUser);
2666 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2668 while (my $link = $old_links_from->Next) {
2669 if (exists $old_seen{$link->Type."-".$link->Target}) {
2672 if ($link->Target eq $MergeInto->URI) {
2675 # First, make sure the link doesn't already exist. then move it over.
2676 my $tmp = RT::Link->new($RT::SystemUser);
2677 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2681 $link->SetBase($MergeInto->URI);
2682 $link->SetLocalBase($MergeInto->id);
2683 $old_seen{$link->Type."-".$link->Target} =1;
2689 # Update time fields
2690 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2692 my $mutator = "Set$type";
2693 $MergeInto->$mutator(
2694 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2697 #add all of this ticket's watchers to that ticket.
2698 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2700 my $people = $self->$watcher_type->MembersObj;
2701 my $addwatcher_type = $watcher_type;
2702 $addwatcher_type =~ s/s$//;
2704 while ( my $watcher = $people->Next ) {
2706 my ($val, $msg) = $MergeInto->_AddWatcher(
2707 Type => $addwatcher_type,
2709 PrincipalId => $watcher->MemberId
2712 $RT::Logger->warning($msg);
2718 #find all of the tickets that were merged into this ticket.
2719 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2720 $old_mergees->Limit(
2721 FIELD => 'EffectiveId',
2726 # update their EffectiveId fields to the new ticket's id
2727 while ( my $ticket = $old_mergees->Next() ) {
2728 my ( $val, $msg ) = $ticket->__Set(
2729 Field => 'EffectiveId',
2730 Value => $MergeInto->Id()
2734 #make a new link: this ticket is merged into that other ticket.
2735 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2737 $MergeInto->_SetLastUpdated;
2739 $RT::Handle->Commit();
2740 return ( 1, $self->loc("Merge Successful") );
2745 Returns list of tickets' ids that's been merged into this ticket.
2753 return @{ $MERGE_CACHE{'merged'}{ $id } }
2754 if $MERGE_CACHE{'merged'}{ $id };
2756 my $mergees = RT::Tickets->new( $self->CurrentUser );
2758 FIELD => 'EffectiveId',
2766 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2767 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2774 # {{{ Routines dealing with ownership
2780 Takes nothing and returns an RT::User object of
2788 #If this gets ACLed, we lose on a rights check in User.pm and
2789 #get deep recursion. if we need ACLs here, we need
2790 #an equiv without ACLs
2792 my $owner = new RT::User( $self->CurrentUser );
2793 $owner->Load( $self->__Value('Owner') );
2795 #Return the owner object
2801 # {{{ sub OwnerAsString
2803 =head2 OwnerAsString
2805 Returns the owner's email address
2811 return ( $self->OwnerObj->EmailAddress );
2821 Takes two arguments:
2822 the Id or Name of the owner
2823 and (optionally) the type of the SetOwner Transaction. It defaults
2824 to 'Give'. 'Steal' is also a valid option.
2831 my $NewOwner = shift;
2832 my $Type = shift || "Give";
2834 $RT::Handle->BeginTransaction();
2836 $self->_SetLastUpdated(); # lock the ticket
2837 $self->Load( $self->id ); # in case $self changed while waiting for lock
2839 my $OldOwnerObj = $self->OwnerObj;
2841 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2842 $NewOwnerObj->Load( $NewOwner );
2843 unless ( $NewOwnerObj->Id ) {
2844 $RT::Handle->Rollback();
2845 return ( 0, $self->loc("That user does not exist") );
2849 # must have ModifyTicket rights
2850 # or TakeTicket/StealTicket and $NewOwner is self
2851 # see if it's a take
2852 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2853 unless ( $self->CurrentUserHasRight('ModifyTicket')
2854 || $self->CurrentUserHasRight('TakeTicket') ) {
2855 $RT::Handle->Rollback();
2856 return ( 0, $self->loc("Permission Denied") );
2860 # see if it's a steal
2861 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2862 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2864 unless ( $self->CurrentUserHasRight('ModifyTicket')
2865 || $self->CurrentUserHasRight('StealTicket') ) {
2866 $RT::Handle->Rollback();
2867 return ( 0, $self->loc("Permission Denied") );
2871 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2872 $RT::Handle->Rollback();
2873 return ( 0, $self->loc("Permission Denied") );
2877 # If we're not stealing and the ticket has an owner and it's not
2879 if ( $Type ne 'Steal' and $Type ne 'Force'
2880 and $OldOwnerObj->Id != $RT::Nobody->Id
2881 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2883 $RT::Handle->Rollback();
2884 return ( 0, $self->loc("You can only take tickets that are unowned") )
2885 if $NewOwnerObj->id == $self->CurrentUser->id;
2888 $self->loc("You can only reassign tickets that you own or that are unowned" )
2892 #If we've specified a new owner and that user can't modify the ticket
2893 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2894 $RT::Handle->Rollback();
2895 return ( 0, $self->loc("That user may not own tickets in that queue") );
2898 # If the ticket has an owner and it's the new owner, we don't need
2900 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2901 $RT::Handle->Rollback();
2902 return ( 0, $self->loc("That user already owns that ticket") );
2905 # Delete the owner in the owner group, then add a new one
2906 # TODO: is this safe? it's not how we really want the API to work
2907 # for most things, but it's fast.
2908 my ( $del_id, $del_msg );
2909 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2910 ($del_id, $del_msg) = $owner->Delete();
2911 last unless ($del_id);
2915 $RT::Handle->Rollback();
2916 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2919 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2920 PrincipalId => $NewOwnerObj->PrincipalId,
2921 InsideTransaction => 1 );
2923 $RT::Handle->Rollback();
2924 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2927 # We call set twice with slightly different arguments, so
2928 # as to not have an SQL transaction span two RT transactions
2930 my ( $val, $msg ) = $self->_Set(
2932 RecordTransaction => 0,
2933 Value => $NewOwnerObj->Id,
2935 TransactionType => $Type,
2936 CheckACL => 0, # don't check acl
2940 $RT::Handle->Rollback;
2941 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2944 ($val, $msg) = $self->_NewTransaction(
2947 NewValue => $NewOwnerObj->Id,
2948 OldValue => $OldOwnerObj->Id,
2953 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2954 $OldOwnerObj->Name, $NewOwnerObj->Name );
2957 $RT::Handle->Rollback();
2961 $RT::Handle->Commit();
2963 return ( $val, $msg );
2972 A convenince method to set the ticket's owner to the current user
2978 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2987 Convenience method to set the owner to 'nobody' if the current user is the owner.
2993 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3002 A convenience method to change the owner of the current ticket to the
3003 current user. Even if it's owned by another user.
3010 if ( $self->IsOwner( $self->CurrentUser ) ) {
3011 return ( 0, $self->loc("You already own this ticket") );
3014 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3024 # {{{ Routines dealing with status
3026 # {{{ sub ValidateStatus
3028 =head2 ValidateStatus STATUS
3030 Takes a string. Returns true if that status is a valid status for this ticket.
3031 Returns false otherwise.
3035 sub ValidateStatus {
3039 #Make sure the status passed in is valid
3040 unless ( $self->QueueObj->IsValidStatus($status) ) {
3052 =head2 SetStatus STATUS
3054 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3056 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.
3067 $args{Status} = shift;
3074 if ( $args{Status} eq 'deleted') {
3075 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3076 return ( 0, $self->loc('Permission Denied') );
3079 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3080 return ( 0, $self->loc('Permission Denied') );
3084 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3085 return (0, $self->loc('That ticket has unresolved dependencies'));
3088 my $now = RT::Date->new( $self->CurrentUser );
3091 #If we're changing the status from new, record that we've started
3092 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3094 #Set the Started time to "now"
3095 $self->_Set( Field => 'Started',
3097 RecordTransaction => 0 );
3100 #When we close a ticket, set the 'Resolved' attribute to now.
3101 # It's misnamed, but that's just historical.
3102 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3103 $self->_Set( Field => 'Resolved',
3105 RecordTransaction => 0 );
3108 #Actually update the status
3109 my ($val, $msg)= $self->_Set( Field => 'Status',
3110 Value => $args{Status},
3113 TransactionType => 'Status' );
3124 Takes no arguments. Marks this ticket for garbage collection
3130 return ( $self->SetStatus('deleted') );
3132 # TODO: garbage collection
3141 Sets this ticket's status to stalled
3147 return ( $self->SetStatus('stalled') );
3156 Sets this ticket's status to rejected
3162 return ( $self->SetStatus('rejected') );
3171 Sets this ticket\'s status to Open
3177 return ( $self->SetStatus('open') );
3186 Sets this ticket\'s status to Resolved
3192 return ( $self->SetStatus('resolved') );
3200 # {{{ Actions + Routines dealing with transactions
3202 # {{{ sub SetTold and _SetTold
3204 =head2 SetTold ISO [TIMETAKEN]
3206 Updates the told and records a transaction
3213 $told = shift if (@_);
3214 my $timetaken = shift || 0;
3216 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3217 return ( 0, $self->loc("Permission Denied") );
3220 my $datetold = new RT::Date( $self->CurrentUser );
3222 $datetold->Set( Format => 'iso',
3226 $datetold->SetToNow();
3229 return ( $self->_Set( Field => 'Told',
3230 Value => $datetold->ISO,
3231 TimeTaken => $timetaken,
3232 TransactionType => 'Told' ) );
3237 Updates the told without a transaction or acl check. Useful when we're sending replies.
3244 my $now = new RT::Date( $self->CurrentUser );
3247 #use __Set to get no ACLs ;)
3248 return ( $self->__Set( Field => 'Told',
3249 Value => $now->ISO ) );
3259 my $uid = $self->CurrentUser->id;
3260 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3261 return if $attr && $attr->Content gt $self->LastUpdated;
3263 my $txns = $self->Transactions;
3264 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3265 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3266 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3270 VALUE => $attr->Content
3272 $txns->RowsPerPage(1);
3273 return $txns->First;
3278 =head2 TransactionBatch
3280 Returns an array reference of all transactions created on this ticket during
3281 this ticket object's lifetime or since last application of a batch, or undef
3284 Only works when the C<UseTransactionBatch> config option is set to true.
3288 sub TransactionBatch {
3290 return $self->{_TransactionBatch};
3293 =head2 ApplyTransactionBatch
3295 Applies scrips on the current batch of transactions and shinks it. Usually
3296 batch is applied when object is destroyed, but in some cases it's too late.
3300 sub ApplyTransactionBatch {
3303 my $batch = $self->TransactionBatch;
3304 return unless $batch && @$batch;
3306 $self->_ApplyTransactionBatch;
3308 $self->{_TransactionBatch} = [];
3311 sub _ApplyTransactionBatch {
3313 my $batch = $self->TransactionBatch;
3316 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3319 RT::Scrips->new($RT::SystemUser)->Apply(
3320 Stage => 'TransactionBatch',
3322 TransactionObj => $batch->[0],
3326 # Entry point of the rule system
3327 my $rules = RT::Ruleset->FindAllRules(
3328 Stage => 'TransactionBatch',
3330 TransactionObj => $batch->[0],
3333 RT::Ruleset->CommitRules($rules);
3339 # DESTROY methods need to localize $@, or it may unset it. This
3340 # causes $m->abort to not bubble all of the way up. See perlbug
3341 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3344 # The following line eliminates reentrancy.
3345 # It protects against the fact that perl doesn't deal gracefully
3346 # when an object's refcount is changed in its destructor.
3347 return if $self->{_Destroyed}++;
3349 my $batch = $self->TransactionBatch;
3350 return unless $batch && @$batch;
3352 return $self->_ApplyTransactionBatch;
3357 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3359 # {{{ sub _OverlayAccessible
3361 sub _OverlayAccessible {
3363 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3364 Queue => { 'read' => 1, 'write' => 1 },
3365 Requestors => { 'read' => 1, 'write' => 1 },
3366 Owner => { 'read' => 1, 'write' => 1 },
3367 Subject => { 'read' => 1, 'write' => 1 },
3368 InitialPriority => { 'read' => 1, 'write' => 1 },
3369 FinalPriority => { 'read' => 1, 'write' => 1 },
3370 Priority => { 'read' => 1, 'write' => 1 },
3371 Status => { 'read' => 1, 'write' => 1 },
3372 TimeEstimated => { 'read' => 1, 'write' => 1 },
3373 TimeWorked => { 'read' => 1, 'write' => 1 },
3374 TimeLeft => { 'read' => 1, 'write' => 1 },
3375 Told => { 'read' => 1, 'write' => 1 },
3376 Resolved => { 'read' => 1 },
3377 Type => { 'read' => 1 },
3378 Starts => { 'read' => 1, 'write' => 1 },
3379 Started => { 'read' => 1, 'write' => 1 },
3380 Due => { 'read' => 1, 'write' => 1 },
3381 Creator => { 'read' => 1, 'auto' => 1 },
3382 Created => { 'read' => 1, 'auto' => 1 },
3383 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3384 LastUpdated => { 'read' => 1, 'auto' => 1 }
3396 my %args = ( Field => undef,
3399 RecordTransaction => 1,
3402 TransactionType => 'Set',
3405 if ($args{'CheckACL'}) {
3406 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3407 return ( 0, $self->loc("Permission Denied"));
3411 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3412 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3413 return(0, $self->loc("Internal Error"));
3416 #if the user is trying to modify the record
3418 #Take care of the old value we really don't want to get in an ACL loop.
3419 # so ask the super::_Value
3420 my $Old = $self->SUPER::_Value("$args{'Field'}");
3423 if ( $args{'UpdateTicket'} ) {
3426 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3427 Value => $args{'Value'} );
3429 #If we can't actually set the field to the value, don't record
3430 # a transaction. instead, get out of here.
3431 return ( 0, $msg ) unless $ret;
3434 if ( $args{'RecordTransaction'} == 1 ) {
3436 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3437 Type => $args{'TransactionType'},
3438 Field => $args{'Field'},
3439 NewValue => $args{'Value'},
3441 TimeTaken => $args{'TimeTaken'},
3443 return ( $Trans, scalar $TransObj->BriefDescription );
3446 return ( $ret, $msg );
3456 Takes the name of a table column.
3457 Returns its value as a string, if the user passes an ACL check
3466 #if the field is public, return it.
3467 if ( $self->_Accessible( $field, 'public' ) ) {
3469 #$RT::Logger->debug("Skipping ACL check for $field");
3470 return ( $self->SUPER::_Value($field) );
3474 #If the current user doesn't have ACLs, don't let em at it.
3476 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3479 return ( $self->SUPER::_Value($field) );
3485 # {{{ sub _UpdateTimeTaken
3487 =head2 _UpdateTimeTaken
3489 This routine will increment the timeworked counter. it should
3490 only be called from _NewTransaction
3494 sub _UpdateTimeTaken {
3496 my $Minutes = shift;
3499 $Total = $self->SUPER::_Value("TimeWorked");
3500 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3502 Field => "TimeWorked",
3513 # {{{ Routines dealing with ACCESS CONTROL
3515 # {{{ sub CurrentUserHasRight
3517 =head2 CurrentUserHasRight
3519 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3520 1 if the user has that right. It returns 0 if the user doesn't have that right.
3524 sub CurrentUserHasRight {
3528 return $self->CurrentUser->PrincipalObj->HasRight(
3540 Takes a paramhash with the attributes 'Right' and 'Principal'
3541 'Right' is a ticket-scoped textual right from RT::ACE
3542 'Principal' is an RT::User object
3544 Returns 1 if the principal has the right. Returns undef if not.
3556 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3558 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3559 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3564 $args{'Principal'}->HasRight(
3566 Right => $args{'Right'}
3577 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3578 It isn't acutally a searchbuilder collection itself.
3585 unless ($self->{'__reminders'}) {
3586 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3587 $self->{'__reminders'}->Ticket($self->id);
3589 return $self->{'__reminders'};
3595 # {{{ sub Transactions
3599 Returns an RT::Transactions object of all transactions on this ticket
3606 my $transactions = RT::Transactions->new( $self->CurrentUser );
3608 #If the user has no rights, return an empty object
3609 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3610 $transactions->LimitToTicket($self->id);
3612 # if the user may not see comments do not return them
3613 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3614 $transactions->Limit(
3620 $transactions->Limit(
3624 VALUE => "CommentEmailRecord",
3625 ENTRYAGGREGATOR => 'AND'
3630 $transactions->Limit(
3634 ENTRYAGGREGATOR => 'AND'
3638 return ($transactions);
3644 # {{{ TransactionCustomFields
3646 =head2 TransactionCustomFields
3648 Returns the custom fields that transactions on tickets will have.
3652 sub TransactionCustomFields {
3654 return $self->QueueObj->TicketTransactionCustomFields;
3659 # {{{ sub CustomFieldValues
3661 =head2 CustomFieldValues
3663 # Do name => id mapping (if needed) before falling back to
3664 # RT::Record's CustomFieldValues
3670 sub CustomFieldValues {
3674 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3676 my $cf = RT::CustomField->new( $self->CurrentUser );
3677 $cf->SetContextObject( $self );
3678 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3679 unless ( $cf->id ) {
3680 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3683 # If we didn't find a valid cfid, give up.
3684 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3686 return $self->SUPER::CustomFieldValues( $cf->id );
3691 # {{{ sub CustomFieldLookupType
3693 =head2 CustomFieldLookupType
3695 Returns the RT::Ticket lookup type, which can be passed to
3696 RT::CustomField->Create() via the 'LookupType' hash key.
3702 sub CustomFieldLookupType {
3703 "RT::Queue-RT::Ticket";
3706 =head2 ACLEquivalenceObjects
3708 This method returns a list of objects for which a user's rights also apply
3709 to this ticket. Generally, this is only the ticket's queue, but some RT
3710 extensions may make other objects available too.
3712 This method is called from L<RT::Principal/HasRight>.
3716 sub ACLEquivalenceObjects {
3718 return $self->QueueObj;
3727 Jesse Vincent, jesse@bestpractical.com