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 my ( $wval, $wmsg ) = $self->_AddLink(
622 Type => $LINKTYPEMAP{$type}->{'Type'},
623 $LINKTYPEMAP{$type}->{'Mode'} => $link,
624 Silent => !$args{'_RecordTransaction'},
625 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
629 push @non_fatal_errors, $wmsg unless ($wval);
635 # {{{ Deal with auto-customer association
637 #unless we already have (a) customer(s)...
638 unless ( $self->Customers->Count ) {
640 #first find any requestors with emails but *without* customer targets
641 my @NoCust_Requestors =
642 grep { $_->EmailAddress && ! $_->Customers->Count }
643 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
645 for my $Requestor (@NoCust_Requestors) {
647 #perhaps the stuff in here should be in a User method??
649 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
651 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
653 ## false laziness w/RT/Interface/Web_Vendor.pm
654 my @link = ( 'Type' => 'MemberOf',
655 'Target' => "freeside://freeside/cust_main/$custnum",
658 my( $val, $msg ) = $Requestor->_AddLink(@link);
659 #XXX should do something with $msg# push @non_fatal_errors, $msg;
665 #find any requestors with customer targets
667 my %cust_target = ();
670 grep { $_->Customers->Count }
671 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
673 foreach my $Requestor ( @Requestors ) {
674 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
675 $cust_target{ $cust_link->Target } = 1;
679 #and then auto-associate this ticket with those customers
681 foreach my $cust_target ( keys %cust_target ) {
683 my @link = ( 'Type' => 'MemberOf',
684 #'Target' => "freeside://freeside/cust_main/$custnum",
685 'Target' => $cust_target,
688 my( $val, $msg ) = $self->_AddLink(@link);
689 push @non_fatal_errors, $msg;
697 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
698 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
700 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
702 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
703 . ") was proposed as a ticket owner but has no rights to own "
704 . "tickets in " . $QueueObj->Name );
705 push @non_fatal_errors, $self->loc(
706 "Owner '[_1]' does not have rights to own this ticket.",
710 $Owner = $DeferOwner;
711 $self->__Set(Field => 'Owner', Value => $Owner->id);
713 $self->OwnerGroup->_AddMember(
714 PrincipalId => $Owner->PrincipalId,
715 InsideTransaction => 1
719 if ( $args{'_RecordTransaction'} ) {
721 # {{{ Add a transaction for the create
722 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
724 TimeTaken => $args{'TimeWorked'},
725 MIMEObj => $args{'MIMEObj'},
726 CommitScrips => !$args{'DryRun'},
729 if ( $self->Id && $Trans ) {
731 $TransObj->UpdateCustomFields(ARGSRef => \%args);
733 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
734 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
735 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
738 $RT::Handle->Rollback();
740 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
741 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
742 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
745 if ( $args{'DryRun'} ) {
746 $RT::Handle->Rollback();
747 return ($self->id, $TransObj, $ErrStr);
749 $RT::Handle->Commit();
750 return ( $self->Id, $TransObj->Id, $ErrStr );
756 # Not going to record a transaction
757 $RT::Handle->Commit();
758 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
759 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
760 return ( $self->Id, 0, $ErrStr );
768 # {{{ _Parse822HeadersForAttributes Content
770 =head2 _Parse822HeadersForAttributes Content
772 Takes an RFC822 style message and parses its attributes into a hash.
776 sub _Parse822HeadersForAttributes {
781 my @lines = ( split ( /\n/, $content ) );
782 while ( defined( my $line = shift @lines ) ) {
783 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
788 if ( defined( $args{$tag} ) )
789 { #if we're about to get a second value, make it an array
790 $args{$tag} = [ $args{$tag} ];
792 if ( ref( $args{$tag} ) )
793 { #If it's an array, we want to push the value
794 push @{ $args{$tag} }, $value;
796 else { #if there's nothing there, just set the value
797 $args{$tag} = $value;
799 } elsif ($line =~ /^$/) {
801 #TODO: this won't work, since "" isn't of the form "foo:value"
803 while ( defined( my $l = shift @lines ) ) {
804 push @{ $args{'content'} }, $l;
810 foreach my $date qw(due starts started resolved) {
811 my $dateobj = RT::Date->new($RT::SystemUser);
812 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
813 $dateobj->Set( Format => 'unix', Value => $args{$date} );
816 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
818 $args{$date} = $dateobj->ISO;
820 $args{'mimeobj'} = MIME::Entity->new();
821 $args{'mimeobj'}->build(
822 Type => ( $args{'contenttype'} || 'text/plain' ),
823 Data => ($args{'content'} || '')
833 =head2 Import PARAMHASH
836 Doesn\'t create a transaction.
837 Doesn\'t supply queue defaults, etc.
845 my ( $ErrStr, $QueueObj, $Owner );
849 EffectiveId => undef,
853 Owner => $RT::Nobody->Id,
854 Subject => '[no subject]',
855 InitialPriority => undef,
856 FinalPriority => undef,
867 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
868 $QueueObj = RT::Queue->new($RT::SystemUser);
869 $QueueObj->Load( $args{'Queue'} );
871 #TODO error check this and return 0 if it\'s not loading properly +++
873 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
874 $QueueObj = RT::Queue->new($RT::SystemUser);
875 $QueueObj->Load( $args{'Queue'}->Id );
879 "$self " . $args{'Queue'} . " not a recognised queue object." );
882 #Can't create a ticket without a queue.
883 unless ( defined($QueueObj) and $QueueObj->Id ) {
884 $RT::Logger->debug("$self No queue given for ticket creation.");
885 return ( 0, $self->loc('Could not create ticket. Queue not set') );
888 #Now that we have a queue, Check the ACLS
890 $self->CurrentUser->HasRight(
891 Right => 'CreateTicket',
897 $self->loc("No permission to create tickets in the queue '[_1]'"
901 # {{{ Deal with setting the owner
903 # Attempt to take user object, user name or user id.
904 # Assign to nobody if lookup fails.
905 if ( defined( $args{'Owner'} ) ) {
906 if ( ref( $args{'Owner'} ) ) {
907 $Owner = $args{'Owner'};
910 $Owner = new RT::User( $self->CurrentUser );
911 $Owner->Load( $args{'Owner'} );
912 if ( !defined( $Owner->id ) ) {
913 $Owner->Load( $RT::Nobody->id );
918 #If we have a proposed owner and they don't have the right
919 #to own a ticket, scream about it and make them not the owner
922 and ( $Owner->Id != $RT::Nobody->Id )
932 $RT::Logger->warning( "$self user "
936 . "as a ticket owner but has no rights to own "
938 . $QueueObj->Name . "'" );
943 #If we haven't been handed a valid owner, make it nobody.
944 unless ( defined($Owner) ) {
945 $Owner = new RT::User( $self->CurrentUser );
946 $Owner->Load( $RT::Nobody->UserObj->Id );
951 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
952 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
955 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
956 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
957 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
958 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
960 # If we're coming in with an id, set that now.
961 my $EffectiveId = undef;
963 $EffectiveId = $args{'id'};
967 my $id = $self->SUPER::Create(
969 EffectiveId => $EffectiveId,
970 Queue => $QueueObj->Id,
972 Subject => $args{'Subject'}, # loc
973 InitialPriority => $args{'InitialPriority'}, # loc
974 FinalPriority => $args{'FinalPriority'}, # loc
975 Priority => $args{'InitialPriority'}, # loc
976 Status => $args{'Status'}, # loc
977 TimeWorked => $args{'TimeWorked'}, # loc
978 Type => $args{'Type'}, # loc
979 Created => $args{'Created'}, # loc
980 Told => $args{'Told'}, # loc
981 LastUpdated => $args{'Updated'}, # loc
982 Resolved => $args{'Resolved'}, # loc
983 Due => $args{'Due'}, # loc
986 # If the ticket didn't have an id
987 # Set the ticket's effective ID now that we've created it.
989 $self->Load( $args{'id'} );
993 $self->__Set( Field => 'EffectiveId', Value => $id );
997 $self . "->Import couldn't set EffectiveId: $msg" );
1001 my $create_groups_ret = $self->_CreateTicketGroups();
1002 unless ($create_groups_ret) {
1004 "Couldn't create ticket groups for ticket " . $self->Id );
1007 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1010 foreach $watcher ( @{ $args{'Cc'} } ) {
1011 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1013 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1014 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1017 foreach $watcher ( @{ $args{'Requestor'} } ) {
1018 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1022 return ( $self->Id, $ErrStr );
1027 # {{{ Routines dealing with watchers.
1029 # {{{ _CreateTicketGroups
1031 =head2 _CreateTicketGroups
1033 Create the ticket groups and links for this ticket.
1034 This routine expects to be called from Ticket->Create _inside of a transaction_
1036 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1038 It will return true on success and undef on failure.
1044 sub _CreateTicketGroups {
1047 my @types = qw(Requestor Owner Cc AdminCc);
1049 foreach my $type (@types) {
1050 my $type_obj = RT::Group->new($self->CurrentUser);
1051 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1052 Instance => $self->Id,
1055 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1056 $self->Id.": ".$msg);
1066 # {{{ sub OwnerGroup
1070 A constructor which returns an RT::Group object containing the owner of this ticket.
1076 my $owner_obj = RT::Group->new($self->CurrentUser);
1077 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1078 return ($owner_obj);
1084 # {{{ sub AddWatcher
1088 AddWatcher takes a parameter hash. The keys are as follows:
1090 Type One of Requestor, Cc, AdminCc
1092 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1094 Email The email address of the new watcher. If a user with this
1095 email address can't be found, a new nonprivileged user will be created.
1097 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.
1105 PrincipalId => undef,
1110 # ModifyTicket works in any case
1111 return $self->_AddWatcher( %args )
1112 if $self->CurrentUserHasRight('ModifyTicket');
1113 if ( $args{'Email'} ) {
1114 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1115 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1118 if ( lc $self->CurrentUser->UserObj->EmailAddress
1119 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1121 $args{'PrincipalId'} = $self->CurrentUser->id;
1122 delete $args{'Email'};
1126 # If the watcher isn't the current user then the current user has no right
1128 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1129 return ( 0, $self->loc("Permission Denied") );
1132 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1133 if ( $args{'Type'} eq 'AdminCc' ) {
1134 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1135 return ( 0, $self->loc('Permission Denied') );
1139 # If it's a Requestor or Cc and they don't have 'Watch', bail
1140 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1141 unless ( $self->CurrentUserHasRight('Watch') ) {
1142 return ( 0, $self->loc('Permission Denied') );
1146 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1147 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1150 return $self->_AddWatcher( %args );
1153 #This contains the meat of AddWatcher. but can be called from a routine like
1154 # Create, which doesn't need the additional acl check
1160 PrincipalId => undef,
1166 my $principal = RT::Principal->new($self->CurrentUser);
1167 if ($args{'Email'}) {
1168 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1169 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'})));
1171 my $user = RT::User->new($RT::SystemUser);
1172 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1173 $args{'PrincipalId'} = $pid if $pid;
1175 if ($args{'PrincipalId'}) {
1176 $principal->Load($args{'PrincipalId'});
1177 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1178 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'})))
1179 if RT::EmailParser->IsRTAddress( $email );
1185 # If we can't find this watcher, we need to bail.
1186 unless ($principal->Id) {
1187 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1188 return(0, $self->loc("Could not find or create that user"));
1192 my $group = RT::Group->new($self->CurrentUser);
1193 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1194 unless ($group->id) {
1195 return(0,$self->loc("Group not found"));
1198 if ( $group->HasMember( $principal)) {
1200 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1204 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1205 InsideTransaction => 1 );
1207 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1209 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1212 unless ( $args{'Silent'} ) {
1213 $self->_NewTransaction(
1214 Type => 'AddWatcher',
1215 NewValue => $principal->Id,
1216 Field => $args{'Type'}
1220 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1226 # {{{ sub DeleteWatcher
1228 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1231 Deletes a Ticket watcher. Takes two arguments:
1233 Type (one of Requestor,Cc,AdminCc)
1237 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1239 Email (the email address of an existing wathcer)
1248 my %args = ( Type => undef,
1249 PrincipalId => undef,
1253 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1254 return ( 0, $self->loc("No principal specified") );
1256 my $principal = RT::Principal->new( $self->CurrentUser );
1257 if ( $args{'PrincipalId'} ) {
1259 $principal->Load( $args{'PrincipalId'} );
1262 my $user = RT::User->new( $self->CurrentUser );
1263 $user->LoadByEmail( $args{'Email'} );
1264 $principal->Load( $user->Id );
1267 # If we can't find this watcher, we need to bail.
1268 unless ( $principal->Id ) {
1269 return ( 0, $self->loc("Could not find that principal") );
1272 my $group = RT::Group->new( $self->CurrentUser );
1273 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1274 unless ( $group->id ) {
1275 return ( 0, $self->loc("Group not found") );
1279 #If the watcher we're trying to add is for the current user
1280 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1282 # If it's an AdminCc and they don't have
1283 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1284 if ( $args{'Type'} eq 'AdminCc' ) {
1285 unless ( $self->CurrentUserHasRight('ModifyTicket')
1286 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1287 return ( 0, $self->loc('Permission Denied') );
1291 # If it's a Requestor or Cc and they don't have
1292 # 'Watch' or 'ModifyTicket', bail
1293 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1295 unless ( $self->CurrentUserHasRight('ModifyTicket')
1296 or $self->CurrentUserHasRight('Watch') ) {
1297 return ( 0, $self->loc('Permission Denied') );
1301 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1303 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1307 # If the watcher isn't the current user
1308 # and the current user doesn't have 'ModifyTicket' bail
1310 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1311 return ( 0, $self->loc("Permission Denied") );
1317 # see if this user is already a watcher.
1319 unless ( $group->HasMember($principal) ) {
1321 $self->loc( 'That principal is not a [_1] for this ticket',
1325 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1327 $RT::Logger->error( "Failed to delete "
1329 . " as a member of group "
1335 'Could not remove that principal as a [_1] for this ticket',
1339 unless ( $args{'Silent'} ) {
1340 $self->_NewTransaction( Type => 'DelWatcher',
1341 OldValue => $principal->Id,
1342 Field => $args{'Type'} );
1346 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1347 $principal->Object->Name,
1356 =head2 SquelchMailTo [EMAIL]
1358 Takes an optional email address to never email about updates to this ticket.
1361 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1369 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1373 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1378 return $self->_SquelchMailTo(@_);
1381 sub _SquelchMailTo {
1385 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1386 unless grep { $_->Content eq $attr }
1387 $self->Attributes->Named('SquelchMailTo');
1389 my @attributes = $self->Attributes->Named('SquelchMailTo');
1390 return (@attributes);
1394 =head2 UnsquelchMailTo ADDRESS
1396 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1398 Returns a tuple of (status, message)
1402 sub UnsquelchMailTo {
1405 my $address = shift;
1406 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1407 return ( 0, $self->loc("Permission Denied") );
1410 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1411 return ($val, $msg);
1415 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1417 =head2 RequestorAddresses
1419 B<Returns> String: All Ticket Requestor email addresses as a string.
1423 sub RequestorAddresses {
1426 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1430 return ( $self->Requestors->MemberEmailAddressesAsString );
1434 =head2 AdminCcAddresses
1436 returns String: All Ticket AdminCc email addresses as a string
1440 sub AdminCcAddresses {
1443 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1447 return ( $self->AdminCc->MemberEmailAddressesAsString )
1453 returns String: All Ticket Ccs as a string of email addresses
1460 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1463 return ( $self->Cc->MemberEmailAddressesAsString);
1469 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1471 # {{{ sub Requestors
1476 Returns this ticket's Requestors as an RT::Group object
1483 my $group = RT::Group->new($self->CurrentUser);
1484 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1485 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1493 # {{{ sub _Requestors
1497 Private non-ACLed variant of Reqeustors so that we can look them up for the
1498 purposes of customer auto-association during create.
1505 my $group = RT::Group->new($RT::SystemUser);
1506 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1517 Returns an RT::Group object which contains this ticket's Ccs.
1518 If the user doesn't have "ShowTicket" permission, returns an empty group
1525 my $group = RT::Group->new($self->CurrentUser);
1526 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1527 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1540 Returns an RT::Group object which contains this ticket's AdminCcs.
1541 If the user doesn't have "ShowTicket" permission, returns an empty group
1548 my $group = RT::Group->new($self->CurrentUser);
1549 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1550 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1560 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1563 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1565 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1567 Takes a param hash with the attributes Type and either PrincipalId or Email
1569 Type is one of Requestor, Cc, AdminCc and Owner
1571 PrincipalId is an RT::Principal id, and Email is an email address.
1573 Returns true if the specified principal (or the one corresponding to the
1574 specified address) is a member of the group Type for this ticket.
1576 XX TODO: This should be Memoized.
1583 my %args = ( Type => 'Requestor',
1584 PrincipalId => undef,
1589 # Load the relevant group.
1590 my $group = RT::Group->new($self->CurrentUser);
1591 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1593 # Find the relevant principal.
1594 if (!$args{PrincipalId} && $args{Email}) {
1595 # Look up the specified user.
1596 my $user = RT::User->new($self->CurrentUser);
1597 $user->LoadByEmail($args{Email});
1599 $args{PrincipalId} = $user->PrincipalId;
1602 # A non-existent user can't be a group member.
1607 # Ask if it has the member in question
1608 return $group->HasMember( $args{'PrincipalId'} );
1613 # {{{ sub IsRequestor
1615 =head2 IsRequestor PRINCIPAL_ID
1617 Takes an L<RT::Principal> id.
1619 Returns true if the principal is a requestor of the current ticket.
1627 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1635 =head2 IsCc PRINCIPAL_ID
1637 Takes an RT::Principal id.
1638 Returns true if the principal is a Cc of the current ticket.
1647 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1655 =head2 IsAdminCc PRINCIPAL_ID
1657 Takes an RT::Principal id.
1658 Returns true if the principal is an AdminCc of the current ticket.
1666 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1676 Takes an RT::User object. Returns true if that user is this ticket's owner.
1677 returns undef otherwise
1685 # no ACL check since this is used in acl decisions
1686 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1690 #Tickets won't yet have owners when they're being created.
1691 unless ( $self->OwnerObj->id ) {
1695 if ( $person->id == $self->OwnerObj->id ) {
1710 =head2 TransactionAddresses
1712 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1713 all this ticket's Create, Comment or Correspond transactions. The keys are
1714 stringified email addresses. Each value is an L<Email::Address> object.
1716 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.
1721 sub TransactionAddresses {
1723 my $txns = $self->Transactions;
1726 foreach my $type (qw(Create Comment Correspond)) {
1727 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1730 while (my $txn = $txns->Next) {
1731 my $txnaddrs = $txn->Addresses;
1732 foreach my $addrlist ( values %$txnaddrs ) {
1733 foreach my $addr (@$addrlist) {
1734 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1735 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1736 # skips "comment-only" addresses
1737 next unless ($addr->address);
1738 $addresses{$addr->address} = $addr;
1750 # {{{ Routines dealing with queues
1752 # {{{ sub ValidateQueue
1759 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1763 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1764 my $id = $QueueObj->Load($Value);
1780 my $NewQueue = shift;
1782 #Redundant. ACL gets checked in _Set;
1783 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1784 return ( 0, $self->loc("Permission Denied") );
1787 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1788 $NewQueueObj->Load($NewQueue);
1790 unless ( $NewQueueObj->Id() ) {
1791 return ( 0, $self->loc("That queue does not exist") );
1794 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1795 return ( 0, $self->loc('That is the same value') );
1798 $self->CurrentUser->HasRight(
1799 Right => 'CreateTicket',
1800 Object => $NewQueueObj
1804 return ( 0, $self->loc("You may not create requests in that queue.") );
1808 $self->OwnerObj->HasRight(
1809 Right => 'OwnTicket',
1810 Object => $NewQueueObj
1814 my $clone = RT::Ticket->new( $RT::SystemUser );
1815 $clone->Load( $self->Id );
1816 unless ( $clone->Id ) {
1817 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1819 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1820 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1823 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1826 # On queue change, change queue for reminders too
1827 my $reminder_collection = $self->Reminders->Collection;
1828 while ( my $reminder = $reminder_collection->Next ) {
1829 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1830 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1834 return ($status, $msg);
1843 Takes nothing. returns this ticket's queue object
1850 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1852 #We call __Value so that we can avoid the ACL decision and some deep recursion
1853 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1854 return ($queue_obj);
1861 # {{{ Date printing routines
1867 Returns an RT::Date object containing this ticket's due date
1874 my $time = new RT::Date( $self->CurrentUser );
1876 # -1 is RT::Date slang for never
1877 if ( my $due = $self->Due ) {
1878 $time->Set( Format => 'sql', Value => $due );
1881 $time->Set( Format => 'unix', Value => -1 );
1889 # {{{ sub DueAsString
1893 Returns this ticket's due date as a human readable string
1899 return $self->DueObj->AsString();
1904 # {{{ sub ResolvedObj
1908 Returns an RT::Date object of this ticket's 'resolved' time.
1915 my $time = new RT::Date( $self->CurrentUser );
1916 $time->Set( Format => 'sql', Value => $self->Resolved );
1922 # {{{ sub SetStarted
1926 Takes a date in ISO format or undef
1927 Returns a transaction id and a message
1928 The client calls "Start" to note that the project was started on the date in $date.
1929 A null date means "now"
1935 my $time = shift || 0;
1937 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1938 return ( 0, $self->loc("Permission Denied") );
1941 #We create a date object to catch date weirdness
1942 my $time_obj = new RT::Date( $self->CurrentUser() );
1944 $time_obj->Set( Format => 'ISO', Value => $time );
1947 $time_obj->SetToNow();
1950 #Now that we're starting, open this ticket
1951 #TODO do we really want to force this as policy? it should be a scrip
1953 #We need $TicketAsSystem, in case the current user doesn't have
1956 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1957 $TicketAsSystem->Load( $self->Id );
1958 if ( $TicketAsSystem->Status eq 'new' ) {
1959 $TicketAsSystem->Open();
1962 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1968 # {{{ sub StartedObj
1972 Returns an RT::Date object which contains this ticket's
1980 my $time = new RT::Date( $self->CurrentUser );
1981 $time->Set( Format => 'sql', Value => $self->Started );
1991 Returns an RT::Date object which contains this ticket's
1999 my $time = new RT::Date( $self->CurrentUser );
2000 $time->Set( Format => 'sql', Value => $self->Starts );
2010 Returns an RT::Date object which contains this ticket's
2018 my $time = new RT::Date( $self->CurrentUser );
2019 $time->Set( Format => 'sql', Value => $self->Told );
2025 # {{{ sub ToldAsString
2029 A convenience method that returns ToldObj->AsString
2031 TODO: This should be deprecated
2037 if ( $self->Told ) {
2038 return $self->ToldObj->AsString();
2047 # {{{ sub TimeWorkedAsString
2049 =head2 TimeWorkedAsString
2051 Returns the amount of time worked on this ticket as a Text String
2055 sub TimeWorkedAsString {
2057 my $value = $self->TimeWorked;
2059 # return the # of minutes worked turned into seconds and written as
2060 # a simple text string, this is not really a date object, but if we
2061 # diff a number of seconds vs the epoch, we'll get a nice description
2063 return "" unless $value;
2064 return RT::Date->new( $self->CurrentUser )
2065 ->DurationAsString( $value * 60 );
2070 # {{{ sub TimeLeftAsString
2072 =head2 TimeLeftAsString
2074 Returns the amount of time left on this ticket as a Text String
2078 sub TimeLeftAsString {
2080 my $value = $self->TimeLeft;
2081 return "" unless $value;
2082 return RT::Date->new( $self->CurrentUser )
2083 ->DurationAsString( $value * 60 );
2088 # {{{ Routines dealing with correspondence/comments
2094 Comment on this ticket.
2095 Takes a hash with the following attributes:
2096 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2099 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2101 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2102 They will, however, be prepared and you'll be able to access them through the TransactionObj
2104 Returns: Transaction id, Error Message, Transaction Object
2105 (note the different order from Create()!)
2112 my %args = ( CcMessageTo => undef,
2113 BccMessageTo => undef,
2120 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2121 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2122 return ( 0, $self->loc("Permission Denied"), undef );
2124 $args{'NoteType'} = 'Comment';
2126 if ($args{'DryRun'}) {
2127 $RT::Handle->BeginTransaction();
2128 $args{'CommitScrips'} = 0;
2131 my @results = $self->_RecordNote(%args);
2132 if ($args{'DryRun'}) {
2133 $RT::Handle->Rollback();
2140 # {{{ sub Correspond
2144 Correspond on this ticket.
2145 Takes a hashref with the following attributes:
2148 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2150 if there's no MIMEObj, Content is used to build a MIME::Entity object
2152 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2153 They will, however, be prepared and you'll be able to access them through the TransactionObj
2155 Returns: Transaction id, Error Message, Transaction Object
2156 (note the different order from Create()!)
2163 my %args = ( CcMessageTo => undef,
2164 BccMessageTo => undef,
2170 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2171 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2172 return ( 0, $self->loc("Permission Denied"), undef );
2175 $args{'NoteType'} = 'Correspond';
2176 if ($args{'DryRun'}) {
2177 $RT::Handle->BeginTransaction();
2178 $args{'CommitScrips'} = 0;
2181 my @results = $self->_RecordNote(%args);
2183 #Set the last told date to now if this isn't mail from the requestor.
2184 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2185 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2187 if ($args{'DryRun'}) {
2188 $RT::Handle->Rollback();
2197 # {{{ sub _RecordNote
2201 the meat of both comment and correspond.
2203 Performs no access control checks. hence, dangerous.
2210 CcMessageTo => undef,
2211 BccMessageTo => undef,
2216 NoteType => 'Correspond',
2222 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2223 return ( 0, $self->loc("No message attached"), undef );
2226 unless ( $args{'MIMEObj'} ) {
2227 $args{'MIMEObj'} = MIME::Entity->build(
2228 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2232 # convert text parts into utf-8
2233 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2235 # If we've been passed in CcMessageTo and BccMessageTo fields,
2236 # add them to the mime object for passing on to the transaction handler
2237 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2238 # RT-Send-Bcc: headers
2241 foreach my $type (qw/Cc Bcc/) {
2242 if ( defined $args{ $type . 'MessageTo' } ) {
2244 my $addresses = join ', ', (
2245 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2246 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2247 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2251 foreach my $argument (qw(Encrypt Sign)) {
2252 $args{'MIMEObj'}->head->add(
2253 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2254 ) if defined $args{ $argument };
2257 # If this is from an external source, we need to come up with its
2258 # internal Message-ID now, so all emails sent because of this
2259 # message have a common Message-ID
2260 my $org = RT->Config->Get('Organization');
2261 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2262 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2263 $args{'MIMEObj'}->head->set(
2264 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2268 #Record the correspondence (write the transaction)
2269 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2270 Type => $args{'NoteType'},
2271 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2272 TimeTaken => $args{'TimeTaken'},
2273 MIMEObj => $args{'MIMEObj'},
2274 CommitScrips => $args{'CommitScrips'},
2278 $RT::Logger->err("$self couldn't init a transaction $msg");
2279 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2282 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2294 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2297 my $type = shift || "";
2299 my $cache_key = "$field$type";
2300 return $self->{ $cache_key } if $self->{ $cache_key };
2302 my $links = $self->{ $cache_key }
2303 = RT::Links->new( $self->CurrentUser );
2304 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2305 $links->Limit( FIELD => 'id', VALUE => 0 );
2309 # Maybe this ticket is a merge ticket
2310 my $limit_on = 'Local'. $field;
2311 # at least to myself
2315 ENTRYAGGREGATOR => 'OR',
2320 ENTRYAGGREGATOR => 'OR',
2321 ) foreach $self->Merged;
2332 # {{{ sub DeleteLink
2336 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2337 SilentBase and SilentTarget. Either Base or Target must be null.
2338 The null value will be replaced with this ticket\'s id.
2340 If Silent is true then no transaction would be recorded, in other
2341 case you can control creation of transactions on both base and
2342 target with SilentBase and SilentTarget respectively. By default
2343 both transactions are created.
2354 SilentBase => undef,
2355 SilentTarget => undef,
2359 unless ( $args{'Target'} || $args{'Base'} ) {
2360 $RT::Logger->error("Base or Target must be specified");
2361 return ( 0, $self->loc('Either base or target must be specified') );
2366 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2367 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2368 return ( 0, $self->loc("Permission Denied") );
2371 # If the other URI is an RT::Ticket, we want to make sure the user
2372 # can modify it too...
2373 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2374 return (0, $msg) unless $status;
2375 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2378 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2379 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2381 return ( 0, $self->loc("Permission Denied") );
2384 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2385 return ( 0, $Msg ) unless $val;
2387 return ( $val, $Msg ) if $args{'Silent'};
2389 my ($direction, $remote_link);
2391 if ( $args{'Base'} ) {
2392 $remote_link = $args{'Base'};
2393 $direction = 'Target';
2395 elsif ( $args{'Target'} ) {
2396 $remote_link = $args{'Target'};
2397 $direction = 'Base';
2400 my $remote_uri = RT::URI->new( $self->CurrentUser );
2401 $remote_uri->FromURI( $remote_link );
2403 unless ( $args{ 'Silent'. $direction } ) {
2404 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2405 Type => 'DeleteLink',
2406 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2407 OldValue => $remote_uri->URI || $remote_link,
2410 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2413 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2414 my $OtherObj = $remote_uri->Object;
2415 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2416 Type => 'DeleteLink',
2417 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2418 : $LINKDIRMAP{$args{'Type'}}->{Target},
2419 OldValue => $self->URI,
2420 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2423 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2426 return ( $val, $Msg );
2435 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2437 If Silent is true then no transaction would be recorded, in other
2438 case you can control creation of transactions on both base and
2439 target with SilentBase and SilentTarget respectively. By default
2440 both transactions are created.
2446 my %args = ( Target => '',
2450 SilentBase => undef,
2451 SilentTarget => undef,
2454 unless ( $args{'Target'} || $args{'Base'} ) {
2455 $RT::Logger->error("Base or Target must be specified");
2456 return ( 0, $self->loc('Either base or target must be specified') );
2460 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2461 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2462 return ( 0, $self->loc("Permission Denied") );
2465 # If the other URI is an RT::Ticket, we want to make sure the user
2466 # can modify it too...
2467 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2468 return (0, $msg) unless $status;
2469 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2472 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2473 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2475 return ( 0, $self->loc("Permission Denied") );
2478 return $self->_AddLink(%args);
2481 sub __GetTicketFromURI {
2483 my %args = ( URI => '', @_ );
2485 # If the other URI is an RT::Ticket, we want to make sure the user
2486 # can modify it too...
2487 my $uri_obj = RT::URI->new( $self->CurrentUser );
2488 $uri_obj->FromURI( $args{'URI'} );
2490 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2491 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2492 $RT::Logger->warning( $msg );
2495 my $obj = $uri_obj->Resolver->Object;
2496 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2497 return (1, 'Found not a ticket', undef);
2499 return (1, 'Found ticket', $obj);
2504 Private non-acled variant of AddLink so that links can be added during create.
2510 my %args = ( Target => '',
2514 SilentBase => undef,
2515 SilentTarget => undef,
2518 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2519 return ($val, $msg) if !$val || $exist;
2520 return ($val, $msg) if $args{'Silent'};
2522 my ($direction, $remote_link);
2523 if ( $args{'Target'} ) {
2524 $remote_link = $args{'Target'};
2525 $direction = 'Base';
2526 } elsif ( $args{'Base'} ) {
2527 $remote_link = $args{'Base'};
2528 $direction = 'Target';
2531 my $remote_uri = RT::URI->new( $self->CurrentUser );
2532 $remote_uri->FromURI( $remote_link );
2534 unless ( $args{ 'Silent'. $direction } ) {
2535 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2537 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2538 NewValue => $remote_uri->URI || $remote_link,
2541 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2544 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2545 my $OtherObj = $remote_uri->Object;
2546 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2548 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2549 : $LINKDIRMAP{$args{'Type'}}->{Target},
2550 NewValue => $self->URI,
2551 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2554 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2557 return ( $val, $msg );
2567 MergeInto take the id of the ticket to merge this ticket into.
2573 my $ticket_id = shift;
2575 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2576 return ( 0, $self->loc("Permission Denied") );
2579 # Load up the new ticket.
2580 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2581 $MergeInto->Load($ticket_id);
2583 # make sure it exists.
2584 unless ( $MergeInto->Id ) {
2585 return ( 0, $self->loc("New ticket doesn't exist") );
2588 # Make sure the current user can modify the new ticket.
2589 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2590 return ( 0, $self->loc("Permission Denied") );
2593 delete $MERGE_CACHE{'effective'}{ $self->id };
2594 delete @{ $MERGE_CACHE{'merged'} }{
2595 $ticket_id, $MergeInto->id, $self->id
2598 $RT::Handle->BeginTransaction();
2600 # We use EffectiveId here even though it duplicates information from
2601 # the links table becasue of the massive performance hit we'd take
2602 # by trying to do a separate database query for merge info everytime
2605 #update this ticket's effective id to the new ticket's id.
2606 my ( $id_val, $id_msg ) = $self->__Set(
2607 Field => 'EffectiveId',
2608 Value => $MergeInto->Id()
2612 $RT::Handle->Rollback();
2613 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2617 if ( $self->__Value('Status') ne 'resolved' ) {
2619 my ( $status_val, $status_msg )
2620 = $self->__Set( Field => 'Status', Value => 'resolved' );
2622 unless ($status_val) {
2623 $RT::Handle->Rollback();
2626 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2630 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2634 # update all the links that point to that old ticket
2635 my $old_links_to = RT::Links->new($self->CurrentUser);
2636 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2639 while (my $link = $old_links_to->Next) {
2640 if (exists $old_seen{$link->Base."-".$link->Type}) {
2643 elsif ($link->Base eq $MergeInto->URI) {
2646 # First, make sure the link doesn't already exist. then move it over.
2647 my $tmp = RT::Link->new($RT::SystemUser);
2648 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2652 $link->SetTarget($MergeInto->URI);
2653 $link->SetLocalTarget($MergeInto->id);
2655 $old_seen{$link->Base."-".$link->Type} =1;
2660 my $old_links_from = RT::Links->new($self->CurrentUser);
2661 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2663 while (my $link = $old_links_from->Next) {
2664 if (exists $old_seen{$link->Type."-".$link->Target}) {
2667 if ($link->Target eq $MergeInto->URI) {
2670 # First, make sure the link doesn't already exist. then move it over.
2671 my $tmp = RT::Link->new($RT::SystemUser);
2672 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2676 $link->SetBase($MergeInto->URI);
2677 $link->SetLocalBase($MergeInto->id);
2678 $old_seen{$link->Type."-".$link->Target} =1;
2684 # Update time fields
2685 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2687 my $mutator = "Set$type";
2688 $MergeInto->$mutator(
2689 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2692 #add all of this ticket's watchers to that ticket.
2693 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2695 my $people = $self->$watcher_type->MembersObj;
2696 my $addwatcher_type = $watcher_type;
2697 $addwatcher_type =~ s/s$//;
2699 while ( my $watcher = $people->Next ) {
2701 my ($val, $msg) = $MergeInto->_AddWatcher(
2702 Type => $addwatcher_type,
2704 PrincipalId => $watcher->MemberId
2707 $RT::Logger->warning($msg);
2713 #find all of the tickets that were merged into this ticket.
2714 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2715 $old_mergees->Limit(
2716 FIELD => 'EffectiveId',
2721 # update their EffectiveId fields to the new ticket's id
2722 while ( my $ticket = $old_mergees->Next() ) {
2723 my ( $val, $msg ) = $ticket->__Set(
2724 Field => 'EffectiveId',
2725 Value => $MergeInto->Id()
2729 #make a new link: this ticket is merged into that other ticket.
2730 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2732 $MergeInto->_SetLastUpdated;
2734 $RT::Handle->Commit();
2735 return ( 1, $self->loc("Merge Successful") );
2740 Returns list of tickets' ids that's been merged into this ticket.
2748 return @{ $MERGE_CACHE{'merged'}{ $id } }
2749 if $MERGE_CACHE{'merged'}{ $id };
2751 my $mergees = RT::Tickets->new( $self->CurrentUser );
2753 FIELD => 'EffectiveId',
2761 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2762 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2769 # {{{ Routines dealing with ownership
2775 Takes nothing and returns an RT::User object of
2783 #If this gets ACLed, we lose on a rights check in User.pm and
2784 #get deep recursion. if we need ACLs here, we need
2785 #an equiv without ACLs
2787 my $owner = new RT::User( $self->CurrentUser );
2788 $owner->Load( $self->__Value('Owner') );
2790 #Return the owner object
2796 # {{{ sub OwnerAsString
2798 =head2 OwnerAsString
2800 Returns the owner's email address
2806 return ( $self->OwnerObj->EmailAddress );
2816 Takes two arguments:
2817 the Id or Name of the owner
2818 and (optionally) the type of the SetOwner Transaction. It defaults
2819 to 'Give'. 'Steal' is also a valid option.
2826 my $NewOwner = shift;
2827 my $Type = shift || "Give";
2829 $RT::Handle->BeginTransaction();
2831 $self->_SetLastUpdated(); # lock the ticket
2832 $self->Load( $self->id ); # in case $self changed while waiting for lock
2834 my $OldOwnerObj = $self->OwnerObj;
2836 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2837 $NewOwnerObj->Load( $NewOwner );
2838 unless ( $NewOwnerObj->Id ) {
2839 $RT::Handle->Rollback();
2840 return ( 0, $self->loc("That user does not exist") );
2844 # must have ModifyTicket rights
2845 # or TakeTicket/StealTicket and $NewOwner is self
2846 # see if it's a take
2847 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2848 unless ( $self->CurrentUserHasRight('ModifyTicket')
2849 || $self->CurrentUserHasRight('TakeTicket') ) {
2850 $RT::Handle->Rollback();
2851 return ( 0, $self->loc("Permission Denied") );
2855 # see if it's a steal
2856 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2857 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2859 unless ( $self->CurrentUserHasRight('ModifyTicket')
2860 || $self->CurrentUserHasRight('StealTicket') ) {
2861 $RT::Handle->Rollback();
2862 return ( 0, $self->loc("Permission Denied") );
2866 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2867 $RT::Handle->Rollback();
2868 return ( 0, $self->loc("Permission Denied") );
2872 # If we're not stealing and the ticket has an owner and it's not
2874 if ( $Type ne 'Steal' and $Type ne 'Force'
2875 and $OldOwnerObj->Id != $RT::Nobody->Id
2876 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2878 $RT::Handle->Rollback();
2879 return ( 0, $self->loc("You can only take tickets that are unowned") )
2880 if $NewOwnerObj->id == $self->CurrentUser->id;
2883 $self->loc("You can only reassign tickets that you own or that are unowned" )
2887 #If we've specified a new owner and that user can't modify the ticket
2888 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2889 $RT::Handle->Rollback();
2890 return ( 0, $self->loc("That user may not own tickets in that queue") );
2893 # If the ticket has an owner and it's the new owner, we don't need
2895 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2896 $RT::Handle->Rollback();
2897 return ( 0, $self->loc("That user already owns that ticket") );
2900 # Delete the owner in the owner group, then add a new one
2901 # TODO: is this safe? it's not how we really want the API to work
2902 # for most things, but it's fast.
2903 my ( $del_id, $del_msg );
2904 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2905 ($del_id, $del_msg) = $owner->Delete();
2906 last unless ($del_id);
2910 $RT::Handle->Rollback();
2911 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2914 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2915 PrincipalId => $NewOwnerObj->PrincipalId,
2916 InsideTransaction => 1 );
2918 $RT::Handle->Rollback();
2919 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2922 # We call set twice with slightly different arguments, so
2923 # as to not have an SQL transaction span two RT transactions
2925 my ( $val, $msg ) = $self->_Set(
2927 RecordTransaction => 0,
2928 Value => $NewOwnerObj->Id,
2930 TransactionType => $Type,
2931 CheckACL => 0, # don't check acl
2935 $RT::Handle->Rollback;
2936 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2939 ($val, $msg) = $self->_NewTransaction(
2942 NewValue => $NewOwnerObj->Id,
2943 OldValue => $OldOwnerObj->Id,
2948 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2949 $OldOwnerObj->Name, $NewOwnerObj->Name );
2952 $RT::Handle->Rollback();
2956 $RT::Handle->Commit();
2958 return ( $val, $msg );
2967 A convenince method to set the ticket's owner to the current user
2973 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2982 Convenience method to set the owner to 'nobody' if the current user is the owner.
2988 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2997 A convenience method to change the owner of the current ticket to the
2998 current user. Even if it's owned by another user.
3005 if ( $self->IsOwner( $self->CurrentUser ) ) {
3006 return ( 0, $self->loc("You already own this ticket") );
3009 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3019 # {{{ Routines dealing with status
3021 # {{{ sub ValidateStatus
3023 =head2 ValidateStatus STATUS
3025 Takes a string. Returns true if that status is a valid status for this ticket.
3026 Returns false otherwise.
3030 sub ValidateStatus {
3034 #Make sure the status passed in is valid
3035 unless ( $self->QueueObj->IsValidStatus($status) ) {
3047 =head2 SetStatus STATUS
3049 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3051 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.
3062 $args{Status} = shift;
3069 if ( $args{Status} eq 'deleted') {
3070 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3071 return ( 0, $self->loc('Permission Denied') );
3074 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3075 return ( 0, $self->loc('Permission Denied') );
3079 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3080 return (0, $self->loc('That ticket has unresolved dependencies'));
3083 my $now = RT::Date->new( $self->CurrentUser );
3086 #If we're changing the status from new, record that we've started
3087 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3089 #Set the Started time to "now"
3090 $self->_Set( Field => 'Started',
3092 RecordTransaction => 0 );
3095 #When we close a ticket, set the 'Resolved' attribute to now.
3096 # It's misnamed, but that's just historical.
3097 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3098 $self->_Set( Field => 'Resolved',
3100 RecordTransaction => 0 );
3103 #Actually update the status
3104 my ($val, $msg)= $self->_Set( Field => 'Status',
3105 Value => $args{Status},
3108 TransactionType => 'Status' );
3119 Takes no arguments. Marks this ticket for garbage collection
3125 return ( $self->SetStatus('deleted') );
3127 # TODO: garbage collection
3136 Sets this ticket's status to stalled
3142 return ( $self->SetStatus('stalled') );
3151 Sets this ticket's status to rejected
3157 return ( $self->SetStatus('rejected') );
3166 Sets this ticket\'s status to Open
3172 return ( $self->SetStatus('open') );
3181 Sets this ticket\'s status to Resolved
3187 return ( $self->SetStatus('resolved') );
3195 # {{{ Actions + Routines dealing with transactions
3197 # {{{ sub SetTold and _SetTold
3199 =head2 SetTold ISO [TIMETAKEN]
3201 Updates the told and records a transaction
3208 $told = shift if (@_);
3209 my $timetaken = shift || 0;
3211 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3212 return ( 0, $self->loc("Permission Denied") );
3215 my $datetold = new RT::Date( $self->CurrentUser );
3217 $datetold->Set( Format => 'iso',
3221 $datetold->SetToNow();
3224 return ( $self->_Set( Field => 'Told',
3225 Value => $datetold->ISO,
3226 TimeTaken => $timetaken,
3227 TransactionType => 'Told' ) );
3232 Updates the told without a transaction or acl check. Useful when we're sending replies.
3239 my $now = new RT::Date( $self->CurrentUser );
3242 #use __Set to get no ACLs ;)
3243 return ( $self->__Set( Field => 'Told',
3244 Value => $now->ISO ) );
3254 my $uid = $self->CurrentUser->id;
3255 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3256 return if $attr && $attr->Content gt $self->LastUpdated;
3258 my $txns = $self->Transactions;
3259 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3260 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3261 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3265 VALUE => $attr->Content
3267 $txns->RowsPerPage(1);
3268 return $txns->First;
3273 =head2 TransactionBatch
3275 Returns an array reference of all transactions created on this ticket during
3276 this ticket object's lifetime or since last application of a batch, or undef
3279 Only works when the C<UseTransactionBatch> config option is set to true.
3283 sub TransactionBatch {
3285 return $self->{_TransactionBatch};
3288 =head2 ApplyTransactionBatch
3290 Applies scrips on the current batch of transactions and shinks it. Usually
3291 batch is applied when object is destroyed, but in some cases it's too late.
3295 sub ApplyTransactionBatch {
3298 my $batch = $self->TransactionBatch;
3299 return unless $batch && @$batch;
3301 $self->_ApplyTransactionBatch;
3303 $self->{_TransactionBatch} = [];
3306 sub _ApplyTransactionBatch {
3308 my $batch = $self->TransactionBatch;
3311 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3314 RT::Scrips->new($RT::SystemUser)->Apply(
3315 Stage => 'TransactionBatch',
3317 TransactionObj => $batch->[0],
3321 # Entry point of the rule system
3322 my $rules = RT::Ruleset->FindAllRules(
3323 Stage => 'TransactionBatch',
3325 TransactionObj => $batch->[0],
3328 RT::Ruleset->CommitRules($rules);
3334 # DESTROY methods need to localize $@, or it may unset it. This
3335 # causes $m->abort to not bubble all of the way up. See perlbug
3336 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3339 # The following line eliminates reentrancy.
3340 # It protects against the fact that perl doesn't deal gracefully
3341 # when an object's refcount is changed in its destructor.
3342 return if $self->{_Destroyed}++;
3344 my $batch = $self->TransactionBatch;
3345 return unless $batch && @$batch;
3347 return $self->_ApplyTransactionBatch;
3352 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3354 # {{{ sub _OverlayAccessible
3356 sub _OverlayAccessible {
3358 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3359 Queue => { 'read' => 1, 'write' => 1 },
3360 Requestors => { 'read' => 1, 'write' => 1 },
3361 Owner => { 'read' => 1, 'write' => 1 },
3362 Subject => { 'read' => 1, 'write' => 1 },
3363 InitialPriority => { 'read' => 1, 'write' => 1 },
3364 FinalPriority => { 'read' => 1, 'write' => 1 },
3365 Priority => { 'read' => 1, 'write' => 1 },
3366 Status => { 'read' => 1, 'write' => 1 },
3367 TimeEstimated => { 'read' => 1, 'write' => 1 },
3368 TimeWorked => { 'read' => 1, 'write' => 1 },
3369 TimeLeft => { 'read' => 1, 'write' => 1 },
3370 Told => { 'read' => 1, 'write' => 1 },
3371 Resolved => { 'read' => 1 },
3372 Type => { 'read' => 1 },
3373 Starts => { 'read' => 1, 'write' => 1 },
3374 Started => { 'read' => 1, 'write' => 1 },
3375 Due => { 'read' => 1, 'write' => 1 },
3376 Creator => { 'read' => 1, 'auto' => 1 },
3377 Created => { 'read' => 1, 'auto' => 1 },
3378 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3379 LastUpdated => { 'read' => 1, 'auto' => 1 }
3391 my %args = ( Field => undef,
3394 RecordTransaction => 1,
3397 TransactionType => 'Set',
3400 if ($args{'CheckACL'}) {
3401 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3402 return ( 0, $self->loc("Permission Denied"));
3406 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3407 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3408 return(0, $self->loc("Internal Error"));
3411 #if the user is trying to modify the record
3413 #Take care of the old value we really don't want to get in an ACL loop.
3414 # so ask the super::_Value
3415 my $Old = $self->SUPER::_Value("$args{'Field'}");
3418 if ( $args{'UpdateTicket'} ) {
3421 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3422 Value => $args{'Value'} );
3424 #If we can't actually set the field to the value, don't record
3425 # a transaction. instead, get out of here.
3426 return ( 0, $msg ) unless $ret;
3429 if ( $args{'RecordTransaction'} == 1 ) {
3431 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3432 Type => $args{'TransactionType'},
3433 Field => $args{'Field'},
3434 NewValue => $args{'Value'},
3436 TimeTaken => $args{'TimeTaken'},
3438 return ( $Trans, scalar $TransObj->BriefDescription );
3441 return ( $ret, $msg );
3451 Takes the name of a table column.
3452 Returns its value as a string, if the user passes an ACL check
3461 #if the field is public, return it.
3462 if ( $self->_Accessible( $field, 'public' ) ) {
3464 #$RT::Logger->debug("Skipping ACL check for $field");
3465 return ( $self->SUPER::_Value($field) );
3469 #If the current user doesn't have ACLs, don't let em at it.
3471 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3474 return ( $self->SUPER::_Value($field) );
3480 # {{{ sub _UpdateTimeTaken
3482 =head2 _UpdateTimeTaken
3484 This routine will increment the timeworked counter. it should
3485 only be called from _NewTransaction
3489 sub _UpdateTimeTaken {
3491 my $Minutes = shift;
3494 $Total = $self->SUPER::_Value("TimeWorked");
3495 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3497 Field => "TimeWorked",
3508 # {{{ Routines dealing with ACCESS CONTROL
3510 # {{{ sub CurrentUserHasRight
3512 =head2 CurrentUserHasRight
3514 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3515 1 if the user has that right. It returns 0 if the user doesn't have that right.
3519 sub CurrentUserHasRight {
3523 return $self->CurrentUser->PrincipalObj->HasRight(
3535 Takes a paramhash with the attributes 'Right' and 'Principal'
3536 'Right' is a ticket-scoped textual right from RT::ACE
3537 'Principal' is an RT::User object
3539 Returns 1 if the principal has the right. Returns undef if not.
3551 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3553 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3554 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3559 $args{'Principal'}->HasRight(
3561 Right => $args{'Right'}
3572 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3573 It isn't acutally a searchbuilder collection itself.
3580 unless ($self->{'__reminders'}) {
3581 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3582 $self->{'__reminders'}->Ticket($self->id);
3584 return $self->{'__reminders'};
3590 # {{{ sub Transactions
3594 Returns an RT::Transactions object of all transactions on this ticket
3601 my $transactions = RT::Transactions->new( $self->CurrentUser );
3603 #If the user has no rights, return an empty object
3604 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3605 $transactions->LimitToTicket($self->id);
3607 # if the user may not see comments do not return them
3608 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3609 $transactions->Limit(
3615 $transactions->Limit(
3619 VALUE => "CommentEmailRecord",
3620 ENTRYAGGREGATOR => 'AND'
3625 $transactions->Limit(
3629 ENTRYAGGREGATOR => 'AND'
3633 return ($transactions);
3639 # {{{ TransactionCustomFields
3641 =head2 TransactionCustomFields
3643 Returns the custom fields that transactions on tickets will have.
3647 sub TransactionCustomFields {
3649 return $self->QueueObj->TicketTransactionCustomFields;
3654 # {{{ sub CustomFieldValues
3656 =head2 CustomFieldValues
3658 # Do name => id mapping (if needed) before falling back to
3659 # RT::Record's CustomFieldValues
3665 sub CustomFieldValues {
3669 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3671 my $cf = RT::CustomField->new( $self->CurrentUser );
3672 $cf->SetContextObject( $self );
3673 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3674 unless ( $cf->id ) {
3675 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3678 # If we didn't find a valid cfid, give up.
3679 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3681 return $self->SUPER::CustomFieldValues( $cf->id );
3686 # {{{ sub CustomFieldLookupType
3688 =head2 CustomFieldLookupType
3690 Returns the RT::Ticket lookup type, which can be passed to
3691 RT::CustomField->Create() via the 'LookupType' hash key.
3697 sub CustomFieldLookupType {
3698 "RT::Queue-RT::Ticket";
3701 =head2 ACLEquivalenceObjects
3703 This method returns a list of objects for which a user's rights also apply
3704 to this ticket. Generally, this is only the ticket's queue, but some RT
3705 extensions may make other objects available too.
3707 This method is called from L<RT::Principal/HasRight>.
3711 sub ACLEquivalenceObjects {
3713 return $self->QueueObj;
3722 Jesse Vincent, jesse@bestpractical.com