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 }
142 Takes a single argument. This can be a ticket id, ticket alias or
143 local ticket uri. If the ticket can't be loaded, returns undef.
144 Otherwise, returns the ticket id.
152 #TODO modify this routine to look at EffectiveId and do the recursive load
153 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
155 # FIXME: there is no TicketBaseURI option in config
156 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
157 #If it's a local URI, turn it into a ticket id
158 if ( $base_uri && defined $id && $id =~ /^$base_uri(\d+)$/ ) {
162 #If it's a remote URI, we're going to punt for now
163 elsif ( $id =~ '://' ) {
167 #If we have an integer URI, load the ticket
168 if ( defined $id && $id =~ /^\d+$/ ) {
169 my ($ticketid,$msg) = $self->LoadById($id);
172 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
177 #It's not a URI. It's not a numerical ticket ID. Punt!
179 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
183 #If we're merged, resolve the merge.
184 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
185 $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
186 return ( $self->Load( $self->EffectiveId ) );
189 #Ok. we're loaded. lets get outa here.
190 return ( $self->Id );
200 Arguments: ARGS is a hash of named parameters. Valid parameters are:
203 Queue - Either a Queue object or a Queue Name
204 Requestor - A reference to a list of email addresses or RT user Names
205 Cc - A reference to a list of email addresses or Names
206 AdminCc - A reference to a list of email addresses or Names
207 SquelchMailTo - A reference to a list of email addresses -
208 who should this ticket not mail
209 Type -- The ticket\'s type. ignore this for now
210 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
211 Subject -- A string describing the subject of the ticket
212 Priority -- an integer from 0 to 99
213 InitialPriority -- an integer from 0 to 99
214 FinalPriority -- an integer from 0 to 99
215 Status -- any valid status (Defined in RT::Queue)
216 TimeEstimated -- an integer. estimated time for this task in minutes
217 TimeWorked -- an integer. time worked so far in minutes
218 TimeLeft -- an integer. time remaining in minutes
219 Starts -- an ISO date describing the ticket\'s start date and time in GMT
220 Due -- an ISO date describing the ticket\'s due date and time in GMT
221 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
222 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
224 Ticket links can be set up during create by passing the link type as a hask key and
225 the ticket id to be linked to as a value (or a URI when linking to other objects).
226 Multiple links of the same type can be created by passing an array ref. For example:
229 DependsOn => [ 15, 22 ],
230 RefersTo => 'http://www.bestpractical.com',
232 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
233 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
234 C<Members> and C<Children> are aliases for C<HasMember>.
236 Returns: TICKETID, Transaction Object, Error Message
246 EffectiveId => undef,
251 SquelchMailTo => undef,
255 InitialPriority => undef,
256 FinalPriority => undef,
267 _RecordTransaction => 1,
272 my ($ErrStr, @non_fatal_errors);
274 my $QueueObj = RT::Queue->new( $RT::SystemUser );
275 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
276 $QueueObj->Load( $args{'Queue'}->Id );
278 elsif ( $args{'Queue'} ) {
279 $QueueObj->Load( $args{'Queue'} );
282 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
285 #Can't create a ticket without a queue.
286 unless ( $QueueObj->Id ) {
287 $RT::Logger->debug("$self No queue given for ticket creation.");
288 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
292 #Now that we have a queue, Check the ACLS
294 $self->CurrentUser->HasRight(
295 Right => 'CreateTicket',
302 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
305 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
306 return ( 0, 0, $self->loc('Invalid value for status') );
309 #Since we have a queue, we can set queue defaults
312 # If there's no queue default initial priority and it's not set, set it to 0
313 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
314 unless defined $args{'InitialPriority'};
317 # If there's no queue default final priority and it's not set, set it to 0
318 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
319 unless defined $args{'FinalPriority'};
321 # Priority may have changed from InitialPriority, for the case
322 # where we're importing tickets (eg, from an older RT version.)
323 $args{'Priority'} = $args{'InitialPriority'}
324 unless defined $args{'Priority'};
327 #TODO we should see what sort of due date we're getting, rather +
328 # than assuming it's in ISO format.
330 #Set the due date. if we didn't get fed one, use the queue default due in
331 my $Due = new RT::Date( $self->CurrentUser );
332 if ( defined $args{'Due'} ) {
333 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
335 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
337 $Due->AddDays( $due_in );
340 my $Starts = new RT::Date( $self->CurrentUser );
341 if ( defined $args{'Starts'} ) {
342 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
345 my $Started = new RT::Date( $self->CurrentUser );
346 if ( defined $args{'Started'} ) {
347 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
349 elsif ( $args{'Status'} ne 'new' ) {
353 my $Resolved = new RT::Date( $self->CurrentUser );
354 if ( defined $args{'Resolved'} ) {
355 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
358 #If the status is an inactive status, set the resolved date
359 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
361 $RT::Logger->debug( "Got a ". $args{'Status'}
362 ."(inactive) ticket with undefined resolved date. Setting to now."
369 # {{{ Dealing with time fields
371 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
372 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
373 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
377 # {{{ Deal with setting the owner
380 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
381 if ( $args{'Owner'}->id ) {
382 $Owner = $args{'Owner'};
384 $RT::Logger->error('passed not loaded owner object');
385 push @non_fatal_errors, $self->loc("Invalid owner object");
390 #If we've been handed something else, try to load the user.
391 elsif ( $args{'Owner'} ) {
392 $Owner = RT::User->new( $self->CurrentUser );
393 $Owner->Load( $args{'Owner'} );
394 $Owner->LoadByEmail( $args{'Owner'} )
396 unless ( $Owner->Id ) {
397 push @non_fatal_errors,
398 $self->loc("Owner could not be set.") . " "
399 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
404 #If we have a proposed owner and they don't have the right
405 #to own a ticket, scream about it and make them not the owner
408 if ( $Owner && $Owner->Id != $RT::Nobody->Id
409 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
411 $DeferOwner = $Owner;
413 $RT::Logger->debug('going to deffer setting owner');
417 #If we haven't been handed a valid owner, make it nobody.
418 unless ( defined($Owner) && $Owner->Id ) {
419 $Owner = new RT::User( $self->CurrentUser );
420 $Owner->Load( $RT::Nobody->Id );
425 # We attempt to load or create each of the people who might have a role for this ticket
426 # _outside_ the transaction, so we don't get into ticket creation races
427 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
428 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
429 foreach my $watcher ( splice @{ $args{$type} } ) {
430 next unless $watcher;
431 if ( $watcher =~ /^\d+$/ ) {
432 push @{ $args{$type} }, $watcher;
434 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
435 foreach my $address( @addresses ) {
436 my $user = RT::User->new( $RT::SystemUser );
437 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
439 push @non_fatal_errors,
440 $self->loc("Couldn't load or create user: [_1]", $msg);
442 push @{ $args{$type} }, $user->id;
449 $RT::Handle->BeginTransaction();
452 Queue => $QueueObj->Id,
454 Subject => $args{'Subject'},
455 InitialPriority => $args{'InitialPriority'},
456 FinalPriority => $args{'FinalPriority'},
457 Priority => $args{'Priority'},
458 Status => $args{'Status'},
459 TimeWorked => $args{'TimeWorked'},
460 TimeEstimated => $args{'TimeEstimated'},
461 TimeLeft => $args{'TimeLeft'},
462 Type => $args{'Type'},
463 Starts => $Starts->ISO,
464 Started => $Started->ISO,
465 Resolved => $Resolved->ISO,
469 # Parameters passed in during an import that we probably don't want to touch, otherwise
470 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
471 $params{$attr} = $args{$attr} if $args{$attr};
474 # Delete null integer parameters
476 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority)
478 delete $params{$attr}
479 unless ( exists $params{$attr} && $params{$attr} );
482 # Delete the time worked if we're counting it in the transaction
483 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
485 my ($id,$ticket_message) = $self->SUPER::Create( %params );
487 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
488 $RT::Handle->Rollback();
490 $self->loc("Ticket could not be created due to an internal error")
494 #Set the ticket's effective ID now that we've created it.
495 my ( $val, $msg ) = $self->__Set(
496 Field => 'EffectiveId',
497 Value => ( $args{'EffectiveId'} || $id )
500 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
501 $RT::Handle->Rollback;
503 $self->loc("Ticket could not be created due to an internal error")
507 my $create_groups_ret = $self->_CreateTicketGroups();
508 unless ($create_groups_ret) {
509 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
511 . ". aborting Ticket creation." );
512 $RT::Handle->Rollback();
514 $self->loc("Ticket could not be created due to an internal error")
518 # Set the owner in the Groups table
519 # We denormalize it into the Ticket table too because doing otherwise would
520 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
521 $self->OwnerGroup->_AddMember(
522 PrincipalId => $Owner->PrincipalId,
523 InsideTransaction => 1
524 ) unless $DeferOwner;
528 # {{{ Deal with setting up watchers
530 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
531 # we know it's an array ref
532 foreach my $watcher ( @{ $args{$type} } ) {
534 # Note that we're using AddWatcher, rather than _AddWatcher, as we
535 # actually _want_ that ACL check. Otherwise, random ticket creators
536 # could make themselves adminccs and maybe get ticket rights. that would
538 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
540 my ($val, $msg) = $self->$method(
542 PrincipalId => $watcher,
545 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
550 if ($args{'SquelchMailTo'}) {
551 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
552 : $args{'SquelchMailTo'};
553 $self->_SquelchMailTo( @squelch );
559 # {{{ Add all the custom fields
561 foreach my $arg ( keys %args ) {
562 next unless $arg =~ /^CustomField-(\d+)$/i;
566 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
568 next unless defined $value && length $value;
570 # Allow passing in uploaded LargeContent etc by hash reference
571 my ($status, $msg) = $self->_AddCustomFieldValue(
572 (UNIVERSAL::isa( $value => 'HASH' )
577 RecordTransaction => 0,
579 push @non_fatal_errors, $msg unless $status;
585 # {{{ Deal with setting up links
587 # TODO: Adding link may fire scrips on other end and those scrips
588 # could create transactions on this ticket before 'Create' transaction.
590 # We should implement different schema: record 'Create' transaction,
591 # create links and only then fire create transaction's scrips.
593 # Ideal variant: add all links without firing scrips, record create
594 # transaction and only then fire scrips on the other ends of links.
598 foreach my $type ( keys %LINKTYPEMAP ) {
599 next unless ( defined $args{$type} );
601 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
603 # Check rights on the other end of the link if we must
604 # then run _AddLink that doesn't check for ACLs
605 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
606 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
608 push @non_fatal_errors, $msg;
611 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
612 push @non_fatal_errors, $self->loc('Linking. Permission denied');
617 my ( $wval, $wmsg ) = $self->_AddLink(
618 Type => $LINKTYPEMAP{$type}->{'Type'},
619 $LINKTYPEMAP{$type}->{'Mode'} => $link,
620 Silent => !$args{'_RecordTransaction'},
621 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
625 push @non_fatal_errors, $wmsg unless ($wval);
630 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
631 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
633 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
635 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
636 . ") was proposed as a ticket owner but has no rights to own "
637 . "tickets in " . $QueueObj->Name );
638 push @non_fatal_errors, $self->loc(
639 "Owner '[_1]' does not have rights to own this ticket.",
643 $Owner = $DeferOwner;
644 $self->__Set(Field => 'Owner', Value => $Owner->id);
646 # {{{ Deal with auto-customer association
648 #unless we already have (a) customer(s)...
649 unless ( $self->Customers->Count ) {
651 #first find any requestors with emails but *without* customer targets
652 my @NoCust_Requestors =
653 grep { $_->EmailAddress && ! $_->Customers->Count }
654 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
656 for my $Requestor (@NoCust_Requestors) {
658 #perhaps the stuff in here should be in a User method??
660 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
662 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
664 ## false laziness w/RT/Interface/Web_Vendor.pm
665 my @link = ( 'Type' => 'MemberOf',
666 'Target' => "freeside://freeside/cust_main/$custnum",
669 my( $val, $msg ) = $Requestor->_AddLink(@link);
670 #XXX should do something with $msg# push @non_fatal_errors, $msg;
676 #find any requestors with customer targets
678 my %cust_target = ();
681 grep { $_->Customers->Count }
682 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
684 foreach my $Requestor ( @Requestors ) {
685 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
686 $cust_target{ $cust_link->Target } = 1;
690 #and then auto-associate this ticket with those customers
692 foreach my $cust_target ( keys %cust_target ) {
694 my @link = ( 'Type' => 'MemberOf',
695 #'Target' => "freeside://freeside/cust_main/$custnum",
696 'Target' => $cust_target,
699 my( $val, $msg ) = $self->_AddLink(@link);
700 push @non_fatal_errors, $msg;
709 $self->OwnerGroup->_AddMember(
710 PrincipalId => $Owner->PrincipalId,
711 InsideTransaction => 1
715 if ( $args{'_RecordTransaction'} ) {
717 # {{{ Add a transaction for the create
718 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
720 TimeTaken => $args{'TimeWorked'},
721 MIMEObj => $args{'MIMEObj'},
722 CommitScrips => !$args{'DryRun'},
725 if ( $self->Id && $Trans ) {
727 $TransObj->UpdateCustomFields(ARGSRef => \%args);
729 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
730 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
731 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
734 $RT::Handle->Rollback();
736 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
737 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
738 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
741 if ( $args{'DryRun'} ) {
742 $RT::Handle->Rollback();
743 return ($self->id, $TransObj, $ErrStr);
745 $RT::Handle->Commit();
746 return ( $self->Id, $TransObj->Id, $ErrStr );
752 # Not going to record a transaction
753 $RT::Handle->Commit();
754 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
755 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
756 return ( $self->Id, 0, $ErrStr );
764 # {{{ _Parse822HeadersForAttributes Content
766 =head2 _Parse822HeadersForAttributes Content
768 Takes an RFC822 style message and parses its attributes into a hash.
772 sub _Parse822HeadersForAttributes {
777 my @lines = ( split ( /\n/, $content ) );
778 while ( defined( my $line = shift @lines ) ) {
779 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
784 if ( defined( $args{$tag} ) )
785 { #if we're about to get a second value, make it an array
786 $args{$tag} = [ $args{$tag} ];
788 if ( ref( $args{$tag} ) )
789 { #If it's an array, we want to push the value
790 push @{ $args{$tag} }, $value;
792 else { #if there's nothing there, just set the value
793 $args{$tag} = $value;
795 } elsif ($line =~ /^$/) {
797 #TODO: this won't work, since "" isn't of the form "foo:value"
799 while ( defined( my $l = shift @lines ) ) {
800 push @{ $args{'content'} }, $l;
806 foreach my $date qw(due starts started resolved) {
807 my $dateobj = RT::Date->new($RT::SystemUser);
808 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
809 $dateobj->Set( Format => 'unix', Value => $args{$date} );
812 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
814 $args{$date} = $dateobj->ISO;
816 $args{'mimeobj'} = MIME::Entity->new();
817 $args{'mimeobj'}->build(
818 Type => ( $args{'contenttype'} || 'text/plain' ),
819 Data => ($args{'content'} || '')
829 =head2 Import PARAMHASH
832 Doesn\'t create a transaction.
833 Doesn\'t supply queue defaults, etc.
841 my ( $ErrStr, $QueueObj, $Owner );
845 EffectiveId => undef,
849 Owner => $RT::Nobody->Id,
850 Subject => '[no subject]',
851 InitialPriority => undef,
852 FinalPriority => undef,
863 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
864 $QueueObj = RT::Queue->new($RT::SystemUser);
865 $QueueObj->Load( $args{'Queue'} );
867 #TODO error check this and return 0 if it\'s not loading properly +++
869 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
870 $QueueObj = RT::Queue->new($RT::SystemUser);
871 $QueueObj->Load( $args{'Queue'}->Id );
875 "$self " . $args{'Queue'} . " not a recognised queue object." );
878 #Can't create a ticket without a queue.
879 unless ( defined($QueueObj) and $QueueObj->Id ) {
880 $RT::Logger->debug("$self No queue given for ticket creation.");
881 return ( 0, $self->loc('Could not create ticket. Queue not set') );
884 #Now that we have a queue, Check the ACLS
886 $self->CurrentUser->HasRight(
887 Right => 'CreateTicket',
893 $self->loc("No permission to create tickets in the queue '[_1]'"
897 # {{{ Deal with setting the owner
899 # Attempt to take user object, user name or user id.
900 # Assign to nobody if lookup fails.
901 if ( defined( $args{'Owner'} ) ) {
902 if ( ref( $args{'Owner'} ) ) {
903 $Owner = $args{'Owner'};
906 $Owner = new RT::User( $self->CurrentUser );
907 $Owner->Load( $args{'Owner'} );
908 if ( !defined( $Owner->id ) ) {
909 $Owner->Load( $RT::Nobody->id );
914 #If we have a proposed owner and they don't have the right
915 #to own a ticket, scream about it and make them not the owner
918 and ( $Owner->Id != $RT::Nobody->Id )
928 $RT::Logger->warning( "$self user "
932 . "as a ticket owner but has no rights to own "
934 . $QueueObj->Name . "'" );
939 #If we haven't been handed a valid owner, make it nobody.
940 unless ( defined($Owner) ) {
941 $Owner = new RT::User( $self->CurrentUser );
942 $Owner->Load( $RT::Nobody->UserObj->Id );
947 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
948 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
951 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
952 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
953 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
954 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
956 # If we're coming in with an id, set that now.
957 my $EffectiveId = undef;
959 $EffectiveId = $args{'id'};
963 my $id = $self->SUPER::Create(
965 EffectiveId => $EffectiveId,
966 Queue => $QueueObj->Id,
968 Subject => $args{'Subject'}, # loc
969 InitialPriority => $args{'InitialPriority'}, # loc
970 FinalPriority => $args{'FinalPriority'}, # loc
971 Priority => $args{'InitialPriority'}, # loc
972 Status => $args{'Status'}, # loc
973 TimeWorked => $args{'TimeWorked'}, # loc
974 Type => $args{'Type'}, # loc
975 Created => $args{'Created'}, # loc
976 Told => $args{'Told'}, # loc
977 LastUpdated => $args{'Updated'}, # loc
978 Resolved => $args{'Resolved'}, # loc
979 Due => $args{'Due'}, # loc
982 # If the ticket didn't have an id
983 # Set the ticket's effective ID now that we've created it.
985 $self->Load( $args{'id'} );
989 $self->__Set( Field => 'EffectiveId', Value => $id );
993 $self . "->Import couldn't set EffectiveId: $msg" );
997 my $create_groups_ret = $self->_CreateTicketGroups();
998 unless ($create_groups_ret) {
1000 "Couldn't create ticket groups for ticket " . $self->Id );
1003 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1006 foreach $watcher ( @{ $args{'Cc'} } ) {
1007 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1009 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1010 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1013 foreach $watcher ( @{ $args{'Requestor'} } ) {
1014 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1018 return ( $self->Id, $ErrStr );
1023 # {{{ Routines dealing with watchers.
1025 # {{{ _CreateTicketGroups
1027 =head2 _CreateTicketGroups
1029 Create the ticket groups and links for this ticket.
1030 This routine expects to be called from Ticket->Create _inside of a transaction_
1032 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1034 It will return true on success and undef on failure.
1040 sub _CreateTicketGroups {
1043 my @types = qw(Requestor Owner Cc AdminCc);
1045 foreach my $type (@types) {
1046 my $type_obj = RT::Group->new($self->CurrentUser);
1047 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1048 Instance => $self->Id,
1051 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1052 $self->Id.": ".$msg);
1062 # {{{ sub OwnerGroup
1066 A constructor which returns an RT::Group object containing the owner of this ticket.
1072 my $owner_obj = RT::Group->new($self->CurrentUser);
1073 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1074 return ($owner_obj);
1080 # {{{ sub AddWatcher
1084 AddWatcher takes a parameter hash. The keys are as follows:
1086 Type One of Requestor, Cc, AdminCc
1088 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1090 Email The email address of the new watcher. If a user with this
1091 email address can't be found, a new nonprivileged user will be created.
1093 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.
1101 PrincipalId => undef,
1106 # ModifyTicket works in any case
1107 return $self->_AddWatcher( %args )
1108 if $self->CurrentUserHasRight('ModifyTicket');
1109 if ( $args{'Email'} ) {
1110 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1111 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1114 if ( lc $self->CurrentUser->UserObj->EmailAddress
1115 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1117 $args{'PrincipalId'} = $self->CurrentUser->id;
1118 delete $args{'Email'};
1122 # If the watcher isn't the current user then the current user has no right
1124 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1125 return ( 0, $self->loc("Permission Denied") );
1128 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1129 if ( $args{'Type'} eq 'AdminCc' ) {
1130 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1131 return ( 0, $self->loc('Permission Denied') );
1135 # If it's a Requestor or Cc and they don't have 'Watch', bail
1136 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1137 unless ( $self->CurrentUserHasRight('Watch') ) {
1138 return ( 0, $self->loc('Permission Denied') );
1142 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1143 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1146 return $self->_AddWatcher( %args );
1149 #This contains the meat of AddWatcher. but can be called from a routine like
1150 # Create, which doesn't need the additional acl check
1156 PrincipalId => undef,
1162 my $principal = RT::Principal->new($self->CurrentUser);
1163 if ($args{'Email'}) {
1164 my $user = RT::User->new($RT::SystemUser);
1165 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1166 $args{'PrincipalId'} = $pid if $pid;
1168 if ($args{'PrincipalId'}) {
1169 $principal->Load($args{'PrincipalId'});
1173 # If we can't find this watcher, we need to bail.
1174 unless ($principal->Id) {
1175 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1176 return(0, $self->loc("Could not find or create that user"));
1180 my $group = RT::Group->new($self->CurrentUser);
1181 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1182 unless ($group->id) {
1183 return(0,$self->loc("Group not found"));
1186 if ( $group->HasMember( $principal)) {
1188 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1192 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1193 InsideTransaction => 1 );
1195 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1197 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1200 unless ( $args{'Silent'} ) {
1201 $self->_NewTransaction(
1202 Type => 'AddWatcher',
1203 NewValue => $principal->Id,
1204 Field => $args{'Type'}
1208 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1214 # {{{ sub DeleteWatcher
1216 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1219 Deletes a Ticket watcher. Takes two arguments:
1221 Type (one of Requestor,Cc,AdminCc)
1225 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1227 Email (the email address of an existing wathcer)
1236 my %args = ( Type => undef,
1237 PrincipalId => undef,
1241 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1242 return ( 0, $self->loc("No principal specified") );
1244 my $principal = RT::Principal->new( $self->CurrentUser );
1245 if ( $args{'PrincipalId'} ) {
1247 $principal->Load( $args{'PrincipalId'} );
1250 my $user = RT::User->new( $self->CurrentUser );
1251 $user->LoadByEmail( $args{'Email'} );
1252 $principal->Load( $user->Id );
1255 # If we can't find this watcher, we need to bail.
1256 unless ( $principal->Id ) {
1257 return ( 0, $self->loc("Could not find that principal") );
1260 my $group = RT::Group->new( $self->CurrentUser );
1261 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1262 unless ( $group->id ) {
1263 return ( 0, $self->loc("Group not found") );
1267 #If the watcher we're trying to add is for the current user
1268 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1270 # If it's an AdminCc and they don't have
1271 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1272 if ( $args{'Type'} eq 'AdminCc' ) {
1273 unless ( $self->CurrentUserHasRight('ModifyTicket')
1274 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1275 return ( 0, $self->loc('Permission Denied') );
1279 # If it's a Requestor or Cc and they don't have
1280 # 'Watch' or 'ModifyTicket', bail
1281 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1283 unless ( $self->CurrentUserHasRight('ModifyTicket')
1284 or $self->CurrentUserHasRight('Watch') ) {
1285 return ( 0, $self->loc('Permission Denied') );
1289 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1291 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1295 # If the watcher isn't the current user
1296 # and the current user doesn't have 'ModifyTicket' bail
1298 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1299 return ( 0, $self->loc("Permission Denied") );
1305 # see if this user is already a watcher.
1307 unless ( $group->HasMember($principal) ) {
1309 $self->loc( 'That principal is not a [_1] for this ticket',
1313 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1315 $RT::Logger->error( "Failed to delete "
1317 . " as a member of group "
1323 'Could not remove that principal as a [_1] for this ticket',
1327 unless ( $args{'Silent'} ) {
1328 $self->_NewTransaction( Type => 'DelWatcher',
1329 OldValue => $principal->Id,
1330 Field => $args{'Type'} );
1334 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1335 $principal->Object->Name,
1344 =head2 SquelchMailTo [EMAIL]
1346 Takes an optional email address to never email about updates to this ticket.
1349 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1357 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1361 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1366 return $self->_SquelchMailTo(@_);
1369 sub _SquelchMailTo {
1373 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1374 unless grep { $_->Content eq $attr }
1375 $self->Attributes->Named('SquelchMailTo');
1377 my @attributes = $self->Attributes->Named('SquelchMailTo');
1378 return (@attributes);
1382 =head2 UnsquelchMailTo ADDRESS
1384 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1386 Returns a tuple of (status, message)
1390 sub UnsquelchMailTo {
1393 my $address = shift;
1394 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1395 return ( 0, $self->loc("Permission Denied") );
1398 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1399 return ($val, $msg);
1403 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1405 =head2 RequestorAddresses
1407 B<Returns> String: All Ticket Requestor email addresses as a string.
1411 sub RequestorAddresses {
1414 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1418 return ( $self->Requestors->MemberEmailAddressesAsString );
1422 =head2 AdminCcAddresses
1424 returns String: All Ticket AdminCc email addresses as a string
1428 sub AdminCcAddresses {
1431 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1435 return ( $self->AdminCc->MemberEmailAddressesAsString )
1441 returns String: All Ticket Ccs as a string of email addresses
1448 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1451 return ( $self->Cc->MemberEmailAddressesAsString);
1457 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1459 # {{{ sub Requestors
1464 Returns this ticket's Requestors as an RT::Group object
1471 my $group = RT::Group->new($self->CurrentUser);
1472 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1473 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1481 # {{{ sub _Requestors
1485 Private non-ACLed variant of Reqeustors so that we can look them up for the
1486 purposes of customer auto-association during create.
1493 my $group = RT::Group->new($RT::SystemUser);
1494 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1505 Returns an RT::Group object which contains this ticket's Ccs.
1506 If the user doesn't have "ShowTicket" permission, returns an empty group
1513 my $group = RT::Group->new($self->CurrentUser);
1514 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1515 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1528 Returns an RT::Group object which contains this ticket's AdminCcs.
1529 If the user doesn't have "ShowTicket" permission, returns an empty group
1536 my $group = RT::Group->new($self->CurrentUser);
1537 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1538 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1548 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1551 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1553 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1555 Takes a param hash with the attributes Type and either PrincipalId or Email
1557 Type is one of Requestor, Cc, AdminCc and Owner
1559 PrincipalId is an RT::Principal id, and Email is an email address.
1561 Returns true if the specified principal (or the one corresponding to the
1562 specified address) is a member of the group Type for this ticket.
1564 XX TODO: This should be Memoized.
1571 my %args = ( Type => 'Requestor',
1572 PrincipalId => undef,
1577 # Load the relevant group.
1578 my $group = RT::Group->new($self->CurrentUser);
1579 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1581 # Find the relevant principal.
1582 if (!$args{PrincipalId} && $args{Email}) {
1583 # Look up the specified user.
1584 my $user = RT::User->new($self->CurrentUser);
1585 $user->LoadByEmail($args{Email});
1587 $args{PrincipalId} = $user->PrincipalId;
1590 # A non-existent user can't be a group member.
1595 # Ask if it has the member in question
1596 return $group->HasMember( $args{'PrincipalId'} );
1601 # {{{ sub IsRequestor
1603 =head2 IsRequestor PRINCIPAL_ID
1605 Takes an L<RT::Principal> id.
1607 Returns true if the principal is a requestor of the current ticket.
1615 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1623 =head2 IsCc PRINCIPAL_ID
1625 Takes an RT::Principal id.
1626 Returns true if the principal is a Cc of the current ticket.
1635 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1643 =head2 IsAdminCc PRINCIPAL_ID
1645 Takes an RT::Principal id.
1646 Returns true if the principal is an AdminCc of the current ticket.
1654 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1664 Takes an RT::User object. Returns true if that user is this ticket's owner.
1665 returns undef otherwise
1673 # no ACL check since this is used in acl decisions
1674 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1678 #Tickets won't yet have owners when they're being created.
1679 unless ( $self->OwnerObj->id ) {
1683 if ( $person->id == $self->OwnerObj->id ) {
1698 =head2 TransactionAddresses
1700 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for all this ticket's Create, Comment or Correspond transactions.
1701 The keys are C<To>, C<Cc> and C<Bcc>. The values are lists of C<Email::Address> objects.
1703 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.
1708 sub TransactionAddresses {
1710 my $txns = $self->Transactions;
1713 foreach my $type (qw(Create Comment Correspond)) {
1714 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1717 while (my $txn = $txns->Next) {
1718 my $txnaddrs = $txn->Addresses;
1719 foreach my $addrlist ( values %$txnaddrs ) {
1720 foreach my $addr (@$addrlist) {
1721 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1722 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1723 # skips "comment-only" addresses
1724 next unless ($addr->address);
1725 $addresses{$addr->address} = $addr;
1737 # {{{ Routines dealing with queues
1739 # {{{ sub ValidateQueue
1746 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1750 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1751 my $id = $QueueObj->Load($Value);
1767 my $NewQueue = shift;
1769 #Redundant. ACL gets checked in _Set;
1770 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1771 return ( 0, $self->loc("Permission Denied") );
1774 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1775 $NewQueueObj->Load($NewQueue);
1777 unless ( $NewQueueObj->Id() ) {
1778 return ( 0, $self->loc("That queue does not exist") );
1781 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1782 return ( 0, $self->loc('That is the same value') );
1785 $self->CurrentUser->HasRight(
1786 Right => 'CreateTicket',
1787 Object => $NewQueueObj
1791 return ( 0, $self->loc("You may not create requests in that queue.") );
1795 $self->OwnerObj->HasRight(
1796 Right => 'OwnTicket',
1797 Object => $NewQueueObj
1801 my $clone = RT::Ticket->new( $RT::SystemUser );
1802 $clone->Load( $self->Id );
1803 unless ( $clone->Id ) {
1804 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1806 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1807 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1810 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1813 # On queue change, change queue for reminders too
1814 my $reminder_collection = $self->Reminders->Collection;
1815 while ( my $reminder = $reminder_collection->Next ) {
1816 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1817 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1821 return ($status, $msg);
1830 Takes nothing. returns this ticket's queue object
1837 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1839 #We call __Value so that we can avoid the ACL decision and some deep recursion
1840 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1841 return ($queue_obj);
1848 # {{{ Date printing routines
1854 Returns an RT::Date object containing this ticket's due date
1861 my $time = new RT::Date( $self->CurrentUser );
1863 # -1 is RT::Date slang for never
1864 if ( my $due = $self->Due ) {
1865 $time->Set( Format => 'sql', Value => $due );
1868 $time->Set( Format => 'unix', Value => -1 );
1876 # {{{ sub DueAsString
1880 Returns this ticket's due date as a human readable string
1886 return $self->DueObj->AsString();
1891 # {{{ sub ResolvedObj
1895 Returns an RT::Date object of this ticket's 'resolved' time.
1902 my $time = new RT::Date( $self->CurrentUser );
1903 $time->Set( Format => 'sql', Value => $self->Resolved );
1909 # {{{ sub SetStarted
1913 Takes a date in ISO format or undef
1914 Returns a transaction id and a message
1915 The client calls "Start" to note that the project was started on the date in $date.
1916 A null date means "now"
1922 my $time = shift || 0;
1924 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1925 return ( 0, $self->loc("Permission Denied") );
1928 #We create a date object to catch date weirdness
1929 my $time_obj = new RT::Date( $self->CurrentUser() );
1931 $time_obj->Set( Format => 'ISO', Value => $time );
1934 $time_obj->SetToNow();
1937 #Now that we're starting, open this ticket
1938 #TODO do we really want to force this as policy? it should be a scrip
1940 #We need $TicketAsSystem, in case the current user doesn't have
1943 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1944 $TicketAsSystem->Load( $self->Id );
1945 if ( $TicketAsSystem->Status eq 'new' ) {
1946 $TicketAsSystem->Open();
1949 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1955 # {{{ sub StartedObj
1959 Returns an RT::Date object which contains this ticket's
1967 my $time = new RT::Date( $self->CurrentUser );
1968 $time->Set( Format => 'sql', Value => $self->Started );
1978 Returns an RT::Date object which contains this ticket's
1986 my $time = new RT::Date( $self->CurrentUser );
1987 $time->Set( Format => 'sql', Value => $self->Starts );
1997 Returns an RT::Date object which contains this ticket's
2005 my $time = new RT::Date( $self->CurrentUser );
2006 $time->Set( Format => 'sql', Value => $self->Told );
2012 # {{{ sub ToldAsString
2016 A convenience method that returns ToldObj->AsString
2018 TODO: This should be deprecated
2024 if ( $self->Told ) {
2025 return $self->ToldObj->AsString();
2034 # {{{ sub TimeWorkedAsString
2036 =head2 TimeWorkedAsString
2038 Returns the amount of time worked on this ticket as a Text String
2042 sub TimeWorkedAsString {
2044 my $value = $self->TimeWorked;
2046 # return the # of minutes worked turned into seconds and written as
2047 # a simple text string, this is not really a date object, but if we
2048 # diff a number of seconds vs the epoch, we'll get a nice description
2050 return "" unless $value;
2051 return RT::Date->new( $self->CurrentUser )
2052 ->DurationAsString( $value * 60 );
2057 # {{{ sub TimeLeftAsString
2059 =head2 TimeLeftAsString
2061 Returns the amount of time left on this ticket as a Text String
2065 sub TimeLeftAsString {
2067 my $value = $self->TimeLeft;
2068 return "" unless $value;
2069 return RT::Date->new( $self->CurrentUser )
2070 ->DurationAsString( $value * 60 );
2075 # {{{ Routines dealing with correspondence/comments
2081 Comment on this ticket.
2082 Takes a hash with the following attributes:
2083 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2086 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2088 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2089 They will, however, be prepared and you'll be able to access them through the TransactionObj
2091 Returns: Transaction id, Error Message, Transaction Object
2092 (note the different order from Create()!)
2099 my %args = ( CcMessageTo => undef,
2100 BccMessageTo => undef,
2107 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2108 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2109 return ( 0, $self->loc("Permission Denied"), undef );
2111 $args{'NoteType'} = 'Comment';
2113 if ($args{'DryRun'}) {
2114 $RT::Handle->BeginTransaction();
2115 $args{'CommitScrips'} = 0;
2118 my @results = $self->_RecordNote(%args);
2119 if ($args{'DryRun'}) {
2120 $RT::Handle->Rollback();
2127 # {{{ sub Correspond
2131 Correspond on this ticket.
2132 Takes a hashref with the following attributes:
2135 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2137 if there's no MIMEObj, Content is used to build a MIME::Entity object
2139 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2140 They will, however, be prepared and you'll be able to access them through the TransactionObj
2142 Returns: Transaction id, Error Message, Transaction Object
2143 (note the different order from Create()!)
2150 my %args = ( CcMessageTo => undef,
2151 BccMessageTo => undef,
2157 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2158 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2159 return ( 0, $self->loc("Permission Denied"), undef );
2162 $args{'NoteType'} = 'Correspond';
2163 if ($args{'DryRun'}) {
2164 $RT::Handle->BeginTransaction();
2165 $args{'CommitScrips'} = 0;
2168 my @results = $self->_RecordNote(%args);
2170 #Set the last told date to now if this isn't mail from the requestor.
2171 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2172 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2174 if ($args{'DryRun'}) {
2175 $RT::Handle->Rollback();
2184 # {{{ sub _RecordNote
2188 the meat of both comment and correspond.
2190 Performs no access control checks. hence, dangerous.
2197 CcMessageTo => undef,
2198 BccMessageTo => undef,
2203 NoteType => 'Correspond',
2209 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2210 return ( 0, $self->loc("No message attached"), undef );
2213 unless ( $args{'MIMEObj'} ) {
2214 $args{'MIMEObj'} = MIME::Entity->build(
2215 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2219 # convert text parts into utf-8
2220 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2222 # If we've been passed in CcMessageTo and BccMessageTo fields,
2223 # add them to the mime object for passing on to the transaction handler
2224 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2225 # RT-Send-Bcc: headers
2228 foreach my $type (qw/Cc Bcc/) {
2229 if ( defined $args{ $type . 'MessageTo' } ) {
2231 my $addresses = join ', ', (
2232 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2233 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2234 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2238 foreach my $argument (qw(Encrypt Sign)) {
2239 $args{'MIMEObj'}->head->add(
2240 "X-RT-$argument" => $args{ $argument }
2241 ) if defined $args{ $argument };
2244 # If this is from an external source, we need to come up with its
2245 # internal Message-ID now, so all emails sent because of this
2246 # message have a common Message-ID
2247 my $org = RT->Config->Get('Organization');
2248 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2249 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2250 $args{'MIMEObj'}->head->set(
2251 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2255 #Record the correspondence (write the transaction)
2256 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2257 Type => $args{'NoteType'},
2258 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2259 TimeTaken => $args{'TimeTaken'},
2260 MIMEObj => $args{'MIMEObj'},
2261 CommitScrips => $args{'CommitScrips'},
2265 $RT::Logger->err("$self couldn't init a transaction $msg");
2266 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2269 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2281 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2284 my $type = shift || "";
2286 unless ( $self->{"$field$type"} ) {
2287 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2289 #not sure what this ACL was supposed to do... but returning the
2290 # bare (unlimited) RT::Links certainly seems wrong, it causes the
2291 # $Ticket->Customers method during creation to return results for every
2293 #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2295 # Maybe this ticket is a merged ticket
2296 my $Tickets = new RT::Tickets( $self->CurrentUser );
2297 # at least to myself
2298 $self->{"$field$type"}->Limit( FIELD => $field,
2299 VALUE => $self->URI,
2300 ENTRYAGGREGATOR => 'OR' );
2301 $Tickets->Limit( FIELD => 'EffectiveId',
2302 VALUE => $self->EffectiveId );
2303 while (my $Ticket = $Tickets->Next) {
2304 $self->{"$field$type"}->Limit( FIELD => $field,
2305 VALUE => $Ticket->URI,
2306 ENTRYAGGREGATOR => 'OR' );
2308 $self->{"$field$type"}->Limit( FIELD => 'Type',
2313 return ( $self->{"$field$type"} );
2318 # {{{ sub DeleteLink
2322 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2323 SilentBase and SilentTarget. Either Base or Target must be null.
2324 The null value will be replaced with this ticket\'s id.
2326 If Silent is true then no transaction would be recorded, in other
2327 case you can control creation of transactions on both base and
2328 target with SilentBase and SilentTarget respectively. By default
2329 both transactions are created.
2340 SilentBase => undef,
2341 SilentTarget => undef,
2345 unless ( $args{'Target'} || $args{'Base'} ) {
2346 $RT::Logger->error("Base or Target must be specified");
2347 return ( 0, $self->loc('Either base or target must be specified') );
2352 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2353 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2354 return ( 0, $self->loc("Permission Denied") );
2357 # If the other URI is an RT::Ticket, we want to make sure the user
2358 # can modify it too...
2359 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2360 return (0, $msg) unless $status;
2361 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2364 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2365 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2367 return ( 0, $self->loc("Permission Denied") );
2370 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2371 return ( 0, $Msg ) unless $val;
2373 return ( $val, $Msg ) if $args{'Silent'};
2375 my ($direction, $remote_link);
2377 if ( $args{'Base'} ) {
2378 $remote_link = $args{'Base'};
2379 $direction = 'Target';
2381 elsif ( $args{'Target'} ) {
2382 $remote_link = $args{'Target'};
2383 $direction = 'Base';
2386 my $remote_uri = RT::URI->new( $self->CurrentUser );
2387 $remote_uri->FromURI( $remote_link );
2389 unless ( $args{ 'Silent'. $direction } ) {
2390 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2391 Type => 'DeleteLink',
2392 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2393 OldValue => $remote_uri->URI || $remote_link,
2396 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2399 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2400 my $OtherObj = $remote_uri->Object;
2401 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2402 Type => 'DeleteLink',
2403 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2404 : $LINKDIRMAP{$args{'Type'}}->{Target},
2405 OldValue => $self->URI,
2406 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2409 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2412 return ( $val, $Msg );
2421 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2423 If Silent is true then no transaction would be recorded, in other
2424 case you can control creation of transactions on both base and
2425 target with SilentBase and SilentTarget respectively. By default
2426 both transactions are created.
2432 my %args = ( Target => '',
2436 SilentBase => undef,
2437 SilentTarget => undef,
2440 unless ( $args{'Target'} || $args{'Base'} ) {
2441 $RT::Logger->error("Base or Target must be specified");
2442 return ( 0, $self->loc('Either base or target must be specified') );
2446 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2447 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2448 return ( 0, $self->loc("Permission Denied") );
2451 # If the other URI is an RT::Ticket, we want to make sure the user
2452 # can modify it too...
2453 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2454 return (0, $msg) unless $status;
2455 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2458 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2459 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2461 return ( 0, $self->loc("Permission Denied") );
2464 return $self->_AddLink(%args);
2467 sub __GetTicketFromURI {
2469 my %args = ( URI => '', @_ );
2471 # If the other URI is an RT::Ticket, we want to make sure the user
2472 # can modify it too...
2473 my $uri_obj = RT::URI->new( $self->CurrentUser );
2474 $uri_obj->FromURI( $args{'URI'} );
2476 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2477 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2478 $RT::Logger->warning( $msg );
2481 my $obj = $uri_obj->Resolver->Object;
2482 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2483 return (1, 'Found not a ticket', undef);
2485 return (1, 'Found ticket', $obj);
2490 Private non-acled variant of AddLink so that links can be added during create.
2496 my %args = ( Target => '',
2500 SilentBase => undef,
2501 SilentTarget => undef,
2504 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2505 return ($val, $msg) if !$val || $exist;
2506 return ($val, $msg) if $args{'Silent'};
2508 my ($direction, $remote_link);
2509 if ( $args{'Target'} ) {
2510 $remote_link = $args{'Target'};
2511 $direction = 'Base';
2512 } elsif ( $args{'Base'} ) {
2513 $remote_link = $args{'Base'};
2514 $direction = 'Target';
2517 my $remote_uri = RT::URI->new( $self->CurrentUser );
2518 $remote_uri->FromURI( $remote_link );
2520 unless ( $args{ 'Silent'. $direction } ) {
2521 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2523 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2524 NewValue => $remote_uri->URI || $remote_link,
2527 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2530 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2531 my $OtherObj = $remote_uri->Object;
2532 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2534 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2535 : $LINKDIRMAP{$args{'Type'}}->{Target},
2536 NewValue => $self->URI,
2537 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2540 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2543 return ( $val, $msg );
2553 MergeInto take the id of the ticket to merge this ticket into.
2561 my $ticket_id = shift;
2563 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2564 return ( 0, $self->loc("Permission Denied") );
2567 # Load up the new ticket.
2568 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2569 $MergeInto->Load($ticket_id);
2571 # make sure it exists.
2572 unless ( $MergeInto->Id ) {
2573 return ( 0, $self->loc("New ticket doesn't exist") );
2576 # Make sure the current user can modify the new ticket.
2577 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2578 return ( 0, $self->loc("Permission Denied") );
2581 $RT::Handle->BeginTransaction();
2583 # We use EffectiveId here even though it duplicates information from
2584 # the links table becasue of the massive performance hit we'd take
2585 # by trying to do a separate database query for merge info everytime
2588 #update this ticket's effective id to the new ticket's id.
2589 my ( $id_val, $id_msg ) = $self->__Set(
2590 Field => 'EffectiveId',
2591 Value => $MergeInto->Id()
2595 $RT::Handle->Rollback();
2596 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2600 if ( $self->__Value('Status') ne 'resolved' ) {
2602 my ( $status_val, $status_msg )
2603 = $self->__Set( Field => 'Status', Value => 'resolved' );
2605 unless ($status_val) {
2606 $RT::Handle->Rollback();
2609 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2613 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2617 # update all the links that point to that old ticket
2618 my $old_links_to = RT::Links->new($self->CurrentUser);
2619 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2622 while (my $link = $old_links_to->Next) {
2623 if (exists $old_seen{$link->Base."-".$link->Type}) {
2626 elsif ($link->Base eq $MergeInto->URI) {
2629 # First, make sure the link doesn't already exist. then move it over.
2630 my $tmp = RT::Link->new($RT::SystemUser);
2631 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2635 $link->SetTarget($MergeInto->URI);
2636 $link->SetLocalTarget($MergeInto->id);
2638 $old_seen{$link->Base."-".$link->Type} =1;
2643 my $old_links_from = RT::Links->new($self->CurrentUser);
2644 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2646 while (my $link = $old_links_from->Next) {
2647 if (exists $old_seen{$link->Type."-".$link->Target}) {
2650 if ($link->Target eq $MergeInto->URI) {
2653 # First, make sure the link doesn't already exist. then move it over.
2654 my $tmp = RT::Link->new($RT::SystemUser);
2655 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2659 $link->SetBase($MergeInto->URI);
2660 $link->SetLocalBase($MergeInto->id);
2661 $old_seen{$link->Type."-".$link->Target} =1;
2667 # Update time fields
2668 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2670 my $mutator = "Set$type";
2671 $MergeInto->$mutator(
2672 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2675 #add all of this ticket's watchers to that ticket.
2676 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2678 my $people = $self->$watcher_type->MembersObj;
2679 my $addwatcher_type = $watcher_type;
2680 $addwatcher_type =~ s/s$//;
2682 while ( my $watcher = $people->Next ) {
2684 my ($val, $msg) = $MergeInto->_AddWatcher(
2685 Type => $addwatcher_type,
2687 PrincipalId => $watcher->MemberId
2690 $RT::Logger->warning($msg);
2696 #find all of the tickets that were merged into this ticket.
2697 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2698 $old_mergees->Limit(
2699 FIELD => 'EffectiveId',
2704 # update their EffectiveId fields to the new ticket's id
2705 while ( my $ticket = $old_mergees->Next() ) {
2706 my ( $val, $msg ) = $ticket->__Set(
2707 Field => 'EffectiveId',
2708 Value => $MergeInto->Id()
2712 #make a new link: this ticket is merged into that other ticket.
2713 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2715 $MergeInto->_SetLastUpdated;
2717 $RT::Handle->Commit();
2718 return ( 1, $self->loc("Merge Successful") );
2723 Returns list of tickets' ids that's been merged into this ticket.
2730 my $mergees = new RT::Tickets( $self->CurrentUser );
2732 FIELD => 'EffectiveId',
2741 return map $_->id, @{ $mergees->ItemsArrayRef || [] };
2748 # {{{ Routines dealing with ownership
2754 Takes nothing and returns an RT::User object of
2762 #If this gets ACLed, we lose on a rights check in User.pm and
2763 #get deep recursion. if we need ACLs here, we need
2764 #an equiv without ACLs
2766 my $owner = new RT::User( $self->CurrentUser );
2767 $owner->Load( $self->__Value('Owner') );
2769 #Return the owner object
2775 # {{{ sub OwnerAsString
2777 =head2 OwnerAsString
2779 Returns the owner's email address
2785 return ( $self->OwnerObj->EmailAddress );
2795 Takes two arguments:
2796 the Id or Name of the owner
2797 and (optionally) the type of the SetOwner Transaction. It defaults
2798 to 'Give'. 'Steal' is also a valid option.
2805 my $NewOwner = shift;
2806 my $Type = shift || "Give";
2808 $RT::Handle->BeginTransaction();
2810 $self->_SetLastUpdated(); # lock the ticket
2811 $self->Load( $self->id ); # in case $self changed while waiting for lock
2813 my $OldOwnerObj = $self->OwnerObj;
2815 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2816 $NewOwnerObj->Load( $NewOwner );
2817 unless ( $NewOwnerObj->Id ) {
2818 $RT::Handle->Rollback();
2819 return ( 0, $self->loc("That user does not exist") );
2823 # must have ModifyTicket rights
2824 # or TakeTicket/StealTicket and $NewOwner is self
2825 # see if it's a take
2826 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2827 unless ( $self->CurrentUserHasRight('ModifyTicket')
2828 || $self->CurrentUserHasRight('TakeTicket') ) {
2829 $RT::Handle->Rollback();
2830 return ( 0, $self->loc("Permission Denied") );
2834 # see if it's a steal
2835 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2836 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2838 unless ( $self->CurrentUserHasRight('ModifyTicket')
2839 || $self->CurrentUserHasRight('StealTicket') ) {
2840 $RT::Handle->Rollback();
2841 return ( 0, $self->loc("Permission Denied") );
2845 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2846 $RT::Handle->Rollback();
2847 return ( 0, $self->loc("Permission Denied") );
2851 # If we're not stealing and the ticket has an owner and it's not
2853 if ( $Type ne 'Steal' and $Type ne 'Force'
2854 and $OldOwnerObj->Id != $RT::Nobody->Id
2855 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2857 $RT::Handle->Rollback();
2858 return ( 0, $self->loc("You can only take tickets that are unowned") )
2859 if $NewOwnerObj->id == $self->CurrentUser->id;
2862 $self->loc("You can only reassign tickets that you own or that are unowned" )
2866 #If we've specified a new owner and that user can't modify the ticket
2867 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2868 $RT::Handle->Rollback();
2869 return ( 0, $self->loc("That user may not own tickets in that queue") );
2872 # If the ticket has an owner and it's the new owner, we don't need
2874 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2875 $RT::Handle->Rollback();
2876 return ( 0, $self->loc("That user already owns that ticket") );
2879 # Delete the owner in the owner group, then add a new one
2880 # TODO: is this safe? it's not how we really want the API to work
2881 # for most things, but it's fast.
2882 my ( $del_id, $del_msg );
2883 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2884 ($del_id, $del_msg) = $owner->Delete();
2885 last unless ($del_id);
2889 $RT::Handle->Rollback();
2890 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2893 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2894 PrincipalId => $NewOwnerObj->PrincipalId,
2895 InsideTransaction => 1 );
2897 $RT::Handle->Rollback();
2898 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2901 # We call set twice with slightly different arguments, so
2902 # as to not have an SQL transaction span two RT transactions
2904 my ( $val, $msg ) = $self->_Set(
2906 RecordTransaction => 0,
2907 Value => $NewOwnerObj->Id,
2909 TransactionType => $Type,
2910 CheckACL => 0, # don't check acl
2914 $RT::Handle->Rollback;
2915 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2918 ($val, $msg) = $self->_NewTransaction(
2921 NewValue => $NewOwnerObj->Id,
2922 OldValue => $OldOwnerObj->Id,
2927 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2928 $OldOwnerObj->Name, $NewOwnerObj->Name );
2931 $RT::Handle->Rollback();
2935 $RT::Handle->Commit();
2937 return ( $val, $msg );
2946 A convenince method to set the ticket's owner to the current user
2952 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2961 Convenience method to set the owner to 'nobody' if the current user is the owner.
2967 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2976 A convenience method to change the owner of the current ticket to the
2977 current user. Even if it's owned by another user.
2984 if ( $self->IsOwner( $self->CurrentUser ) ) {
2985 return ( 0, $self->loc("You already own this ticket") );
2988 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2998 # {{{ Routines dealing with status
3000 # {{{ sub ValidateStatus
3002 =head2 ValidateStatus STATUS
3004 Takes a string. Returns true if that status is a valid status for this ticket.
3005 Returns false otherwise.
3009 sub ValidateStatus {
3013 #Make sure the status passed in is valid
3014 unless ( $self->QueueObj->IsValidStatus($status) ) {
3026 =head2 SetStatus STATUS
3028 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3030 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.
3041 $args{Status} = shift;
3048 if ( $args{Status} eq 'deleted') {
3049 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3050 return ( 0, $self->loc('Permission Denied') );
3053 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3054 return ( 0, $self->loc('Permission Denied') );
3058 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3059 return (0, $self->loc('That ticket has unresolved dependencies'));
3062 my $now = RT::Date->new( $self->CurrentUser );
3065 #If we're changing the status from new, record that we've started
3066 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3068 #Set the Started time to "now"
3069 $self->_Set( Field => 'Started',
3071 RecordTransaction => 0 );
3074 #When we close a ticket, set the 'Resolved' attribute to now.
3075 # It's misnamed, but that's just historical.
3076 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3077 $self->_Set( Field => 'Resolved',
3079 RecordTransaction => 0 );
3082 #Actually update the status
3083 my ($val, $msg)= $self->_Set( Field => 'Status',
3084 Value => $args{Status},
3087 TransactionType => 'Status' );
3098 Takes no arguments. Marks this ticket for garbage collection
3104 return ( $self->SetStatus('deleted') );
3106 # TODO: garbage collection
3115 Sets this ticket's status to stalled
3121 return ( $self->SetStatus('stalled') );
3130 Sets this ticket's status to rejected
3136 return ( $self->SetStatus('rejected') );
3145 Sets this ticket\'s status to Open
3151 return ( $self->SetStatus('open') );
3160 Sets this ticket\'s status to Resolved
3166 return ( $self->SetStatus('resolved') );
3174 # {{{ Actions + Routines dealing with transactions
3176 # {{{ sub SetTold and _SetTold
3178 =head2 SetTold ISO [TIMETAKEN]
3180 Updates the told and records a transaction
3187 $told = shift if (@_);
3188 my $timetaken = shift || 0;
3190 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3191 return ( 0, $self->loc("Permission Denied") );
3194 my $datetold = new RT::Date( $self->CurrentUser );
3196 $datetold->Set( Format => 'iso',
3200 $datetold->SetToNow();
3203 return ( $self->_Set( Field => 'Told',
3204 Value => $datetold->ISO,
3205 TimeTaken => $timetaken,
3206 TransactionType => 'Told' ) );
3211 Updates the told without a transaction or acl check. Useful when we're sending replies.
3218 my $now = new RT::Date( $self->CurrentUser );
3221 #use __Set to get no ACLs ;)
3222 return ( $self->__Set( Field => 'Told',
3223 Value => $now->ISO ) );
3233 my $uid = $self->CurrentUser->id;
3234 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3235 return if $attr && $attr->Content gt $self->LastUpdated;
3237 my $txns = $self->Transactions;
3238 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3239 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3240 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3244 VALUE => $attr->Content
3246 $txns->RowsPerPage(1);
3247 return $txns->First;
3252 =head2 TransactionBatch
3254 Returns an array reference of all transactions created on this ticket during
3255 this ticket object's lifetime or since last application of a batch, or undef
3258 Only works when the C<UseTransactionBatch> config option is set to true.
3262 sub TransactionBatch {
3264 return $self->{_TransactionBatch};
3267 =head2 ApplyTransactionBatch
3269 Applies scrips on the current batch of transactions and shinks it. Usually
3270 batch is applied when object is destroyed, but in some cases it's too late.
3274 sub ApplyTransactionBatch {
3277 my $batch = $self->TransactionBatch;
3278 return unless $batch && @$batch;
3280 $self->_ApplyTransactionBatch;
3282 $self->{_TransactionBatch} = [];
3285 sub _ApplyTransactionBatch {
3287 my $batch = $self->TransactionBatch;
3290 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3293 RT::Scrips->new($RT::SystemUser)->Apply(
3294 Stage => 'TransactionBatch',
3296 TransactionObj => $batch->[0],
3300 # Entry point of the rule system
3301 my $rules = RT::Ruleset->FindAllRules(
3302 Stage => 'TransactionBatch',
3304 TransactionObj => $batch->[0],
3307 RT::Ruleset->CommitRules($rules);
3313 # DESTROY methods need to localize $@, or it may unset it. This
3314 # causes $m->abort to not bubble all of the way up. See perlbug
3315 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3318 # The following line eliminates reentrancy.
3319 # It protects against the fact that perl doesn't deal gracefully
3320 # when an object's refcount is changed in its destructor.
3321 return if $self->{_Destroyed}++;
3323 my $batch = $self->TransactionBatch;
3324 return unless $batch && @$batch;
3326 return $self->_ApplyTransactionBatch;
3331 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3333 # {{{ sub _OverlayAccessible
3335 sub _OverlayAccessible {
3337 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3338 Queue => { 'read' => 1, 'write' => 1 },
3339 Requestors => { 'read' => 1, 'write' => 1 },
3340 Owner => { 'read' => 1, 'write' => 1 },
3341 Subject => { 'read' => 1, 'write' => 1 },
3342 InitialPriority => { 'read' => 1, 'write' => 1 },
3343 FinalPriority => { 'read' => 1, 'write' => 1 },
3344 Priority => { 'read' => 1, 'write' => 1 },
3345 Status => { 'read' => 1, 'write' => 1 },
3346 TimeEstimated => { 'read' => 1, 'write' => 1 },
3347 TimeWorked => { 'read' => 1, 'write' => 1 },
3348 TimeLeft => { 'read' => 1, 'write' => 1 },
3349 Told => { 'read' => 1, 'write' => 1 },
3350 Resolved => { 'read' => 1 },
3351 Type => { 'read' => 1 },
3352 Starts => { 'read' => 1, 'write' => 1 },
3353 Started => { 'read' => 1, 'write' => 1 },
3354 Due => { 'read' => 1, 'write' => 1 },
3355 Creator => { 'read' => 1, 'auto' => 1 },
3356 Created => { 'read' => 1, 'auto' => 1 },
3357 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3358 LastUpdated => { 'read' => 1, 'auto' => 1 }
3370 my %args = ( Field => undef,
3373 RecordTransaction => 1,
3376 TransactionType => 'Set',
3379 if ($args{'CheckACL'}) {
3380 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3381 return ( 0, $self->loc("Permission Denied"));
3385 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3386 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3387 return(0, $self->loc("Internal Error"));
3390 #if the user is trying to modify the record
3392 #Take care of the old value we really don't want to get in an ACL loop.
3393 # so ask the super::_Value
3394 my $Old = $self->SUPER::_Value("$args{'Field'}");
3397 if ( $args{'UpdateTicket'} ) {
3400 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3401 Value => $args{'Value'} );
3403 #If we can't actually set the field to the value, don't record
3404 # a transaction. instead, get out of here.
3405 return ( 0, $msg ) unless $ret;
3408 if ( $args{'RecordTransaction'} == 1 ) {
3410 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3411 Type => $args{'TransactionType'},
3412 Field => $args{'Field'},
3413 NewValue => $args{'Value'},
3415 TimeTaken => $args{'TimeTaken'},
3417 return ( $Trans, scalar $TransObj->BriefDescription );
3420 return ( $ret, $msg );
3430 Takes the name of a table column.
3431 Returns its value as a string, if the user passes an ACL check
3440 #if the field is public, return it.
3441 if ( $self->_Accessible( $field, 'public' ) ) {
3443 #$RT::Logger->debug("Skipping ACL check for $field");
3444 return ( $self->SUPER::_Value($field) );
3448 #If the current user doesn't have ACLs, don't let em at it.
3450 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3453 return ( $self->SUPER::_Value($field) );
3459 # {{{ sub _UpdateTimeTaken
3461 =head2 _UpdateTimeTaken
3463 This routine will increment the timeworked counter. it should
3464 only be called from _NewTransaction
3468 sub _UpdateTimeTaken {
3470 my $Minutes = shift;
3473 $Total = $self->SUPER::_Value("TimeWorked");
3474 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3476 Field => "TimeWorked",
3487 # {{{ Routines dealing with ACCESS CONTROL
3489 # {{{ sub CurrentUserHasRight
3491 =head2 CurrentUserHasRight
3493 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3494 1 if the user has that right. It returns 0 if the user doesn't have that right.
3498 sub CurrentUserHasRight {
3502 return $self->CurrentUser->PrincipalObj->HasRight(
3514 Takes a paramhash with the attributes 'Right' and 'Principal'
3515 'Right' is a ticket-scoped textual right from RT::ACE
3516 'Principal' is an RT::User object
3518 Returns 1 if the principal has the right. Returns undef if not.
3530 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3532 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3533 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3538 $args{'Principal'}->HasRight(
3540 Right => $args{'Right'}
3551 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3552 It isn't acutally a searchbuilder collection itself.
3559 unless ($self->{'__reminders'}) {
3560 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3561 $self->{'__reminders'}->Ticket($self->id);
3563 return $self->{'__reminders'};
3569 # {{{ sub Transactions
3573 Returns an RT::Transactions object of all transactions on this ticket
3580 my $transactions = RT::Transactions->new( $self->CurrentUser );
3582 #If the user has no rights, return an empty object
3583 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3584 $transactions->LimitToTicket($self->id);
3586 # if the user may not see comments do not return them
3587 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3588 $transactions->Limit(
3594 $transactions->Limit(
3598 VALUE => "CommentEmailRecord",
3599 ENTRYAGGREGATOR => 'AND'
3604 $transactions->Limit(
3608 ENTRYAGGREGATOR => 'AND'
3612 return ($transactions);
3618 # {{{ TransactionCustomFields
3620 =head2 TransactionCustomFields
3622 Returns the custom fields that transactions on tickets will have.
3626 sub TransactionCustomFields {
3628 return $self->QueueObj->TicketTransactionCustomFields;
3633 # {{{ sub CustomFieldValues
3635 =head2 CustomFieldValues
3637 # Do name => id mapping (if needed) before falling back to
3638 # RT::Record's CustomFieldValues
3644 sub CustomFieldValues {
3648 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3650 my $cf = RT::CustomField->new( $self->CurrentUser );
3651 $cf->SetContextObject( $self );
3652 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3653 unless ( $cf->id ) {
3654 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3657 # If we didn't find a valid cfid, give up.
3658 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3660 return $self->SUPER::CustomFieldValues( $cf->id );
3665 # {{{ sub CustomFieldLookupType
3667 =head2 CustomFieldLookupType
3669 Returns the RT::Ticket lookup type, which can be passed to
3670 RT::CustomField->Create() via the 'LookupType' hash key.
3676 sub CustomFieldLookupType {
3677 "RT::Queue-RT::Ticket";
3680 =head2 ACLEquivalenceObjects
3682 This method returns a list of objects for which a user's rights also apply
3683 to this ticket. Generally, this is only the ticket's queue, but some RT
3684 extensions may make other objects available too.
3686 This method is called from L<RT::Principal/HasRight>.
3690 sub ACLEquivalenceObjects {
3692 return $self->QueueObj;
3701 Jesse Vincent, jesse@bestpractical.com