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 # {{{ Deal with auto-customer association
561 #unless we already have (a) customer(s)...
562 unless ( $self->Customers->Count ) {
564 #first find any requestors with emails but *without* customer targets
565 my @NoCust_Requestors =
566 grep { $_->EmailAddress && ! $_->Customers->Count }
567 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
569 for my $Requestor (@NoCust_Requestors) {
571 #perhaps the stuff in here should be in a User method??
573 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
575 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
577 ## false laziness w/RT/Interface/Web_Vendor.pm
578 my @link = ( 'Type' => 'MemberOf',
579 'Target' => "freeside://freeside/cust_main/$custnum",
582 my( $val, $msg ) = $Requestor->_AddLink(@link);
583 #XXX should do something with $msg# push @non_fatal_errors, $msg;
589 #find any requestors with customer targets
591 my %cust_target = ();
594 $RT::Logger->info( Dumper( $self->_Requestors ) );
595 $RT::Logger->info( Dumper( $self->_Requestors->UserMembersObj ) );
596 $RT::Logger->info( Dumper( $self->_Requestors->UserMembersObj->ItemsArrayRef ) );
599 grep { $_->Customers->Count }
600 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
602 foreach my $Requestor ( @Requestors ) {
603 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
604 $cust_target{ $cust_link->Target } = 1;
608 #and then auto-associate this ticket with those customers
610 foreach my $cust_target ( keys %cust_target ) {
612 my @link = ( 'Type' => 'MemberOf',
613 #'Target' => "freeside://freeside/cust_main/$custnum",
614 'Target' => $cust_target,
617 my( $val, $msg ) = $self->_AddLink(@link);
618 push @non_fatal_errors, $msg;
623 $RT::Logger->info( "ticket already has customer links; not auto-associating" );
629 # {{{ Add all the custom fields
631 foreach my $arg ( keys %args ) {
632 next unless $arg =~ /^CustomField-(\d+)$/i;
636 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
638 next unless defined $value && length $value;
640 # Allow passing in uploaded LargeContent etc by hash reference
641 my ($status, $msg) = $self->_AddCustomFieldValue(
642 (UNIVERSAL::isa( $value => 'HASH' )
647 RecordTransaction => 0,
649 push @non_fatal_errors, $msg unless $status;
655 # {{{ Deal with setting up links
657 # TODO: Adding link may fire scrips on other end and those scrips
658 # could create transactions on this ticket before 'Create' transaction.
660 # We should implement different schema: record 'Create' transaction,
661 # create links and only then fire create transaction's scrips.
663 # Ideal variant: add all links without firing scrips, record create
664 # transaction and only then fire scrips on the other ends of links.
668 foreach my $type ( keys %LINKTYPEMAP ) {
669 next unless ( defined $args{$type} );
671 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
673 # Check rights on the other end of the link if we must
674 # then run _AddLink that doesn't check for ACLs
675 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
676 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
678 push @non_fatal_errors, $msg;
681 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
682 push @non_fatal_errors, $self->loc('Linking. Permission denied');
687 my ( $wval, $wmsg ) = $self->_AddLink(
688 Type => $LINKTYPEMAP{$type}->{'Type'},
689 $LINKTYPEMAP{$type}->{'Mode'} => $link,
690 Silent => !$args{'_RecordTransaction'},
691 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
695 push @non_fatal_errors, $wmsg unless ($wval);
700 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
701 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
703 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
705 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
706 . ") was proposed as a ticket owner but has no rights to own "
707 . "tickets in " . $QueueObj->Name );
708 push @non_fatal_errors, $self->loc(
709 "Owner '[_1]' does not have rights to own this ticket.",
713 $Owner = $DeferOwner;
714 $self->__Set(Field => 'Owner', Value => $Owner->id);
716 $self->OwnerGroup->_AddMember(
717 PrincipalId => $Owner->PrincipalId,
718 InsideTransaction => 1
722 if ( $args{'_RecordTransaction'} ) {
724 # {{{ Add a transaction for the create
725 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
727 TimeTaken => $args{'TimeWorked'},
728 MIMEObj => $args{'MIMEObj'},
729 CommitScrips => !$args{'DryRun'},
732 if ( $self->Id && $Trans ) {
734 $TransObj->UpdateCustomFields(ARGSRef => \%args);
736 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
737 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
738 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
741 $RT::Handle->Rollback();
743 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
744 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
745 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
748 if ( $args{'DryRun'} ) {
749 $RT::Handle->Rollback();
750 return ($self->id, $TransObj, $ErrStr);
752 $RT::Handle->Commit();
753 return ( $self->Id, $TransObj->Id, $ErrStr );
759 # Not going to record a transaction
760 $RT::Handle->Commit();
761 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
762 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
763 return ( $self->Id, 0, $ErrStr );
771 # {{{ _Parse822HeadersForAttributes Content
773 =head2 _Parse822HeadersForAttributes Content
775 Takes an RFC822 style message and parses its attributes into a hash.
779 sub _Parse822HeadersForAttributes {
784 my @lines = ( split ( /\n/, $content ) );
785 while ( defined( my $line = shift @lines ) ) {
786 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
791 if ( defined( $args{$tag} ) )
792 { #if we're about to get a second value, make it an array
793 $args{$tag} = [ $args{$tag} ];
795 if ( ref( $args{$tag} ) )
796 { #If it's an array, we want to push the value
797 push @{ $args{$tag} }, $value;
799 else { #if there's nothing there, just set the value
800 $args{$tag} = $value;
802 } elsif ($line =~ /^$/) {
804 #TODO: this won't work, since "" isn't of the form "foo:value"
806 while ( defined( my $l = shift @lines ) ) {
807 push @{ $args{'content'} }, $l;
813 foreach my $date qw(due starts started resolved) {
814 my $dateobj = RT::Date->new($RT::SystemUser);
815 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
816 $dateobj->Set( Format => 'unix', Value => $args{$date} );
819 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
821 $args{$date} = $dateobj->ISO;
823 $args{'mimeobj'} = MIME::Entity->new();
824 $args{'mimeobj'}->build(
825 Type => ( $args{'contenttype'} || 'text/plain' ),
826 Data => ($args{'content'} || '')
836 =head2 Import PARAMHASH
839 Doesn\'t create a transaction.
840 Doesn\'t supply queue defaults, etc.
848 my ( $ErrStr, $QueueObj, $Owner );
852 EffectiveId => undef,
856 Owner => $RT::Nobody->Id,
857 Subject => '[no subject]',
858 InitialPriority => undef,
859 FinalPriority => undef,
870 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
871 $QueueObj = RT::Queue->new($RT::SystemUser);
872 $QueueObj->Load( $args{'Queue'} );
874 #TODO error check this and return 0 if it\'s not loading properly +++
876 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
877 $QueueObj = RT::Queue->new($RT::SystemUser);
878 $QueueObj->Load( $args{'Queue'}->Id );
882 "$self " . $args{'Queue'} . " not a recognised queue object." );
885 #Can't create a ticket without a queue.
886 unless ( defined($QueueObj) and $QueueObj->Id ) {
887 $RT::Logger->debug("$self No queue given for ticket creation.");
888 return ( 0, $self->loc('Could not create ticket. Queue not set') );
891 #Now that we have a queue, Check the ACLS
893 $self->CurrentUser->HasRight(
894 Right => 'CreateTicket',
900 $self->loc("No permission to create tickets in the queue '[_1]'"
904 # {{{ Deal with setting the owner
906 # Attempt to take user object, user name or user id.
907 # Assign to nobody if lookup fails.
908 if ( defined( $args{'Owner'} ) ) {
909 if ( ref( $args{'Owner'} ) ) {
910 $Owner = $args{'Owner'};
913 $Owner = new RT::User( $self->CurrentUser );
914 $Owner->Load( $args{'Owner'} );
915 if ( !defined( $Owner->id ) ) {
916 $Owner->Load( $RT::Nobody->id );
921 #If we have a proposed owner and they don't have the right
922 #to own a ticket, scream about it and make them not the owner
925 and ( $Owner->Id != $RT::Nobody->Id )
935 $RT::Logger->warning( "$self user "
939 . "as a ticket owner but has no rights to own "
941 . $QueueObj->Name . "'" );
946 #If we haven't been handed a valid owner, make it nobody.
947 unless ( defined($Owner) ) {
948 $Owner = new RT::User( $self->CurrentUser );
949 $Owner->Load( $RT::Nobody->UserObj->Id );
954 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
955 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
958 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
959 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
960 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
961 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
963 # If we're coming in with an id, set that now.
964 my $EffectiveId = undef;
966 $EffectiveId = $args{'id'};
970 my $id = $self->SUPER::Create(
972 EffectiveId => $EffectiveId,
973 Queue => $QueueObj->Id,
975 Subject => $args{'Subject'}, # loc
976 InitialPriority => $args{'InitialPriority'}, # loc
977 FinalPriority => $args{'FinalPriority'}, # loc
978 Priority => $args{'InitialPriority'}, # loc
979 Status => $args{'Status'}, # loc
980 TimeWorked => $args{'TimeWorked'}, # loc
981 Type => $args{'Type'}, # loc
982 Created => $args{'Created'}, # loc
983 Told => $args{'Told'}, # loc
984 LastUpdated => $args{'Updated'}, # loc
985 Resolved => $args{'Resolved'}, # loc
986 Due => $args{'Due'}, # loc
989 # If the ticket didn't have an id
990 # Set the ticket's effective ID now that we've created it.
992 $self->Load( $args{'id'} );
996 $self->__Set( Field => 'EffectiveId', Value => $id );
1000 $self . "->Import couldn't set EffectiveId: $msg" );
1004 my $create_groups_ret = $self->_CreateTicketGroups();
1005 unless ($create_groups_ret) {
1007 "Couldn't create ticket groups for ticket " . $self->Id );
1010 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1013 foreach $watcher ( @{ $args{'Cc'} } ) {
1014 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1016 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1017 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1020 foreach $watcher ( @{ $args{'Requestor'} } ) {
1021 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1025 return ( $self->Id, $ErrStr );
1030 # {{{ Routines dealing with watchers.
1032 # {{{ _CreateTicketGroups
1034 =head2 _CreateTicketGroups
1036 Create the ticket groups and links for this ticket.
1037 This routine expects to be called from Ticket->Create _inside of a transaction_
1039 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1041 It will return true on success and undef on failure.
1047 sub _CreateTicketGroups {
1050 my @types = qw(Requestor Owner Cc AdminCc);
1052 foreach my $type (@types) {
1053 my $type_obj = RT::Group->new($self->CurrentUser);
1054 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1055 Instance => $self->Id,
1058 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1059 $self->Id.": ".$msg);
1069 # {{{ sub OwnerGroup
1073 A constructor which returns an RT::Group object containing the owner of this ticket.
1079 my $owner_obj = RT::Group->new($self->CurrentUser);
1080 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1081 return ($owner_obj);
1087 # {{{ sub AddWatcher
1091 AddWatcher takes a parameter hash. The keys are as follows:
1093 Type One of Requestor, Cc, AdminCc
1095 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1097 Email The email address of the new watcher. If a user with this
1098 email address can't be found, a new nonprivileged user will be created.
1100 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.
1108 PrincipalId => undef,
1113 # ModifyTicket works in any case
1114 return $self->_AddWatcher( %args )
1115 if $self->CurrentUserHasRight('ModifyTicket');
1116 if ( $args{'Email'} ) {
1117 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1118 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1121 if ( lc $self->CurrentUser->UserObj->EmailAddress
1122 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1124 $args{'PrincipalId'} = $self->CurrentUser->id;
1125 delete $args{'Email'};
1129 # If the watcher isn't the current user then the current user has no right
1131 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1132 return ( 0, $self->loc("Permission Denied") );
1135 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1136 if ( $args{'Type'} eq 'AdminCc' ) {
1137 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1138 return ( 0, $self->loc('Permission Denied') );
1142 # If it's a Requestor or Cc and they don't have 'Watch', bail
1143 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1144 unless ( $self->CurrentUserHasRight('Watch') ) {
1145 return ( 0, $self->loc('Permission Denied') );
1149 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1150 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1153 return $self->_AddWatcher( %args );
1156 #This contains the meat of AddWatcher. but can be called from a routine like
1157 # Create, which doesn't need the additional acl check
1163 PrincipalId => undef,
1169 my $principal = RT::Principal->new($self->CurrentUser);
1170 if ($args{'Email'}) {
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'});
1180 # If we can't find this watcher, we need to bail.
1181 unless ($principal->Id) {
1182 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1183 return(0, $self->loc("Could not find or create that user"));
1187 my $group = RT::Group->new($self->CurrentUser);
1188 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1189 unless ($group->id) {
1190 return(0,$self->loc("Group not found"));
1193 if ( $group->HasMember( $principal)) {
1195 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1199 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1200 InsideTransaction => 1 );
1202 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1204 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1207 unless ( $args{'Silent'} ) {
1208 $self->_NewTransaction(
1209 Type => 'AddWatcher',
1210 NewValue => $principal->Id,
1211 Field => $args{'Type'}
1215 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1221 # {{{ sub DeleteWatcher
1223 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1226 Deletes a Ticket watcher. Takes two arguments:
1228 Type (one of Requestor,Cc,AdminCc)
1232 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1234 Email (the email address of an existing wathcer)
1243 my %args = ( Type => undef,
1244 PrincipalId => undef,
1248 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1249 return ( 0, $self->loc("No principal specified") );
1251 my $principal = RT::Principal->new( $self->CurrentUser );
1252 if ( $args{'PrincipalId'} ) {
1254 $principal->Load( $args{'PrincipalId'} );
1257 my $user = RT::User->new( $self->CurrentUser );
1258 $user->LoadByEmail( $args{'Email'} );
1259 $principal->Load( $user->Id );
1262 # If we can't find this watcher, we need to bail.
1263 unless ( $principal->Id ) {
1264 return ( 0, $self->loc("Could not find that principal") );
1267 my $group = RT::Group->new( $self->CurrentUser );
1268 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1269 unless ( $group->id ) {
1270 return ( 0, $self->loc("Group not found") );
1274 #If the watcher we're trying to add is for the current user
1275 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1277 # If it's an AdminCc and they don't have
1278 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1279 if ( $args{'Type'} eq 'AdminCc' ) {
1280 unless ( $self->CurrentUserHasRight('ModifyTicket')
1281 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1282 return ( 0, $self->loc('Permission Denied') );
1286 # If it's a Requestor or Cc and they don't have
1287 # 'Watch' or 'ModifyTicket', bail
1288 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1290 unless ( $self->CurrentUserHasRight('ModifyTicket')
1291 or $self->CurrentUserHasRight('Watch') ) {
1292 return ( 0, $self->loc('Permission Denied') );
1296 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1298 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1302 # If the watcher isn't the current user
1303 # and the current user doesn't have 'ModifyTicket' bail
1305 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1306 return ( 0, $self->loc("Permission Denied") );
1312 # see if this user is already a watcher.
1314 unless ( $group->HasMember($principal) ) {
1316 $self->loc( 'That principal is not a [_1] for this ticket',
1320 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1322 $RT::Logger->error( "Failed to delete "
1324 . " as a member of group "
1330 'Could not remove that principal as a [_1] for this ticket',
1334 unless ( $args{'Silent'} ) {
1335 $self->_NewTransaction( Type => 'DelWatcher',
1336 OldValue => $principal->Id,
1337 Field => $args{'Type'} );
1341 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1342 $principal->Object->Name,
1351 =head2 SquelchMailTo [EMAIL]
1353 Takes an optional email address to never email about updates to this ticket.
1356 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1364 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1368 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1373 return $self->_SquelchMailTo(@_);
1376 sub _SquelchMailTo {
1380 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1381 unless grep { $_->Content eq $attr }
1382 $self->Attributes->Named('SquelchMailTo');
1384 my @attributes = $self->Attributes->Named('SquelchMailTo');
1385 return (@attributes);
1389 =head2 UnsquelchMailTo ADDRESS
1391 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1393 Returns a tuple of (status, message)
1397 sub UnsquelchMailTo {
1400 my $address = shift;
1401 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1402 return ( 0, $self->loc("Permission Denied") );
1405 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1406 return ($val, $msg);
1410 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1412 =head2 RequestorAddresses
1414 B<Returns> String: All Ticket Requestor email addresses as a string.
1418 sub RequestorAddresses {
1421 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1425 return ( $self->Requestors->MemberEmailAddressesAsString );
1429 =head2 AdminCcAddresses
1431 returns String: All Ticket AdminCc email addresses as a string
1435 sub AdminCcAddresses {
1438 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1442 return ( $self->AdminCc->MemberEmailAddressesAsString )
1448 returns String: All Ticket Ccs as a string of email addresses
1455 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1458 return ( $self->Cc->MemberEmailAddressesAsString);
1464 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1466 # {{{ sub Requestors
1471 Returns this ticket's Requestors as an RT::Group object
1478 my $group = RT::Group->new($self->CurrentUser);
1479 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1480 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1488 # {{{ sub _Requestors
1492 Private non-ACLed variant of Reqeustors so that we can look them up for the
1493 purposes of customer auto-association during create.
1500 my $group = RT::Group->new($RT::SystemUser);
1501 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1512 Returns an RT::Group object which contains this ticket's Ccs.
1513 If the user doesn't have "ShowTicket" permission, returns an empty group
1520 my $group = RT::Group->new($self->CurrentUser);
1521 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1522 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1535 Returns an RT::Group object which contains this ticket's AdminCcs.
1536 If the user doesn't have "ShowTicket" permission, returns an empty group
1543 my $group = RT::Group->new($self->CurrentUser);
1544 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1545 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1555 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1558 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1560 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1562 Takes a param hash with the attributes Type and either PrincipalId or Email
1564 Type is one of Requestor, Cc, AdminCc and Owner
1566 PrincipalId is an RT::Principal id, and Email is an email address.
1568 Returns true if the specified principal (or the one corresponding to the
1569 specified address) is a member of the group Type for this ticket.
1571 XX TODO: This should be Memoized.
1578 my %args = ( Type => 'Requestor',
1579 PrincipalId => undef,
1584 # Load the relevant group.
1585 my $group = RT::Group->new($self->CurrentUser);
1586 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1588 # Find the relevant principal.
1589 if (!$args{PrincipalId} && $args{Email}) {
1590 # Look up the specified user.
1591 my $user = RT::User->new($self->CurrentUser);
1592 $user->LoadByEmail($args{Email});
1594 $args{PrincipalId} = $user->PrincipalId;
1597 # A non-existent user can't be a group member.
1602 # Ask if it has the member in question
1603 return $group->HasMember( $args{'PrincipalId'} );
1608 # {{{ sub IsRequestor
1610 =head2 IsRequestor PRINCIPAL_ID
1612 Takes an L<RT::Principal> id.
1614 Returns true if the principal is a requestor of the current ticket.
1622 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1630 =head2 IsCc PRINCIPAL_ID
1632 Takes an RT::Principal id.
1633 Returns true if the principal is a Cc of the current ticket.
1642 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1650 =head2 IsAdminCc PRINCIPAL_ID
1652 Takes an RT::Principal id.
1653 Returns true if the principal is an AdminCc of the current ticket.
1661 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1671 Takes an RT::User object. Returns true if that user is this ticket's owner.
1672 returns undef otherwise
1680 # no ACL check since this is used in acl decisions
1681 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1685 #Tickets won't yet have owners when they're being created.
1686 unless ( $self->OwnerObj->id ) {
1690 if ( $person->id == $self->OwnerObj->id ) {
1705 =head2 TransactionAddresses
1707 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for all this ticket's Create, Comment or Correspond transactions.
1708 The keys are C<To>, C<Cc> and C<Bcc>. The values are lists of C<Email::Address> objects.
1710 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.
1715 sub TransactionAddresses {
1717 my $txns = $self->Transactions;
1720 foreach my $type (qw(Create Comment Correspond)) {
1721 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1724 while (my $txn = $txns->Next) {
1725 my $txnaddrs = $txn->Addresses;
1726 foreach my $addrlist ( values %$txnaddrs ) {
1727 foreach my $addr (@$addrlist) {
1728 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1729 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1730 # skips "comment-only" addresses
1731 next unless ($addr->address);
1732 $addresses{$addr->address} = $addr;
1744 # {{{ Routines dealing with queues
1746 # {{{ sub ValidateQueue
1753 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1757 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1758 my $id = $QueueObj->Load($Value);
1774 my $NewQueue = shift;
1776 #Redundant. ACL gets checked in _Set;
1777 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1778 return ( 0, $self->loc("Permission Denied") );
1781 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1782 $NewQueueObj->Load($NewQueue);
1784 unless ( $NewQueueObj->Id() ) {
1785 return ( 0, $self->loc("That queue does not exist") );
1788 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1789 return ( 0, $self->loc('That is the same value') );
1792 $self->CurrentUser->HasRight(
1793 Right => 'CreateTicket',
1794 Object => $NewQueueObj
1798 return ( 0, $self->loc("You may not create requests in that queue.") );
1802 $self->OwnerObj->HasRight(
1803 Right => 'OwnTicket',
1804 Object => $NewQueueObj
1808 my $clone = RT::Ticket->new( $RT::SystemUser );
1809 $clone->Load( $self->Id );
1810 unless ( $clone->Id ) {
1811 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1813 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1814 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1817 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1820 # On queue change, change queue for reminders too
1821 my $reminder_collection = $self->Reminders->Collection;
1822 while ( my $reminder = $reminder_collection->Next ) {
1823 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1824 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1828 return ($status, $msg);
1837 Takes nothing. returns this ticket's queue object
1844 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1846 #We call __Value so that we can avoid the ACL decision and some deep recursion
1847 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1848 return ($queue_obj);
1855 # {{{ Date printing routines
1861 Returns an RT::Date object containing this ticket's due date
1868 my $time = new RT::Date( $self->CurrentUser );
1870 # -1 is RT::Date slang for never
1871 if ( my $due = $self->Due ) {
1872 $time->Set( Format => 'sql', Value => $due );
1875 $time->Set( Format => 'unix', Value => -1 );
1883 # {{{ sub DueAsString
1887 Returns this ticket's due date as a human readable string
1893 return $self->DueObj->AsString();
1898 # {{{ sub ResolvedObj
1902 Returns an RT::Date object of this ticket's 'resolved' time.
1909 my $time = new RT::Date( $self->CurrentUser );
1910 $time->Set( Format => 'sql', Value => $self->Resolved );
1916 # {{{ sub SetStarted
1920 Takes a date in ISO format or undef
1921 Returns a transaction id and a message
1922 The client calls "Start" to note that the project was started on the date in $date.
1923 A null date means "now"
1929 my $time = shift || 0;
1931 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1932 return ( 0, $self->loc("Permission Denied") );
1935 #We create a date object to catch date weirdness
1936 my $time_obj = new RT::Date( $self->CurrentUser() );
1938 $time_obj->Set( Format => 'ISO', Value => $time );
1941 $time_obj->SetToNow();
1944 #Now that we're starting, open this ticket
1945 #TODO do we really want to force this as policy? it should be a scrip
1947 #We need $TicketAsSystem, in case the current user doesn't have
1950 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1951 $TicketAsSystem->Load( $self->Id );
1952 if ( $TicketAsSystem->Status eq 'new' ) {
1953 $TicketAsSystem->Open();
1956 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1962 # {{{ sub StartedObj
1966 Returns an RT::Date object which contains this ticket's
1974 my $time = new RT::Date( $self->CurrentUser );
1975 $time->Set( Format => 'sql', Value => $self->Started );
1985 Returns an RT::Date object which contains this ticket's
1993 my $time = new RT::Date( $self->CurrentUser );
1994 $time->Set( Format => 'sql', Value => $self->Starts );
2004 Returns an RT::Date object which contains this ticket's
2012 my $time = new RT::Date( $self->CurrentUser );
2013 $time->Set( Format => 'sql', Value => $self->Told );
2019 # {{{ sub ToldAsString
2023 A convenience method that returns ToldObj->AsString
2025 TODO: This should be deprecated
2031 if ( $self->Told ) {
2032 return $self->ToldObj->AsString();
2041 # {{{ sub TimeWorkedAsString
2043 =head2 TimeWorkedAsString
2045 Returns the amount of time worked on this ticket as a Text String
2049 sub TimeWorkedAsString {
2051 my $value = $self->TimeWorked;
2053 # return the # of minutes worked turned into seconds and written as
2054 # a simple text string, this is not really a date object, but if we
2055 # diff a number of seconds vs the epoch, we'll get a nice description
2057 return "" unless $value;
2058 return RT::Date->new( $self->CurrentUser )
2059 ->DurationAsString( $value * 60 );
2064 # {{{ sub TimeLeftAsString
2066 =head2 TimeLeftAsString
2068 Returns the amount of time left on this ticket as a Text String
2072 sub TimeLeftAsString {
2074 my $value = $self->TimeLeft;
2075 return "" unless $value;
2076 return RT::Date->new( $self->CurrentUser )
2077 ->DurationAsString( $value * 60 );
2082 # {{{ Routines dealing with correspondence/comments
2088 Comment on this ticket.
2089 Takes a hash with the following attributes:
2090 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2093 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2095 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2096 They will, however, be prepared and you'll be able to access them through the TransactionObj
2098 Returns: Transaction id, Error Message, Transaction Object
2099 (note the different order from Create()!)
2106 my %args = ( CcMessageTo => undef,
2107 BccMessageTo => undef,
2114 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2115 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2116 return ( 0, $self->loc("Permission Denied"), undef );
2118 $args{'NoteType'} = 'Comment';
2120 if ($args{'DryRun'}) {
2121 $RT::Handle->BeginTransaction();
2122 $args{'CommitScrips'} = 0;
2125 my @results = $self->_RecordNote(%args);
2126 if ($args{'DryRun'}) {
2127 $RT::Handle->Rollback();
2134 # {{{ sub Correspond
2138 Correspond on this ticket.
2139 Takes a hashref with the following attributes:
2142 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2144 if there's no MIMEObj, Content is used to build a MIME::Entity object
2146 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2147 They will, however, be prepared and you'll be able to access them through the TransactionObj
2149 Returns: Transaction id, Error Message, Transaction Object
2150 (note the different order from Create()!)
2157 my %args = ( CcMessageTo => undef,
2158 BccMessageTo => undef,
2164 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2165 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2166 return ( 0, $self->loc("Permission Denied"), undef );
2169 $args{'NoteType'} = 'Correspond';
2170 if ($args{'DryRun'}) {
2171 $RT::Handle->BeginTransaction();
2172 $args{'CommitScrips'} = 0;
2175 my @results = $self->_RecordNote(%args);
2177 #Set the last told date to now if this isn't mail from the requestor.
2178 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2179 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2181 if ($args{'DryRun'}) {
2182 $RT::Handle->Rollback();
2191 # {{{ sub _RecordNote
2195 the meat of both comment and correspond.
2197 Performs no access control checks. hence, dangerous.
2204 CcMessageTo => undef,
2205 BccMessageTo => undef,
2210 NoteType => 'Correspond',
2216 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2217 return ( 0, $self->loc("No message attached"), undef );
2220 unless ( $args{'MIMEObj'} ) {
2221 $args{'MIMEObj'} = MIME::Entity->build(
2222 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2226 # convert text parts into utf-8
2227 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2229 # If we've been passed in CcMessageTo and BccMessageTo fields,
2230 # add them to the mime object for passing on to the transaction handler
2231 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2232 # RT-Send-Bcc: headers
2235 foreach my $type (qw/Cc Bcc/) {
2236 if ( defined $args{ $type . 'MessageTo' } ) {
2238 my $addresses = join ', ', (
2239 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2240 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2241 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2245 foreach my $argument (qw(Encrypt Sign)) {
2246 $args{'MIMEObj'}->head->add(
2247 "X-RT-$argument" => $args{ $argument }
2248 ) if defined $args{ $argument };
2251 # If this is from an external source, we need to come up with its
2252 # internal Message-ID now, so all emails sent because of this
2253 # message have a common Message-ID
2254 my $org = RT->Config->Get('Organization');
2255 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2256 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2257 $args{'MIMEObj'}->head->set(
2258 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2262 #Record the correspondence (write the transaction)
2263 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2264 Type => $args{'NoteType'},
2265 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2266 TimeTaken => $args{'TimeTaken'},
2267 MIMEObj => $args{'MIMEObj'},
2268 CommitScrips => $args{'CommitScrips'},
2272 $RT::Logger->err("$self couldn't init a transaction $msg");
2273 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2276 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2288 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2291 my $type = shift || "";
2293 unless ( $self->{"$field$type"} ) {
2294 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2296 #not sure what this ACL was supposed to do... but returning the
2297 # bare (unlimited) RT::Links certainly seems wrong, it causes the
2298 # $Ticket->Customers method during creation to return results for every
2300 #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2302 # Maybe this ticket is a merged ticket
2303 my $Tickets = new RT::Tickets( $self->CurrentUser );
2304 # at least to myself
2305 $self->{"$field$type"}->Limit( FIELD => $field,
2306 VALUE => $self->URI,
2307 ENTRYAGGREGATOR => 'OR' );
2308 $Tickets->Limit( FIELD => 'EffectiveId',
2309 VALUE => $self->EffectiveId );
2310 while (my $Ticket = $Tickets->Next) {
2311 $self->{"$field$type"}->Limit( FIELD => $field,
2312 VALUE => $Ticket->URI,
2313 ENTRYAGGREGATOR => 'OR' );
2315 $self->{"$field$type"}->Limit( FIELD => 'Type',
2320 return ( $self->{"$field$type"} );
2325 # {{{ sub DeleteLink
2329 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2330 SilentBase and SilentTarget. Either Base or Target must be null.
2331 The null value will be replaced with this ticket\'s id.
2333 If Silent is true then no transaction would be recorded, in other
2334 case you can control creation of transactions on both base and
2335 target with SilentBase and SilentTarget respectively. By default
2336 both transactions are created.
2347 SilentBase => undef,
2348 SilentTarget => undef,
2352 unless ( $args{'Target'} || $args{'Base'} ) {
2353 $RT::Logger->error("Base or Target must be specified");
2354 return ( 0, $self->loc('Either base or target must be specified') );
2359 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2360 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2361 return ( 0, $self->loc("Permission Denied") );
2364 # If the other URI is an RT::Ticket, we want to make sure the user
2365 # can modify it too...
2366 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2367 return (0, $msg) unless $status;
2368 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2371 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2372 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2374 return ( 0, $self->loc("Permission Denied") );
2377 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2378 return ( 0, $Msg ) unless $val;
2380 return ( $val, $Msg ) if $args{'Silent'};
2382 my ($direction, $remote_link);
2384 if ( $args{'Base'} ) {
2385 $remote_link = $args{'Base'};
2386 $direction = 'Target';
2388 elsif ( $args{'Target'} ) {
2389 $remote_link = $args{'Target'};
2390 $direction = 'Base';
2393 my $remote_uri = RT::URI->new( $self->CurrentUser );
2394 $remote_uri->FromURI( $remote_link );
2396 unless ( $args{ 'Silent'. $direction } ) {
2397 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2398 Type => 'DeleteLink',
2399 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2400 OldValue => $remote_uri->URI || $remote_link,
2403 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2406 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2407 my $OtherObj = $remote_uri->Object;
2408 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2409 Type => 'DeleteLink',
2410 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2411 : $LINKDIRMAP{$args{'Type'}}->{Target},
2412 OldValue => $self->URI,
2413 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2416 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2419 return ( $val, $Msg );
2428 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2430 If Silent is true then no transaction would be recorded, in other
2431 case you can control creation of transactions on both base and
2432 target with SilentBase and SilentTarget respectively. By default
2433 both transactions are created.
2439 my %args = ( Target => '',
2443 SilentBase => undef,
2444 SilentTarget => undef,
2447 unless ( $args{'Target'} || $args{'Base'} ) {
2448 $RT::Logger->error("Base or Target must be specified");
2449 return ( 0, $self->loc('Either base or target must be specified') );
2453 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2454 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2455 return ( 0, $self->loc("Permission Denied") );
2458 # If the other URI is an RT::Ticket, we want to make sure the user
2459 # can modify it too...
2460 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2461 return (0, $msg) unless $status;
2462 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2465 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2466 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2468 return ( 0, $self->loc("Permission Denied") );
2471 return $self->_AddLink(%args);
2474 sub __GetTicketFromURI {
2476 my %args = ( URI => '', @_ );
2478 # If the other URI is an RT::Ticket, we want to make sure the user
2479 # can modify it too...
2480 my $uri_obj = RT::URI->new( $self->CurrentUser );
2481 $uri_obj->FromURI( $args{'URI'} );
2483 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2484 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2485 $RT::Logger->warning( $msg );
2488 my $obj = $uri_obj->Resolver->Object;
2489 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2490 return (1, 'Found not a ticket', undef);
2492 return (1, 'Found ticket', $obj);
2497 Private non-acled variant of AddLink so that links can be added during create.
2503 my %args = ( Target => '',
2507 SilentBase => undef,
2508 SilentTarget => undef,
2511 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2512 return ($val, $msg) if !$val || $exist;
2513 return ($val, $msg) if $args{'Silent'};
2515 my ($direction, $remote_link);
2516 if ( $args{'Target'} ) {
2517 $remote_link = $args{'Target'};
2518 $direction = 'Base';
2519 } elsif ( $args{'Base'} ) {
2520 $remote_link = $args{'Base'};
2521 $direction = 'Target';
2524 my $remote_uri = RT::URI->new( $self->CurrentUser );
2525 $remote_uri->FromURI( $remote_link );
2527 unless ( $args{ 'Silent'. $direction } ) {
2528 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2530 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2531 NewValue => $remote_uri->URI || $remote_link,
2534 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2537 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2538 my $OtherObj = $remote_uri->Object;
2539 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2541 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2542 : $LINKDIRMAP{$args{'Type'}}->{Target},
2543 NewValue => $self->URI,
2544 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2547 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2550 return ( $val, $msg );
2560 MergeInto take the id of the ticket to merge this ticket into.
2568 my $ticket_id = shift;
2570 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2571 return ( 0, $self->loc("Permission Denied") );
2574 # Load up the new ticket.
2575 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2576 $MergeInto->Load($ticket_id);
2578 # make sure it exists.
2579 unless ( $MergeInto->Id ) {
2580 return ( 0, $self->loc("New ticket doesn't exist") );
2583 # Make sure the current user can modify the new ticket.
2584 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2585 return ( 0, $self->loc("Permission Denied") );
2588 $RT::Handle->BeginTransaction();
2590 # We use EffectiveId here even though it duplicates information from
2591 # the links table becasue of the massive performance hit we'd take
2592 # by trying to do a separate database query for merge info everytime
2595 #update this ticket's effective id to the new ticket's id.
2596 my ( $id_val, $id_msg ) = $self->__Set(
2597 Field => 'EffectiveId',
2598 Value => $MergeInto->Id()
2602 $RT::Handle->Rollback();
2603 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2607 if ( $self->__Value('Status') ne 'resolved' ) {
2609 my ( $status_val, $status_msg )
2610 = $self->__Set( Field => 'Status', Value => 'resolved' );
2612 unless ($status_val) {
2613 $RT::Handle->Rollback();
2616 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2620 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2624 # update all the links that point to that old ticket
2625 my $old_links_to = RT::Links->new($self->CurrentUser);
2626 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2629 while (my $link = $old_links_to->Next) {
2630 if (exists $old_seen{$link->Base."-".$link->Type}) {
2633 elsif ($link->Base eq $MergeInto->URI) {
2636 # First, make sure the link doesn't already exist. then move it over.
2637 my $tmp = RT::Link->new($RT::SystemUser);
2638 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2642 $link->SetTarget($MergeInto->URI);
2643 $link->SetLocalTarget($MergeInto->id);
2645 $old_seen{$link->Base."-".$link->Type} =1;
2650 my $old_links_from = RT::Links->new($self->CurrentUser);
2651 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2653 while (my $link = $old_links_from->Next) {
2654 if (exists $old_seen{$link->Type."-".$link->Target}) {
2657 if ($link->Target eq $MergeInto->URI) {
2660 # First, make sure the link doesn't already exist. then move it over.
2661 my $tmp = RT::Link->new($RT::SystemUser);
2662 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2666 $link->SetBase($MergeInto->URI);
2667 $link->SetLocalBase($MergeInto->id);
2668 $old_seen{$link->Type."-".$link->Target} =1;
2674 # Update time fields
2675 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2677 my $mutator = "Set$type";
2678 $MergeInto->$mutator(
2679 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2682 #add all of this ticket's watchers to that ticket.
2683 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2685 my $people = $self->$watcher_type->MembersObj;
2686 my $addwatcher_type = $watcher_type;
2687 $addwatcher_type =~ s/s$//;
2689 while ( my $watcher = $people->Next ) {
2691 my ($val, $msg) = $MergeInto->_AddWatcher(
2692 Type => $addwatcher_type,
2694 PrincipalId => $watcher->MemberId
2697 $RT::Logger->warning($msg);
2703 #find all of the tickets that were merged into this ticket.
2704 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2705 $old_mergees->Limit(
2706 FIELD => 'EffectiveId',
2711 # update their EffectiveId fields to the new ticket's id
2712 while ( my $ticket = $old_mergees->Next() ) {
2713 my ( $val, $msg ) = $ticket->__Set(
2714 Field => 'EffectiveId',
2715 Value => $MergeInto->Id()
2719 #make a new link: this ticket is merged into that other ticket.
2720 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2722 $MergeInto->_SetLastUpdated;
2724 $RT::Handle->Commit();
2725 return ( 1, $self->loc("Merge Successful") );
2730 Returns list of tickets' ids that's been merged into this ticket.
2737 my $mergees = new RT::Tickets( $self->CurrentUser );
2739 FIELD => 'EffectiveId',
2748 return map $_->id, @{ $mergees->ItemsArrayRef || [] };
2755 # {{{ Routines dealing with ownership
2761 Takes nothing and returns an RT::User object of
2769 #If this gets ACLed, we lose on a rights check in User.pm and
2770 #get deep recursion. if we need ACLs here, we need
2771 #an equiv without ACLs
2773 my $owner = new RT::User( $self->CurrentUser );
2774 $owner->Load( $self->__Value('Owner') );
2776 #Return the owner object
2782 # {{{ sub OwnerAsString
2784 =head2 OwnerAsString
2786 Returns the owner's email address
2792 return ( $self->OwnerObj->EmailAddress );
2802 Takes two arguments:
2803 the Id or Name of the owner
2804 and (optionally) the type of the SetOwner Transaction. It defaults
2805 to 'Give'. 'Steal' is also a valid option.
2812 my $NewOwner = shift;
2813 my $Type = shift || "Give";
2815 $RT::Handle->BeginTransaction();
2817 $self->_SetLastUpdated(); # lock the ticket
2818 $self->Load( $self->id ); # in case $self changed while waiting for lock
2820 my $OldOwnerObj = $self->OwnerObj;
2822 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2823 $NewOwnerObj->Load( $NewOwner );
2824 unless ( $NewOwnerObj->Id ) {
2825 $RT::Handle->Rollback();
2826 return ( 0, $self->loc("That user does not exist") );
2830 # must have ModifyTicket rights
2831 # or TakeTicket/StealTicket and $NewOwner is self
2832 # see if it's a take
2833 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2834 unless ( $self->CurrentUserHasRight('ModifyTicket')
2835 || $self->CurrentUserHasRight('TakeTicket') ) {
2836 $RT::Handle->Rollback();
2837 return ( 0, $self->loc("Permission Denied") );
2841 # see if it's a steal
2842 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2843 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2845 unless ( $self->CurrentUserHasRight('ModifyTicket')
2846 || $self->CurrentUserHasRight('StealTicket') ) {
2847 $RT::Handle->Rollback();
2848 return ( 0, $self->loc("Permission Denied") );
2852 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2853 $RT::Handle->Rollback();
2854 return ( 0, $self->loc("Permission Denied") );
2858 # If we're not stealing and the ticket has an owner and it's not
2860 if ( $Type ne 'Steal' and $Type ne 'Force'
2861 and $OldOwnerObj->Id != $RT::Nobody->Id
2862 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2864 $RT::Handle->Rollback();
2865 return ( 0, $self->loc("You can only take tickets that are unowned") )
2866 if $NewOwnerObj->id == $self->CurrentUser->id;
2869 $self->loc("You can only reassign tickets that you own or that are unowned" )
2873 #If we've specified a new owner and that user can't modify the ticket
2874 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2875 $RT::Handle->Rollback();
2876 return ( 0, $self->loc("That user may not own tickets in that queue") );
2879 # If the ticket has an owner and it's the new owner, we don't need
2881 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2882 $RT::Handle->Rollback();
2883 return ( 0, $self->loc("That user already owns that ticket") );
2886 # Delete the owner in the owner group, then add a new one
2887 # TODO: is this safe? it's not how we really want the API to work
2888 # for most things, but it's fast.
2889 my ( $del_id, $del_msg );
2890 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2891 ($del_id, $del_msg) = $owner->Delete();
2892 last unless ($del_id);
2896 $RT::Handle->Rollback();
2897 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2900 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2901 PrincipalId => $NewOwnerObj->PrincipalId,
2902 InsideTransaction => 1 );
2904 $RT::Handle->Rollback();
2905 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2908 # We call set twice with slightly different arguments, so
2909 # as to not have an SQL transaction span two RT transactions
2911 my ( $val, $msg ) = $self->_Set(
2913 RecordTransaction => 0,
2914 Value => $NewOwnerObj->Id,
2916 TransactionType => $Type,
2917 CheckACL => 0, # don't check acl
2921 $RT::Handle->Rollback;
2922 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2925 ($val, $msg) = $self->_NewTransaction(
2928 NewValue => $NewOwnerObj->Id,
2929 OldValue => $OldOwnerObj->Id,
2934 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2935 $OldOwnerObj->Name, $NewOwnerObj->Name );
2938 $RT::Handle->Rollback();
2942 $RT::Handle->Commit();
2944 return ( $val, $msg );
2953 A convenince method to set the ticket's owner to the current user
2959 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2968 Convenience method to set the owner to 'nobody' if the current user is the owner.
2974 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2983 A convenience method to change the owner of the current ticket to the
2984 current user. Even if it's owned by another user.
2991 if ( $self->IsOwner( $self->CurrentUser ) ) {
2992 return ( 0, $self->loc("You already own this ticket") );
2995 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3005 # {{{ Routines dealing with status
3007 # {{{ sub ValidateStatus
3009 =head2 ValidateStatus STATUS
3011 Takes a string. Returns true if that status is a valid status for this ticket.
3012 Returns false otherwise.
3016 sub ValidateStatus {
3020 #Make sure the status passed in is valid
3021 unless ( $self->QueueObj->IsValidStatus($status) ) {
3033 =head2 SetStatus STATUS
3035 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3037 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.
3048 $args{Status} = shift;
3055 if ( $args{Status} eq 'deleted') {
3056 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3057 return ( 0, $self->loc('Permission Denied') );
3060 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3061 return ( 0, $self->loc('Permission Denied') );
3065 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3066 return (0, $self->loc('That ticket has unresolved dependencies'));
3069 my $now = RT::Date->new( $self->CurrentUser );
3072 #If we're changing the status from new, record that we've started
3073 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3075 #Set the Started time to "now"
3076 $self->_Set( Field => 'Started',
3078 RecordTransaction => 0 );
3081 #When we close a ticket, set the 'Resolved' attribute to now.
3082 # It's misnamed, but that's just historical.
3083 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3084 $self->_Set( Field => 'Resolved',
3086 RecordTransaction => 0 );
3089 #Actually update the status
3090 my ($val, $msg)= $self->_Set( Field => 'Status',
3091 Value => $args{Status},
3094 TransactionType => 'Status' );
3105 Takes no arguments. Marks this ticket for garbage collection
3111 return ( $self->SetStatus('deleted') );
3113 # TODO: garbage collection
3122 Sets this ticket's status to stalled
3128 return ( $self->SetStatus('stalled') );
3137 Sets this ticket's status to rejected
3143 return ( $self->SetStatus('rejected') );
3152 Sets this ticket\'s status to Open
3158 return ( $self->SetStatus('open') );
3167 Sets this ticket\'s status to Resolved
3173 return ( $self->SetStatus('resolved') );
3181 # {{{ Actions + Routines dealing with transactions
3183 # {{{ sub SetTold and _SetTold
3185 =head2 SetTold ISO [TIMETAKEN]
3187 Updates the told and records a transaction
3194 $told = shift if (@_);
3195 my $timetaken = shift || 0;
3197 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3198 return ( 0, $self->loc("Permission Denied") );
3201 my $datetold = new RT::Date( $self->CurrentUser );
3203 $datetold->Set( Format => 'iso',
3207 $datetold->SetToNow();
3210 return ( $self->_Set( Field => 'Told',
3211 Value => $datetold->ISO,
3212 TimeTaken => $timetaken,
3213 TransactionType => 'Told' ) );
3218 Updates the told without a transaction or acl check. Useful when we're sending replies.
3225 my $now = new RT::Date( $self->CurrentUser );
3228 #use __Set to get no ACLs ;)
3229 return ( $self->__Set( Field => 'Told',
3230 Value => $now->ISO ) );
3240 my $uid = $self->CurrentUser->id;
3241 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3242 return if $attr && $attr->Content gt $self->LastUpdated;
3244 my $txns = $self->Transactions;
3245 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3246 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3247 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3251 VALUE => $attr->Content
3253 $txns->RowsPerPage(1);
3254 return $txns->First;
3259 =head2 TransactionBatch
3261 Returns an array reference of all transactions created on this ticket during
3262 this ticket object's lifetime or since last application of a batch, or undef
3265 Only works when the C<UseTransactionBatch> config option is set to true.
3269 sub TransactionBatch {
3271 return $self->{_TransactionBatch};
3274 =head2 ApplyTransactionBatch
3276 Applies scrips on the current batch of transactions and shinks it. Usually
3277 batch is applied when object is destroyed, but in some cases it's too late.
3281 sub ApplyTransactionBatch {
3284 my $batch = $self->TransactionBatch;
3285 return unless $batch && @$batch;
3287 $self->_ApplyTransactionBatch;
3289 $self->{_TransactionBatch} = [];
3292 sub _ApplyTransactionBatch {
3294 my $batch = $self->TransactionBatch;
3297 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->Type, grep defined, @{$batch};
3300 RT::Scrips->new($RT::SystemUser)->Apply(
3301 Stage => 'TransactionBatch',
3303 TransactionObj => $batch->[0],
3307 # Entry point of the rule system
3308 my $rules = RT::Ruleset->FindAllRules(
3309 Stage => 'TransactionBatch',
3311 TransactionObj => $batch->[0],
3314 RT::Ruleset->CommitRules($rules);
3320 # DESTROY methods need to localize $@, or it may unset it. This
3321 # causes $m->abort to not bubble all of the way up. See perlbug
3322 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3325 # The following line eliminates reentrancy.
3326 # It protects against the fact that perl doesn't deal gracefully
3327 # when an object's refcount is changed in its destructor.
3328 return if $self->{_Destroyed}++;
3330 my $batch = $self->TransactionBatch;
3331 return unless $batch && @$batch;
3333 return $self->_ApplyTransactionBatch;
3338 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3340 # {{{ sub _OverlayAccessible
3342 sub _OverlayAccessible {
3344 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3345 Queue => { 'read' => 1, 'write' => 1 },
3346 Requestors => { 'read' => 1, 'write' => 1 },
3347 Owner => { 'read' => 1, 'write' => 1 },
3348 Subject => { 'read' => 1, 'write' => 1 },
3349 InitialPriority => { 'read' => 1, 'write' => 1 },
3350 FinalPriority => { 'read' => 1, 'write' => 1 },
3351 Priority => { 'read' => 1, 'write' => 1 },
3352 Status => { 'read' => 1, 'write' => 1 },
3353 TimeEstimated => { 'read' => 1, 'write' => 1 },
3354 TimeWorked => { 'read' => 1, 'write' => 1 },
3355 TimeLeft => { 'read' => 1, 'write' => 1 },
3356 Told => { 'read' => 1, 'write' => 1 },
3357 Resolved => { 'read' => 1 },
3358 Type => { 'read' => 1 },
3359 Starts => { 'read' => 1, 'write' => 1 },
3360 Started => { 'read' => 1, 'write' => 1 },
3361 Due => { 'read' => 1, 'write' => 1 },
3362 Creator => { 'read' => 1, 'auto' => 1 },
3363 Created => { 'read' => 1, 'auto' => 1 },
3364 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3365 LastUpdated => { 'read' => 1, 'auto' => 1 }
3377 my %args = ( Field => undef,
3380 RecordTransaction => 1,
3383 TransactionType => 'Set',
3386 if ($args{'CheckACL'}) {
3387 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3388 return ( 0, $self->loc("Permission Denied"));
3392 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3393 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3394 return(0, $self->loc("Internal Error"));
3397 #if the user is trying to modify the record
3399 #Take care of the old value we really don't want to get in an ACL loop.
3400 # so ask the super::_Value
3401 my $Old = $self->SUPER::_Value("$args{'Field'}");
3404 if ( $args{'UpdateTicket'} ) {
3407 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3408 Value => $args{'Value'} );
3410 #If we can't actually set the field to the value, don't record
3411 # a transaction. instead, get out of here.
3412 return ( 0, $msg ) unless $ret;
3415 if ( $args{'RecordTransaction'} == 1 ) {
3417 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3418 Type => $args{'TransactionType'},
3419 Field => $args{'Field'},
3420 NewValue => $args{'Value'},
3422 TimeTaken => $args{'TimeTaken'},
3424 return ( $Trans, scalar $TransObj->BriefDescription );
3427 return ( $ret, $msg );
3437 Takes the name of a table column.
3438 Returns its value as a string, if the user passes an ACL check
3447 #if the field is public, return it.
3448 if ( $self->_Accessible( $field, 'public' ) ) {
3450 #$RT::Logger->debug("Skipping ACL check for $field");
3451 return ( $self->SUPER::_Value($field) );
3455 #If the current user doesn't have ACLs, don't let em at it.
3457 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3460 return ( $self->SUPER::_Value($field) );
3466 # {{{ sub _UpdateTimeTaken
3468 =head2 _UpdateTimeTaken
3470 This routine will increment the timeworked counter. it should
3471 only be called from _NewTransaction
3475 sub _UpdateTimeTaken {
3477 my $Minutes = shift;
3480 $Total = $self->SUPER::_Value("TimeWorked");
3481 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3483 Field => "TimeWorked",
3494 # {{{ Routines dealing with ACCESS CONTROL
3496 # {{{ sub CurrentUserHasRight
3498 =head2 CurrentUserHasRight
3500 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3501 1 if the user has that right. It returns 0 if the user doesn't have that right.
3505 sub CurrentUserHasRight {
3509 return $self->CurrentUser->PrincipalObj->HasRight(
3521 Takes a paramhash with the attributes 'Right' and 'Principal'
3522 'Right' is a ticket-scoped textual right from RT::ACE
3523 'Principal' is an RT::User object
3525 Returns 1 if the principal has the right. Returns undef if not.
3537 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3539 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3540 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3545 $args{'Principal'}->HasRight(
3547 Right => $args{'Right'}
3558 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3559 It isn't acutally a searchbuilder collection itself.
3566 unless ($self->{'__reminders'}) {
3567 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3568 $self->{'__reminders'}->Ticket($self->id);
3570 return $self->{'__reminders'};
3576 # {{{ sub Transactions
3580 Returns an RT::Transactions object of all transactions on this ticket
3587 my $transactions = RT::Transactions->new( $self->CurrentUser );
3589 #If the user has no rights, return an empty object
3590 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3591 $transactions->LimitToTicket($self->id);
3593 # if the user may not see comments do not return them
3594 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3595 $transactions->Limit(
3601 $transactions->Limit(
3605 VALUE => "CommentEmailRecord",
3606 ENTRYAGGREGATOR => 'AND'
3611 $transactions->Limit(
3615 ENTRYAGGREGATOR => 'AND'
3619 return ($transactions);
3625 # {{{ TransactionCustomFields
3627 =head2 TransactionCustomFields
3629 Returns the custom fields that transactions on tickets will have.
3633 sub TransactionCustomFields {
3635 return $self->QueueObj->TicketTransactionCustomFields;
3640 # {{{ sub CustomFieldValues
3642 =head2 CustomFieldValues
3644 # Do name => id mapping (if needed) before falling back to
3645 # RT::Record's CustomFieldValues
3651 sub CustomFieldValues {
3655 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3657 my $cf = RT::CustomField->new( $self->CurrentUser );
3658 $cf->SetContextObject( $self );
3659 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3660 unless ( $cf->id ) {
3661 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3664 # If we didn't find a valid cfid, give up.
3665 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3667 return $self->SUPER::CustomFieldValues( $cf->id );
3672 # {{{ sub CustomFieldLookupType
3674 =head2 CustomFieldLookupType
3676 Returns the RT::Ticket lookup type, which can be passed to
3677 RT::CustomField->Create() via the 'LookupType' hash key.
3683 sub CustomFieldLookupType {
3684 "RT::Queue-RT::Ticket";
3687 =head2 ACLEquivalenceObjects
3689 This method returns a list of objects for which a user's rights also apply
3690 to this ticket. Generally, this is only the ticket's queue, but some RT
3691 extensions may make other objects available too.
3693 This method is called from L<RT::Principal/HasRight>.
3697 sub ACLEquivalenceObjects {
3699 return $self->QueueObj;
3708 Jesse Vincent, jesse@bestpractical.com