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 }}}
53 my $ticket = new RT::Ticket($CurrentUser);
54 $ticket->Load($ticket_id);
58 This module lets you manipulate RT\'s ticket object.
66 ok(my $testqueue = RT::Queue->new($RT::SystemUser));
67 ok($testqueue->Create( Name => 'ticket tests'));
68 ok($testqueue->Id != 0);
69 use_ok(RT::CustomField);
70 ok(my $testcf = RT::CustomField->new($RT::SystemUser));
71 my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti',
72 Queue => $testqueue->id,
73 Type => 'SelectMultiple');
74 ok($ret,"Created the custom field - ".$cmsg);
75 ($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1',
77 Description => 'A testing value');
79 ok($ret, "Added a value - ".$cmsg);
81 ok($testcf->AddValue ( Name => 'Value2',
83 Description => 'Another testing value'));
84 ok($testcf->AddValue ( Name => 'Value3',
86 Description => 'Yet Another testing value'));
88 ok($testcf->Values->Count == 3);
92 my $u = RT::User->new($RT::SystemUser);
94 ok ($u->Id, "Found the root user");
95 ok(my $t = RT::Ticket->new($RT::SystemUser));
96 ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
101 ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
102 ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
104 ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
105 ok($t->CustomFieldValues($testcf->Id)->Count == 1);
106 ok($t->CustomFieldValues($testcf->Id)->First &&
107 $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
109 ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
111 ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
112 ok($t->CustomFieldValues($testcf->Id)->Count == 0);
114 ok(my $t2 = RT::Ticket->new($RT::SystemUser));
116 is($t2->Subject, 'Testing');
117 is($t2->QueueObj->Id, $testqueue->id);
118 ok($t2->OwnerObj->Id == $u->Id);
120 my $t3 = RT::Ticket->new($RT::SystemUser);
121 my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
122 Subject => 'Testing',
124 my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
126 ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
127 my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
129 ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
130 my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
132 ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
133 ok($t->CustomFieldValues($testcf->Id)->Count == 2,
134 "This ticket has 2 custom field values");
135 ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
136 "This ticket has 1 custom field value");
146 no warnings qw(redefine);
153 use RT::CustomFields;
155 use RT::Transactions;
157 use RT::URI::fsck_com_rt;
164 ok(require RT::Ticket, "Loading the RT::Ticket library");
173 # A helper table for links mapping to make it easier
174 # to build and parse links between tickets
176 use vars '%LINKTYPEMAP';
179 MemberOf => { Type => 'MemberOf',
181 Parents => { Type => 'MemberOf',
183 Members => { Type => 'MemberOf',
185 Children => { Type => 'MemberOf',
187 HasMember => { Type => 'MemberOf',
189 RefersTo => { Type => 'RefersTo',
191 ReferredToBy => { Type => 'RefersTo',
193 DependsOn => { Type => 'DependsOn',
195 DependedOnBy => { Type => 'DependsOn',
197 MergedInto => { Type => 'MergedInto',
205 # A helper table for links mapping to make it easier
206 # to build and parse links between tickets
208 use vars '%LINKDIRMAP';
211 MemberOf => { Base => 'MemberOf',
212 Target => 'HasMember', },
213 RefersTo => { Base => 'RefersTo',
214 Target => 'ReferredToBy', },
215 DependsOn => { Base => 'DependsOn',
216 Target => 'DependedOnBy', },
217 MergedInto => { Base => 'MergedInto',
218 Target => 'MergedInto', },
224 sub LINKTYPEMAP { return \%LINKTYPEMAP }
225 sub LINKDIRMAP { return \%LINKDIRMAP }
231 Takes a single argument. This can be a ticket id, ticket alias or
232 local ticket uri. If the ticket can't be loaded, returns undef.
233 Otherwise, returns the ticket id.
241 #TODO modify this routine to look at EffectiveId and do the recursive load
242 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
245 #If it's a local URI, turn it into a ticket id
246 if ( $RT::TicketBaseURI && $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
250 #If it's a remote URI, we're going to punt for now
251 elsif ( $id =~ '://' ) {
255 #If we have an integer URI, load the ticket
256 if ( $id =~ /^\d+$/ ) {
257 my ($ticketid,$msg) = $self->LoadById($id);
260 $RT::Logger->crit("$self tried to load a bogus ticket: $id\n");
265 #It's not a URI. It's not a numerical ticket ID. Punt!
267 $RT::Logger->warning("Tried to load a bogus ticket id: '$id'");
271 #If we're merged, resolve the merge.
272 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
273 $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
274 return ( $self->Load( $self->EffectiveId ) );
277 #Ok. we're loaded. lets get outa here.
278 return ( $self->Id );
288 Given a local ticket URI, loads the specified ticket.
296 if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
298 return ( $self->Load($id) );
311 Arguments: ARGS is a hash of named parameters. Valid parameters are:
314 Queue - Either a Queue object or a Queue Name
315 Requestor - A reference to a list of email addresses or RT user Names
316 Cc - A reference to a list of email addresses or Names
317 AdminCc - A reference to a list of email addresses or Names
318 Type -- The ticket\'s type. ignore this for now
319 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
320 Subject -- A string describing the subject of the ticket
321 Priority -- an integer from 0 to 99
322 InitialPriority -- an integer from 0 to 99
323 FinalPriority -- an integer from 0 to 99
324 Status -- any valid status (Defined in RT::Queue)
325 TimeEstimated -- an integer. estimated time for this task in minutes
326 TimeWorked -- an integer. time worked so far in minutes
327 TimeLeft -- an integer. time remaining in minutes
328 Starts -- an ISO date describing the ticket\'s start date and time in GMT
329 Due -- an ISO date describing the ticket\'s due date and time in GMT
330 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
331 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
333 Ticket links can be set up during create by passing the link type as a hask key and
334 the ticket id to be linked to as a value (or a URI when linking to other objects).
335 Multiple links of the same type can be created by passing an array ref. For example:
338 DependsOn => [ 15, 22 ],
339 RefersTo => 'http://www.bestpractical.com',
341 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
342 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
343 C<Members> and C<Children> are aliases for C<HasMember>.
345 Returns: TICKETID, Transaction Object, Error Message
349 my $t = RT::Ticket->new($RT::SystemUser);
351 ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created");
353 ok ( my $id = $t->Id, "Got ticket id");
354 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
355 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
356 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
367 EffectiveId => undef,
375 InitialPriority => undef,
376 FinalPriority => undef,
387 _RecordTransaction => 1,
391 my ( $ErrStr, $Owner, $resolved );
392 my (@non_fatal_errors);
394 my $QueueObj = RT::Queue->new($RT::SystemUser);
396 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
397 $QueueObj->Load( $args{'Queue'} );
399 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
400 $QueueObj->Load( $args{'Queue'}->Id );
403 $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object." );
406 #Can't create a ticket without a queue.
407 unless ( defined($QueueObj) && $QueueObj->Id ) {
408 $RT::Logger->debug("$self No queue given for ticket creation.");
409 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
412 #Now that we have a queue, Check the ACLS
414 $self->CurrentUser->HasRight(
415 Right => 'CreateTicket',
422 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
425 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
426 return ( 0, 0, $self->loc('Invalid value for status') );
429 #Since we have a queue, we can set queue defaults
432 # If there's no queue default initial priority and it's not set, set it to 0
433 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
434 unless ( $args{'InitialPriority'} );
438 # If there's no queue default final priority and it's not set, set it to 0
439 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
440 unless ( $args{'FinalPriority'} );
442 # Priority may have changed from InitialPriority, for the case
443 # where we're importing tickets (eg, from an older RT version.)
444 my $priority = $args{'Priority'} || $args{'InitialPriority'};
447 #TODO we should see what sort of due date we're getting, rather +
448 # than assuming it's in ISO format.
450 #Set the due date. if we didn't get fed one, use the queue default due in
451 my $Due = new RT::Date( $self->CurrentUser );
453 if ( $args{'Due'} ) {
454 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
456 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
458 $Due->AddDays( $due_in );
461 my $Starts = new RT::Date( $self->CurrentUser );
462 if ( defined $args{'Starts'} ) {
463 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
466 my $Started = new RT::Date( $self->CurrentUser );
467 if ( defined $args{'Started'} ) {
468 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
471 my $Resolved = new RT::Date( $self->CurrentUser );
472 if ( defined $args{'Resolved'} ) {
473 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
476 #If the status is an inactive status, set the resolved date
477 if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} )
479 $RT::Logger->debug( "Got a ". $args{'Status'}
480 ." ticket with undefined resolved date. Setting to now."
487 # {{{ Dealing with time fields
489 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
490 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
491 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
495 # {{{ Deal with setting the owner
497 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
498 $Owner = $args{'Owner'};
501 #If we've been handed something else, try to load the user.
502 elsif ( $args{'Owner'} ) {
503 $Owner = RT::User->new( $self->CurrentUser );
504 $Owner->Load( $args{'Owner'} );
506 push( @non_fatal_errors,
507 $self->loc("Owner could not be set.") . " "
508 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} )
510 unless ( $Owner->Id );
513 #If we have a proposed owner and they don't have the right
514 #to own a ticket, scream about it and make them not the owner
518 and ( $Owner->Id != $RT::Nobody->Id )
528 $RT::Logger->warning( "User "
532 . "as a ticket owner but has no rights to own "
536 push @non_fatal_errors,
537 $self->loc( "Owner '[_1]' does not have rights to own this ticket.",
544 #If we haven't been handed a valid owner, make it nobody.
545 unless ( defined($Owner) && $Owner->Id ) {
546 $Owner = new RT::User( $self->CurrentUser );
547 $Owner->Load( $RT::Nobody->Id );
552 # We attempt to load or create each of the people who might have a role for this ticket
553 # _outside_ the transaction, so we don't get into ticket creation races
554 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
555 next unless ( defined $args{$type} );
556 foreach my $watcher (
557 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
559 my $user = RT::User->new($RT::SystemUser);
560 $user->LoadOrCreateByEmail($watcher)
561 if ( $watcher && $watcher !~ /^\d+$/ );
565 $RT::Handle->BeginTransaction();
568 Queue => $QueueObj->Id,
570 Subject => $args{'Subject'},
571 InitialPriority => $args{'InitialPriority'},
572 FinalPriority => $args{'FinalPriority'},
573 Priority => $priority,
574 Status => $args{'Status'},
575 TimeWorked => $args{'TimeWorked'},
576 TimeEstimated => $args{'TimeEstimated'},
577 TimeLeft => $args{'TimeLeft'},
578 Type => $args{'Type'},
579 Starts => $Starts->ISO,
580 Started => $Started->ISO,
581 Resolved => $Resolved->ISO,
585 # Parameters passed in during an import that we probably don't want to touch, otherwise
586 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
587 $params{$attr} = $args{$attr} if ( $args{$attr} );
590 # Delete null integer parameters
592 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
593 delete $params{$attr}
594 unless ( exists $params{$attr} && $params{$attr} );
597 # Delete the time worked if we're counting it in the transaction
598 delete $params{TimeWorked} if $args{'_RecordTransaction'};
600 my ($id,$ticket_message) = $self->SUPER::Create( %params);
602 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
603 $RT::Handle->Rollback();
605 $self->loc("Ticket could not be created due to an internal error")
609 #Set the ticket's effective ID now that we've created it.
610 my ( $val, $msg ) = $self->__Set(
611 Field => 'EffectiveId',
612 Value => ( $args{'EffectiveId'} || $id )
616 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
617 $RT::Handle->Rollback();
619 $self->loc("Ticket could not be created due to an internal error")
623 my $create_groups_ret = $self->_CreateTicketGroups();
624 unless ($create_groups_ret) {
625 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
627 . ". aborting Ticket creation." );
628 $RT::Handle->Rollback();
630 $self->loc("Ticket could not be created due to an internal error")
634 # Set the owner in the Groups table
635 # We denormalize it into the Ticket table too because doing otherwise would
636 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
638 $self->OwnerGroup->_AddMember(
639 PrincipalId => $Owner->PrincipalId,
640 InsideTransaction => 1
643 # {{{ Deal with setting up watchers
645 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
646 next unless ( defined $args{$type} );
647 foreach my $watcher (
648 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
651 # If there is an empty entry in the list, let's get out of here.
652 next unless $watcher;
654 # we reason that all-digits number must be a principal id, not email
655 # this is the only way to can add
657 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
661 if ( $type eq 'AdminCc' ) {
663 # Note that we're using AddWatcher, rather than _AddWatcher, as we
664 # actually _want_ that ACL check. Otherwise, random ticket creators
665 # could make themselves adminccs and maybe get ticket rights. that would
667 ( $wval, $wmsg ) = $self->AddWatcher(
674 ( $wval, $wmsg ) = $self->_AddWatcher(
681 push @non_fatal_errors, $wmsg unless ($wval);
686 # {{{ Deal with setting up links
688 foreach my $type ( keys %LINKTYPEMAP ) {
689 next unless ( defined $args{$type} );
691 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
693 # Check rights on the other end of the link if we must
694 # then run _AddLink that doesn't check for ACLs
695 if ( $RT::StrictLinkACL ) {
696 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
698 push @non_fatal_errors, $msg;
701 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
702 push @non_fatal_errors, $self->loc('Linking. Permission denied');
707 my ( $wval, $wmsg ) = $self->_AddLink(
708 Type => $LINKTYPEMAP{$type}->{'Type'},
709 $LINKTYPEMAP{$type}->{'Mode'} => $link,
713 push @non_fatal_errors, $wmsg unless ($wval);
719 # {{{ Deal with auto-customer association
721 #unless we already have (a) customer(s)...
722 unless ( $self->Customers->Count ) {
724 #first find any requestors with emails but *without* customer targets
725 my @NoCust_Requestors =
726 grep { $_->EmailAddress && ! $_->Customers->Count }
727 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
729 for my $Requestor (@NoCust_Requestors) {
731 #perhaps the stuff in here should be in a User method??
733 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
735 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
737 ## false laziness w/RT/Interface/Web_Vendor.pm
738 my @link = ( 'Type' => 'MemberOf',
739 'Target' => "freeside://freeside/cust_main/$custnum",
742 my( $val, $msg ) = $Requestor->_AddLink(@link);
743 #XXX should do something with $msg# push @non_fatal_errors, $msg;
749 #find any requestors with customer targets
751 my %cust_target = ();
754 grep { $_->Customers->Count }
755 @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
757 foreach my $Requestor ( @Requestors ) {
758 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
759 $cust_target{ $cust_link->Target } = 1;
763 #and then auto-associate this ticket with those customers
765 foreach my $cust_target ( keys %cust_target ) {
767 my @link = ( 'Type' => 'MemberOf',
768 #'Target' => "freeside://freeside/cust_main/$custnum",
769 'Target' => $cust_target,
772 my( $val, $msg ) = $self->_AddLink(@link);
773 push @non_fatal_errors, $msg;
781 # {{{ Add all the custom fields
783 foreach my $arg ( keys %args ) {
784 next unless ( $arg =~ /^CustomField-(\d+)$/i );
787 my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
789 next unless ( length($value) );
791 # Allow passing in uploaded LargeContent etc by hash reference
792 $self->_AddCustomFieldValue(
793 (UNIVERSAL::isa( $value => 'HASH' )
798 RecordTransaction => 0,
805 if ( $args{'_RecordTransaction'} ) {
807 # {{{ Add a transaction for the create
808 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
810 TimeTaken => $args{'TimeWorked'},
811 MIMEObj => $args{'MIMEObj'}
814 if ( $self->Id && $Trans ) {
816 $TransObj->UpdateCustomFields(ARGSRef => \%args);
818 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
819 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
820 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
823 $RT::Handle->Rollback();
825 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
826 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
827 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
830 $RT::Handle->Commit();
831 return ( $self->Id, $TransObj->Id, $ErrStr );
837 # Not going to record a transaction
838 $RT::Handle->Commit();
839 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
840 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
841 return ( $self->Id, 0, $ErrStr );
852 =head2 UpdateFrom822 $MESSAGE
854 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
855 Returns an um. ask me again when the code exists
860 my $simple_update = <<EOF;
862 AddRequestor: jesse\@example.com
865 my $ticket = RT::Ticket->new($RT::SystemUser);
866 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
867 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
868 $ticket->UpdateFrom822($simple_update);
869 is($ticket->Subject, 'target', "changed the subject");
870 my $jesse = RT::User->new($RT::SystemUser);
871 $jesse->LoadByEmail('jesse@example.com');
872 ok ($jesse->Id, "There's a user for jesse");
873 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
883 my %args = $self->_Parse822HeadersForAttributes($content);
887 Queue => $args{'queue'},
888 Subject => $args{'subject'},
889 Status => $args{'status'},
891 Starts => $args{'starts'},
892 Started => $args{'started'},
893 Resolved => $args{'resolved'},
894 Owner => $args{'owner'},
895 Requestor => $args{'requestor'},
897 AdminCc => $args{'admincc'},
898 TimeWorked => $args{'timeworked'},
899 TimeEstimated => $args{'timeestimated'},
900 TimeLeft => $args{'timeleft'},
901 InitialPriority => $args{'initialpriority'},
902 Priority => $args{'priority'},
903 FinalPriority => $args{'finalpriority'},
904 Type => $args{'type'},
905 DependsOn => $args{'dependson'},
906 DependedOnBy => $args{'dependedonby'},
907 RefersTo => $args{'refersto'},
908 ReferredToBy => $args{'referredtoby'},
909 Members => $args{'members'},
910 MemberOf => $args{'memberof'},
911 MIMEObj => $args{'mimeobj'}
914 foreach my $type qw(Requestor Cc Admincc) {
916 foreach my $action ( 'Add', 'Del', '' ) {
918 my $lctag = lc($action) . lc($type);
919 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
921 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
922 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
927 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
932 # Add custom field entries to %ticketargs.
933 # TODO: allow named custom fields
935 /^customfield-(\d+)$/
936 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
939 # for each ticket we've been told to update, iterate through the set of
940 # rfc822 headers and perform that update to the ticket.
943 # {{{ Set basic fields
957 # Resolve the queue from a name to a numeric id.
958 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
959 my $tempqueue = RT::Queue->new($RT::SystemUser);
960 $tempqueue->Load( $ticketargs{'Queue'} );
961 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
966 foreach my $attribute (@attribs) {
967 my $value = $ticketargs{$attribute};
969 if ( $value ne $self->$attribute() ) {
971 my $method = "Set$attribute";
972 my ( $code, $msg ) = $self->$method($value);
974 push @results, $self->loc($attribute) . ': ' . $msg;
979 # We special case owner changing, so we can use ForceOwnerChange
980 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
981 my $ChownType = "Give";
982 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
984 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
985 push ( @results, $msg );
989 # Deal with setting watchers
992 # Acceptable arguments:
999 foreach my $type qw(Requestor Cc AdminCc) {
1001 # If we've been given a number of delresses to del, do it.
1002 foreach my $address (@{$ticketargs{'Del'.$type}}) {
1003 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
1004 push (@results, $msg) ;
1007 # If we've been given a number of addresses to add, do it.
1008 foreach my $address (@{$ticketargs{'Add'.$type}}) {
1009 $RT::Logger->debug("Adding $address as a $type");
1010 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
1011 push (@results, $msg) ;
1022 # {{{ _Parse822HeadersForAttributes Content
1024 =head2 _Parse822HeadersForAttributes Content
1026 Takes an RFC822 style message and parses its attributes into a hash.
1030 sub _Parse822HeadersForAttributes {
1032 my $content = shift;
1035 my @lines = ( split ( /\n/, $content ) );
1036 while ( defined( my $line = shift @lines ) ) {
1037 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1042 if ( defined( $args{$tag} ) )
1043 { #if we're about to get a second value, make it an array
1044 $args{$tag} = [ $args{$tag} ];
1046 if ( ref( $args{$tag} ) )
1047 { #If it's an array, we want to push the value
1048 push @{ $args{$tag} }, $value;
1050 else { #if there's nothing there, just set the value
1051 $args{$tag} = $value;
1053 } elsif ($line =~ /^$/) {
1055 #TODO: this won't work, since "" isn't of the form "foo:value"
1057 while ( defined( my $l = shift @lines ) ) {
1058 push @{ $args{'content'} }, $l;
1064 foreach my $date qw(due starts started resolved) {
1065 my $dateobj = RT::Date->new($RT::SystemUser);
1066 if ( $args{$date} =~ /^\d+$/ ) {
1067 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1070 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1072 $args{$date} = $dateobj->ISO;
1074 $args{'mimeobj'} = MIME::Entity->new();
1075 $args{'mimeobj'}->build(
1076 Type => ( $args{'contenttype'} || 'text/plain' ),
1077 Data => ($args{'content'} || '')
1087 =head2 Import PARAMHASH
1090 Doesn\'t create a transaction.
1091 Doesn\'t supply queue defaults, etc.
1099 my ( $ErrStr, $QueueObj, $Owner );
1103 EffectiveId => undef,
1107 Owner => $RT::Nobody->Id,
1108 Subject => '[no subject]',
1109 InitialPriority => undef,
1110 FinalPriority => undef,
1121 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1122 $QueueObj = RT::Queue->new($RT::SystemUser);
1123 $QueueObj->Load( $args{'Queue'} );
1125 #TODO error check this and return 0 if it\'s not loading properly +++
1127 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1128 $QueueObj = RT::Queue->new($RT::SystemUser);
1129 $QueueObj->Load( $args{'Queue'}->Id );
1133 "$self " . $args{'Queue'} . " not a recognised queue object." );
1136 #Can't create a ticket without a queue.
1137 unless ( defined($QueueObj) and $QueueObj->Id ) {
1138 $RT::Logger->debug("$self No queue given for ticket creation.");
1139 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1142 #Now that we have a queue, Check the ACLS
1144 $self->CurrentUser->HasRight(
1145 Right => 'CreateTicket',
1151 $self->loc("No permission to create tickets in the queue '[_1]'"
1152 , $QueueObj->Name));
1155 # {{{ Deal with setting the owner
1157 # Attempt to take user object, user name or user id.
1158 # Assign to nobody if lookup fails.
1159 if ( defined( $args{'Owner'} ) ) {
1160 if ( ref( $args{'Owner'} ) ) {
1161 $Owner = $args{'Owner'};
1164 $Owner = new RT::User( $self->CurrentUser );
1165 $Owner->Load( $args{'Owner'} );
1166 if ( !defined( $Owner->id ) ) {
1167 $Owner->Load( $RT::Nobody->id );
1172 #If we have a proposed owner and they don't have the right
1173 #to own a ticket, scream about it and make them not the owner
1176 and ( $Owner->Id != $RT::Nobody->Id )
1179 Object => $QueueObj,
1180 Right => 'OwnTicket'
1186 $RT::Logger->warning( "$self user "
1187 . $Owner->Name . "("
1190 . "as a ticket owner but has no rights to own "
1192 . $QueueObj->Name . "'\n" );
1197 #If we haven't been handed a valid owner, make it nobody.
1198 unless ( defined($Owner) ) {
1199 $Owner = new RT::User( $self->CurrentUser );
1200 $Owner->Load( $RT::Nobody->UserObj->Id );
1205 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1206 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1209 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1210 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1211 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1212 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1214 # If we're coming in with an id, set that now.
1215 my $EffectiveId = undef;
1216 if ( $args{'id'} ) {
1217 $EffectiveId = $args{'id'};
1221 my $id = $self->SUPER::Create(
1223 EffectiveId => $EffectiveId,
1224 Queue => $QueueObj->Id,
1225 Owner => $Owner->Id,
1226 Subject => $args{'Subject'}, # loc
1227 InitialPriority => $args{'InitialPriority'}, # loc
1228 FinalPriority => $args{'FinalPriority'}, # loc
1229 Priority => $args{'InitialPriority'}, # loc
1230 Status => $args{'Status'}, # loc
1231 TimeWorked => $args{'TimeWorked'}, # loc
1232 Type => $args{'Type'}, # loc
1233 Created => $args{'Created'}, # loc
1234 Told => $args{'Told'}, # loc
1235 LastUpdated => $args{'Updated'}, # loc
1236 Resolved => $args{'Resolved'}, # loc
1237 Due => $args{'Due'}, # loc
1240 # If the ticket didn't have an id
1241 # Set the ticket's effective ID now that we've created it.
1242 if ( $args{'id'} ) {
1243 $self->Load( $args{'id'} );
1247 $self->__Set( Field => 'EffectiveId', Value => $id );
1251 $self . "->Import couldn't set EffectiveId: $msg\n" );
1255 my $create_groups_ret = $self->_CreateTicketGroups();
1256 unless ($create_groups_ret) {
1258 "Couldn't create ticket groups for ticket " . $self->Id );
1261 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1264 foreach $watcher ( @{ $args{'Cc'} } ) {
1265 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1267 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1268 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1271 foreach $watcher ( @{ $args{'Requestor'} } ) {
1272 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1276 return ( $self->Id, $ErrStr );
1281 # {{{ Routines dealing with watchers.
1283 # {{{ _CreateTicketGroups
1285 =head2 _CreateTicketGroups
1287 Create the ticket groups and links for this ticket.
1288 This routine expects to be called from Ticket->Create _inside of a transaction_
1290 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1292 It will return true on success and undef on failure.
1296 my $ticket = RT::Ticket->new($RT::SystemUser);
1297 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1298 Owner => $RT::SystemUser->Id,
1300 Requestor => ['jesse@example.com'],
1303 ok ($id, "Ticket $id was created");
1304 ok(my $group = RT::Group->new($RT::SystemUser));
1305 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1306 ok ($group->Id, "Found the requestors object for this ticket");
1308 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1309 $jesse->LoadByEmail('jesse@example.com');
1310 ok($jesse->Id, "Found the jesse rt user");
1313 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1314 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1315 ok ($add_id, "Add succeeded: ($add_msg)");
1316 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1317 $bob->LoadByEmail('bob@fsck.com');
1318 ok($bob->Id, "Found the bob rt user");
1319 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1320 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1321 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1324 $group = RT::Group->new($RT::SystemUser);
1325 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1326 ok ($group->Id, "Found the cc object for this ticket");
1327 $group = RT::Group->new($RT::SystemUser);
1328 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1329 ok ($group->Id, "Found the AdminCc object for this ticket");
1330 $group = RT::Group->new($RT::SystemUser);
1331 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1332 ok ($group->Id, "Found the Owner object for this ticket");
1333 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1340 sub _CreateTicketGroups {
1343 my @types = qw(Requestor Owner Cc AdminCc);
1345 foreach my $type (@types) {
1346 my $type_obj = RT::Group->new($self->CurrentUser);
1347 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1348 Instance => $self->Id,
1351 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1352 $self->Id.": ".$msg);
1362 # {{{ sub OwnerGroup
1366 A constructor which returns an RT::Group object containing the owner of this ticket.
1372 my $owner_obj = RT::Group->new($self->CurrentUser);
1373 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1374 return ($owner_obj);
1380 # {{{ sub AddWatcher
1384 AddWatcher takes a parameter hash. The keys are as follows:
1386 Type One of Requestor, Cc, AdminCc
1388 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1390 Email The email address of the new watcher. If a user with this
1391 email address can't be found, a new nonprivileged user will be created.
1393 If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1401 PrincipalId => undef,
1406 return ( 0, "No principal specified" )
1407 unless $args{'Email'} or $args{'PrincipalId'};
1409 if ( !$args{'PrincipalId'} and $args{'Email'} ) {
1410 my $user = RT::User->new( $self->CurrentUser );
1411 $user->LoadByEmail( $args{'Email'} );
1413 $args{'PrincipalId'} = $user->PrincipalId;
1414 delete $args{'Email'};
1419 # ModifyTicket allow you to add any watcher
1420 return $self->_AddWatcher(%args)
1421 if $self->CurrentUserHasRight('ModifyTicket');
1423 #If the watcher we're trying to add is for the current user
1424 if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0) ) {
1425 # If it's an AdminCc and they have 'WatchAsAdminCc'
1426 if ( $args{'Type'} eq 'AdminCc' ) {
1427 return $self->_AddWatcher( %args )
1428 if $self->CurrentUserHasRight('WatchAsAdminCc');
1431 # If it's a Requestor or Cc and they have 'Watch'
1432 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1433 return $self->_AddWatcher( %args )
1434 if $self->CurrentUserHasRight('Watch');
1437 $RT::Logger->warning( "AddWatcher got passed a bogus type" );
1438 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1442 return ( 0, $self->loc("Permission Denied") );
1445 #This contains the meat of AddWatcher. but can be called from a routine like
1446 # Create, which doesn't need the additional acl check
1452 PrincipalId => undef,
1458 my $principal = RT::Principal->new($self->CurrentUser);
1459 if ($args{'Email'}) {
1460 my $user = RT::User->new($RT::SystemUser);
1461 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1462 # If we can't load the user by email address, let's try to load by username
1464 ($pid,$msg) = $user->Load($args{'Email'})
1467 $args{'PrincipalId'} = $pid;
1470 if ($args{'PrincipalId'}) {
1471 $principal->Load($args{'PrincipalId'});
1475 # If we can't find this watcher, we need to bail.
1476 unless ($principal->Id) {
1477 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1478 return(0, $self->loc("Could not find or create that user"));
1482 my $group = RT::Group->new($self->CurrentUser);
1483 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1484 unless ($group->id) {
1485 return(0,$self->loc("Group not found"));
1488 if ( $group->HasMember( $principal)) {
1490 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1494 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1495 InsideTransaction => 1 );
1497 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1499 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1502 unless ( $args{'Silent'} ) {
1503 $self->_NewTransaction(
1504 Type => 'AddWatcher',
1505 NewValue => $principal->Id,
1506 Field => $args{'Type'}
1510 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1516 # {{{ sub DeleteWatcher
1518 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1521 Deletes a Ticket watcher. Takes two arguments:
1523 Type (one of Requestor,Cc,AdminCc)
1527 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1529 Email (the email address of an existing wathcer)
1538 my %args = ( Type => undef,
1539 PrincipalId => undef,
1543 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1544 return ( 0, $self->loc("No principal specified") );
1546 my $principal = RT::Principal->new( $self->CurrentUser );
1547 if ( $args{'PrincipalId'} ) {
1549 $principal->Load( $args{'PrincipalId'} );
1552 my $user = RT::User->new( $self->CurrentUser );
1553 $user->LoadByEmail( $args{'Email'} );
1554 $principal->Load( $user->Id );
1557 # If we can't find this watcher, we need to bail.
1558 unless ( $principal->Id ) {
1559 return ( 0, $self->loc("Could not find that principal") );
1562 my $group = RT::Group->new( $self->CurrentUser );
1563 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1564 unless ( $group->id ) {
1565 return ( 0, $self->loc("Group not found") );
1569 #If the watcher we're trying to add is for the current user
1570 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1572 # If it's an AdminCc and they don't have
1573 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1574 if ( $args{'Type'} eq 'AdminCc' ) {
1575 unless ( $self->CurrentUserHasRight('ModifyTicket')
1576 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1577 return ( 0, $self->loc('Permission Denied') );
1581 # If it's a Requestor or Cc and they don't have
1582 # 'Watch' or 'ModifyTicket', bail
1583 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1585 unless ( $self->CurrentUserHasRight('ModifyTicket')
1586 or $self->CurrentUserHasRight('Watch') ) {
1587 return ( 0, $self->loc('Permission Denied') );
1591 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1593 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1597 # If the watcher isn't the current user
1598 # and the current user doesn't have 'ModifyTicket' bail
1600 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1601 return ( 0, $self->loc("Permission Denied") );
1607 # see if this user is already a watcher.
1609 unless ( $group->HasMember($principal) ) {
1611 $self->loc( 'That principal is not a [_1] for this ticket',
1615 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1617 $RT::Logger->error( "Failed to delete "
1619 . " as a member of group "
1625 'Could not remove that principal as a [_1] for this ticket',
1629 unless ( $args{'Silent'} ) {
1630 $self->_NewTransaction( Type => 'DelWatcher',
1631 OldValue => $principal->Id,
1632 Field => $args{'Type'} );
1636 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1637 $principal->Object->Name,
1646 =head2 SquelchMailTo [EMAIL]
1648 Takes an optional email address to never email about updates to this ticket.
1651 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1655 my $t = RT::Ticket->new($RT::SystemUser);
1656 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1658 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1660 my @returned = $t->SquelchMailTo('nobody@example.com');
1662 is($#returned, 0, "The ticket has one squelched recipients");
1664 my @names = $t->Attributes->Names;
1665 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1666 @returned = $t->SquelchMailTo('nobody@example.com');
1669 is($#returned, 0, "The ticket has one squelched recipients");
1671 @names = $t->Attributes->Names;
1672 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1675 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1676 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1677 @returned = $t->SquelchMailTo();
1678 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1688 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1692 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1693 unless grep { $_->Content eq $attr }
1694 $self->Attributes->Named('SquelchMailTo');
1697 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1700 my @attributes = $self->Attributes->Named('SquelchMailTo');
1701 return (@attributes);
1705 =head2 UnsquelchMailTo ADDRESS
1707 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1709 Returns a tuple of (status, message)
1713 sub UnsquelchMailTo {
1716 my $address = shift;
1717 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1718 return ( 0, $self->loc("Permission Denied") );
1721 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1722 return ($val, $msg);
1726 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1728 =head2 RequestorAddresses
1730 B<Returns> String: All Ticket Requestor email addresses as a string.
1734 sub RequestorAddresses {
1737 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1741 return ( $self->Requestors->MemberEmailAddressesAsString );
1745 =head2 AdminCcAddresses
1747 returns String: All Ticket AdminCc email addresses as a string
1751 sub AdminCcAddresses {
1754 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1758 return ( $self->AdminCc->MemberEmailAddressesAsString )
1764 returns String: All Ticket Ccs as a string of email addresses
1771 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1775 return ( $self->Cc->MemberEmailAddressesAsString);
1781 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1783 # {{{ sub Requestors
1788 Returns this ticket's Requestors as an RT::Group object
1795 my $group = RT::Group->new($self->CurrentUser);
1796 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1797 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1805 # {{{ sub _Requestors
1809 Private non-ACLed variant of Reqeustors so that we can look them up for the
1810 purposes of customer auto-association during create.
1817 my $group = RT::Group->new($RT::SystemUser);
1818 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1829 Returns an RT::Group object which contains this ticket's Ccs.
1830 If the user doesn't have "ShowTicket" permission, returns an empty group
1837 my $group = RT::Group->new($self->CurrentUser);
1838 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1839 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1852 Returns an RT::Group object which contains this ticket's AdminCcs.
1853 If the user doesn't have "ShowTicket" permission, returns an empty group
1860 my $group = RT::Group->new($self->CurrentUser);
1861 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1862 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1872 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1875 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1877 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1879 Takes a param hash with the attributes Type and either PrincipalId or Email
1881 Type is one of Requestor, Cc, AdminCc and Owner
1883 PrincipalId is an RT::Principal id, and Email is an email address.
1885 Returns true if the specified principal (or the one corresponding to the
1886 specified address) is a member of the group Type for this ticket.
1888 XX TODO: This should be Memoized.
1895 my %args = ( Type => 'Requestor',
1896 PrincipalId => undef,
1901 # Load the relevant group.
1902 my $group = RT::Group->new($self->CurrentUser);
1903 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1905 # Find the relevant principal.
1906 my $principal = RT::Principal->new($self->CurrentUser);
1907 if (!$args{PrincipalId} && $args{Email}) {
1908 # Look up the specified user.
1909 my $user = RT::User->new($self->CurrentUser);
1910 $user->LoadByEmail($args{Email});
1912 $args{PrincipalId} = $user->PrincipalId;
1915 # A non-existent user can't be a group member.
1919 $principal->Load($args{'PrincipalId'});
1921 # Ask if it has the member in question
1922 return ($group->HasMember($principal));
1927 # {{{ sub IsRequestor
1929 =head2 IsRequestor PRINCIPAL_ID
1931 Takes an RT::Principal id
1932 Returns true if the principal is a requestor of the current ticket.
1941 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1949 =head2 IsCc PRINCIPAL_ID
1951 Takes an RT::Principal id.
1952 Returns true if the principal is a requestor of the current ticket.
1961 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1969 =head2 IsAdminCc PRINCIPAL_ID
1971 Takes an RT::Principal id.
1972 Returns true if the principal is a requestor of the current ticket.
1980 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1990 Takes an RT::User object. Returns true if that user is this ticket's owner.
1991 returns undef otherwise
1999 # no ACL check since this is used in acl decisions
2000 # unless ($self->CurrentUserHasRight('ShowTicket')) {
2004 #Tickets won't yet have owners when they're being created.
2005 unless ( $self->OwnerObj->id ) {
2009 if ( $person->id == $self->OwnerObj->id ) {
2023 # {{{ Routines dealing with queues
2025 # {{{ sub ValidateQueue
2032 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
2036 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2037 my $id = $QueueObj->Load($Value);
2053 my $NewQueue = shift;
2055 #Redundant. ACL gets checked in _Set;
2056 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2057 return ( 0, $self->loc("Permission Denied") );
2060 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2061 $NewQueueObj->Load($NewQueue);
2063 unless ( $NewQueueObj->Id() ) {
2064 return ( 0, $self->loc("That queue does not exist") );
2067 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2068 return ( 0, $self->loc('That is the same value') );
2071 $self->CurrentUser->HasRight(
2072 Right => 'CreateTicket',
2073 Object => $NewQueueObj
2077 return ( 0, $self->loc("You may not create requests in that queue.") );
2081 $self->OwnerObj->HasRight(
2082 Right => 'OwnTicket',
2083 Object => $NewQueueObj
2087 my $clone = RT::Ticket->new( $RT::SystemUser );
2088 $clone->Load( $self->Id );
2089 unless ( $clone->Id ) {
2090 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2092 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2093 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2096 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2105 Takes nothing. returns this ticket's queue object
2112 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2114 #We call __Value so that we can avoid the ACL decision and some deep recursion
2115 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2116 return ($queue_obj);
2123 # {{{ Date printing routines
2129 Returns an RT::Date object containing this ticket's due date
2136 my $time = new RT::Date( $self->CurrentUser );
2138 # -1 is RT::Date slang for never
2140 $time->Set( Format => 'sql', Value => $self->Due );
2143 $time->Set( Format => 'unix', Value => -1 );
2151 # {{{ sub DueAsString
2155 Returns this ticket's due date as a human readable string
2161 return $self->DueObj->AsString();
2166 # {{{ sub ResolvedObj
2170 Returns an RT::Date object of this ticket's 'resolved' time.
2177 my $time = new RT::Date( $self->CurrentUser );
2178 $time->Set( Format => 'sql', Value => $self->Resolved );
2184 # {{{ sub SetStarted
2188 Takes a date in ISO format or undef
2189 Returns a transaction id and a message
2190 The client calls "Start" to note that the project was started on the date in $date.
2191 A null date means "now"
2197 my $time = shift || 0;
2199 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2200 return ( 0, $self->loc("Permission Denied") );
2203 #We create a date object to catch date weirdness
2204 my $time_obj = new RT::Date( $self->CurrentUser() );
2206 $time_obj->Set( Format => 'ISO', Value => $time );
2209 $time_obj->SetToNow();
2212 #Now that we're starting, open this ticket
2213 #TODO do we really want to force this as policy? it should be a scrip
2215 #We need $TicketAsSystem, in case the current user doesn't have
2218 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2219 $TicketAsSystem->Load( $self->Id );
2220 if ( $TicketAsSystem->Status eq 'new' ) {
2221 $TicketAsSystem->Open();
2224 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2230 # {{{ sub StartedObj
2234 Returns an RT::Date object which contains this ticket's
2242 my $time = new RT::Date( $self->CurrentUser );
2243 $time->Set( Format => 'sql', Value => $self->Started );
2253 Returns an RT::Date object which contains this ticket's
2261 my $time = new RT::Date( $self->CurrentUser );
2262 $time->Set( Format => 'sql', Value => $self->Starts );
2272 Returns an RT::Date object which contains this ticket's
2280 my $time = new RT::Date( $self->CurrentUser );
2281 $time->Set( Format => 'sql', Value => $self->Told );
2287 # {{{ sub ToldAsString
2291 A convenience method that returns ToldObj->AsString
2293 TODO: This should be deprecated
2299 if ( $self->Told ) {
2300 return $self->ToldObj->AsString();
2309 # {{{ sub TimeWorkedAsString
2311 =head2 TimeWorkedAsString
2313 Returns the amount of time worked on this ticket as a Text String
2317 sub TimeWorkedAsString {
2319 return "0" unless $self->TimeWorked;
2321 #This is not really a date object, but if we diff a number of seconds
2322 #vs the epoch, we'll get a nice description of time worked.
2324 my $worked = new RT::Date( $self->CurrentUser );
2326 #return the #of minutes worked turned into seconds and written as
2327 # a simple text string
2329 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2336 # {{{ Routines dealing with correspondence/comments
2342 Comment on this ticket.
2343 Takes a hashref with the following attributes:
2344 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2347 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2349 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2350 They will, however, be prepared and you'll be able to access them through the TransactionObj
2352 Returns: Transaction id, Error Message, Transaction Object
2353 (note the different order from Create()!)
2360 my %args = ( CcMessageTo => undef,
2361 BccMessageTo => undef,
2368 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2369 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2370 return ( 0, $self->loc("Permission Denied"), undef );
2372 $args{'NoteType'} = 'Comment';
2374 if ($args{'DryRun'}) {
2375 $RT::Handle->BeginTransaction();
2376 $args{'CommitScrips'} = 0;
2379 my @results = $self->_RecordNote(%args);
2380 if ($args{'DryRun'}) {
2381 $RT::Handle->Rollback();
2388 # {{{ sub Correspond
2392 Correspond on this ticket.
2393 Takes a hashref with the following attributes:
2396 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2398 if there's no MIMEObj, Content is used to build a MIME::Entity object
2400 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2401 They will, however, be prepared and you'll be able to access them through the TransactionObj
2403 Returns: Transaction id, Error Message, Transaction Object
2404 (note the different order from Create()!)
2411 my %args = ( CcMessageTo => undef,
2412 BccMessageTo => undef,
2418 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2419 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2420 return ( 0, $self->loc("Permission Denied"), undef );
2423 $args{'NoteType'} = 'Correspond';
2424 if ($args{'DryRun'}) {
2425 $RT::Handle->BeginTransaction();
2426 $args{'CommitScrips'} = 0;
2429 my @results = $self->_RecordNote(%args);
2431 #Set the last told date to now if this isn't mail from the requestor.
2432 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2433 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2435 if ($args{'DryRun'}) {
2436 $RT::Handle->Rollback();
2445 # {{{ sub _RecordNote
2449 the meat of both comment and correspond.
2451 Performs no access control checks. hence, dangerous.
2458 my %args = ( CcMessageTo => undef,
2459 BccMessageTo => undef,
2466 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2467 return ( 0, $self->loc("No message attached"), undef );
2469 unless ( $args{'MIMEObj'} ) {
2470 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2471 ref $args{'Content'}
2473 : [ $args{'Content'} ]
2477 # convert text parts into utf-8
2478 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2480 # If we've been passed in CcMessageTo and BccMessageTo fields,
2481 # add them to the mime object for passing on to the transaction handler
2482 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2486 foreach my $type (qw/Cc Bcc/) {
2487 if ( defined $args{ $type . 'MessageTo' } ) {
2489 my $addresses = join ', ', (
2490 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2491 Mail::Address->parse( $args{ $type . 'MessageTo' } ) );
2492 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2496 # If this is from an external source, we need to come up with its
2497 # internal Message-ID now, so all emails sent because of this
2498 # message have a common Message-ID
2499 unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2500 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2502 $args{'MIMEObj'}->head->replace( 'RT-Message-ID',
2504 . $RT::VERSION . "-"
2506 . CORE::time() . "-"
2507 . int(rand(2000)) . '.'
2510 . "0" . "@" # Email sent
2515 #Record the correspondence (write the transaction)
2516 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2517 Type => $args{'NoteType'},
2518 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2519 TimeTaken => $args{'TimeTaken'},
2520 MIMEObj => $args{'MIMEObj'},
2521 CommitScrips => $args{'CommitScrips'},
2525 $RT::Logger->err("$self couldn't init a transaction $msg");
2526 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2529 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2541 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2544 my $type = shift || "";
2546 unless ( $self->{"$field$type"} ) {
2547 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2549 #not sure what this ACL was supposed to do... but returning the
2550 # bare (unlimited) RT::Links certainly seems wrong, it causes the
2551 # $Ticket->Customers method during creation to return results for every
2553 #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2555 # Maybe this ticket is a merged ticket
2556 my $Tickets = new RT::Tickets( $self->CurrentUser );
2557 # at least to myself
2558 $self->{"$field$type"}->Limit( FIELD => $field,
2559 VALUE => $self->URI,
2560 ENTRYAGGREGATOR => 'OR' );
2561 $Tickets->Limit( FIELD => 'EffectiveId',
2562 VALUE => $self->EffectiveId );
2563 while (my $Ticket = $Tickets->Next) {
2564 $self->{"$field$type"}->Limit( FIELD => $field,
2565 VALUE => $Ticket->URI,
2566 ENTRYAGGREGATOR => 'OR' );
2568 $self->{"$field$type"}->Limit( FIELD => 'Type',
2573 return ( $self->{"$field$type"} );
2578 # {{{ sub DeleteLink
2582 Delete a link. takes a paramhash of Base, Target and Type.
2583 Either Base or Target must be null. The null value will
2584 be replaced with this ticket\'s id
2597 unless ( $args{'Target'} || $args{'Base'} ) {
2598 $RT::Logger->error("Base or Target must be specified\n");
2599 return ( 0, $self->loc('Either base or target must be specified') );
2604 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2605 if ( !$right && $RT::StrictLinkACL ) {
2606 return ( 0, $self->loc("Permission Denied") );
2609 # If the other URI is an RT::Ticket, we want to make sure the user
2610 # can modify it too...
2611 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2612 return (0, $msg) unless $status;
2613 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2616 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2617 ( $RT::StrictLinkACL && $right < 2 ) )
2619 return ( 0, $self->loc("Permission Denied") );
2622 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2625 $RT::Logger->debug("Couldn't find that link\n");
2629 my ($direction, $remote_link);
2631 if ( $args{'Base'} ) {
2632 $remote_link = $args{'Base'};
2633 $direction = 'Target';
2635 elsif ( $args{'Target'} ) {
2636 $remote_link = $args{'Target'};
2640 if ( $args{'Silent'} ) {
2641 return ( $val, $Msg );
2644 my $remote_uri = RT::URI->new( $self->CurrentUser );
2645 $remote_uri->FromURI( $remote_link );
2647 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2648 Type => 'DeleteLink',
2649 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2650 OldValue => $remote_uri->URI || $remote_link,
2654 if ( $remote_uri->IsLocal ) {
2656 my $OtherObj = $remote_uri->Object;
2657 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2658 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2659 : $LINKDIRMAP{$args{'Type'}}->{Target},
2660 OldValue => $self->URI,
2661 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2665 return ( $Trans, $Msg );
2675 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2681 my %args = ( Target => '',
2687 unless ( $args{'Target'} || $args{'Base'} ) {
2688 $RT::Logger->error("Base or Target must be specified\n");
2689 return ( 0, $self->loc('Either base or target must be specified') );
2693 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2694 if ( !$right && $RT::StrictLinkACL ) {
2695 return ( 0, $self->loc("Permission Denied") );
2698 # If the other URI is an RT::Ticket, we want to make sure the user
2699 # can modify it too...
2700 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2701 return (0, $msg) unless $status;
2702 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2705 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2706 ( $RT::StrictLinkACL && $right < 2 ) )
2708 return ( 0, $self->loc("Permission Denied") );
2711 return $self->_AddLink(%args);
2714 sub __GetTicketFromURI {
2716 my %args = ( URI => '', @_ );
2718 # If the other URI is an RT::Ticket, we want to make sure the user
2719 # can modify it too...
2720 my $uri_obj = RT::URI->new( $self->CurrentUser );
2721 $uri_obj->FromURI( $args{'URI'} );
2723 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2724 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2725 $RT::Logger->warning( "$msg\n" );
2728 my $obj = $uri_obj->Resolver->Object;
2729 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2730 return (1, 'Found not a ticket', undef);
2732 return (1, 'Found ticket', $obj);
2737 Private non-acled variant of AddLink so that links can be added during create.
2743 my %args = ( Target => '',
2749 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2750 return ($val, $msg) if !$val || $exist;
2752 my ($direction, $remote_link);
2753 if ( $args{'Target'} ) {
2754 $remote_link = $args{'Target'};
2755 $direction = 'Base';
2756 } elsif ( $args{'Base'} ) {
2757 $remote_link = $args{'Base'};
2758 $direction = 'Target';
2761 # Don't write the transaction if we're doing this on create
2762 if ( $args{'Silent'} ) {
2763 return ( $val, $msg );
2766 my $remote_uri = RT::URI->new( $self->CurrentUser );
2767 $remote_uri->FromURI( $remote_link );
2769 #Write the transaction
2770 my ( $Trans, $Msg, $TransObj ) =
2771 $self->_NewTransaction(Type => 'AddLink',
2772 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2773 NewValue => $remote_uri->URI || $remote_link,
2776 if ( $remote_uri->IsLocal ) {
2778 my $OtherObj = $remote_uri->Object;
2779 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2780 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2781 : $LINKDIRMAP{$args{'Type'}}->{Target},
2782 NewValue => $self->URI,
2783 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2786 return ( $val, $Msg );
2798 MergeInto take the id of the ticket to merge this ticket into.
2803 my $t1 = RT::Ticket->new($RT::SystemUser);
2804 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2806 my $t2 = RT::Ticket->new($RT::SystemUser);
2807 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2809 my ($msg, $val) = $t1->MergeInto($t2->id);
2811 $t1 = RT::Ticket->new($RT::SystemUser);
2812 is ($t1->id, undef, "ok. we've got a blank ticket1");
2815 is ($t1->id, $t2->id);
2817 is ($t1->Requestors->MembersObj->Count, 2);
2826 my $ticket_id = shift;
2828 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2829 return ( 0, $self->loc("Permission Denied") );
2832 # Load up the new ticket.
2833 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2834 $MergeInto->Load($ticket_id);
2836 # make sure it exists.
2837 unless ( $MergeInto->Id ) {
2838 return ( 0, $self->loc("New ticket doesn't exist") );
2841 # Make sure the current user can modify the new ticket.
2842 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2843 return ( 0, $self->loc("Permission Denied") );
2846 $RT::Handle->BeginTransaction();
2848 # We use EffectiveId here even though it duplicates information from
2849 # the links table becasue of the massive performance hit we'd take
2850 # by trying to do a separate database query for merge info everytime
2853 #update this ticket's effective id to the new ticket's id.
2854 my ( $id_val, $id_msg ) = $self->__Set(
2855 Field => 'EffectiveId',
2856 Value => $MergeInto->Id()
2860 $RT::Handle->Rollback();
2861 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2865 if ( $self->__Value('Status') ne 'resolved' ) {
2867 my ( $status_val, $status_msg )
2868 = $self->__Set( Field => 'Status', Value => 'resolved' );
2870 unless ($status_val) {
2871 $RT::Handle->Rollback();
2874 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2878 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2882 # update all the links that point to that old ticket
2883 my $old_links_to = RT::Links->new($self->CurrentUser);
2884 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2887 while (my $link = $old_links_to->Next) {
2888 if (exists $old_seen{$link->Base."-".$link->Type}) {
2891 elsif ($link->Base eq $MergeInto->URI) {
2894 # First, make sure the link doesn't already exist. then move it over.
2895 my $tmp = RT::Link->new($RT::SystemUser);
2896 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2900 $link->SetTarget($MergeInto->URI);
2901 $link->SetLocalTarget($MergeInto->id);
2903 $old_seen{$link->Base."-".$link->Type} =1;
2908 my $old_links_from = RT::Links->new($self->CurrentUser);
2909 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2911 while (my $link = $old_links_from->Next) {
2912 if (exists $old_seen{$link->Type."-".$link->Target}) {
2915 if ($link->Target eq $MergeInto->URI) {
2918 # First, make sure the link doesn't already exist. then move it over.
2919 my $tmp = RT::Link->new($RT::SystemUser);
2920 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2924 $link->SetBase($MergeInto->URI);
2925 $link->SetLocalBase($MergeInto->id);
2926 $old_seen{$link->Type."-".$link->Target} =1;
2932 # Update time fields
2933 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2935 my $mutator = "Set$type";
2936 $MergeInto->$mutator(
2937 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2940 #add all of this ticket's watchers to that ticket.
2941 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2943 my $people = $self->$watcher_type->MembersObj;
2944 my $addwatcher_type = $watcher_type;
2945 $addwatcher_type =~ s/s$//;
2947 while ( my $watcher = $people->Next ) {
2949 my ($val, $msg) = $MergeInto->_AddWatcher(
2950 Type => $addwatcher_type,
2952 PrincipalId => $watcher->MemberId
2955 $RT::Logger->warning($msg);
2961 #find all of the tickets that were merged into this ticket.
2962 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2963 $old_mergees->Limit(
2964 FIELD => 'EffectiveId',
2969 # update their EffectiveId fields to the new ticket's id
2970 while ( my $ticket = $old_mergees->Next() ) {
2971 my ( $val, $msg ) = $ticket->__Set(
2972 Field => 'EffectiveId',
2973 Value => $MergeInto->Id()
2977 #make a new link: this ticket is merged into that other ticket.
2978 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2980 $MergeInto->_SetLastUpdated;
2982 $RT::Handle->Commit();
2983 return ( 1, $self->loc("Merge Successful") );
2990 # {{{ Routines dealing with ownership
2996 Takes nothing and returns an RT::User object of
3004 #If this gets ACLed, we lose on a rights check in User.pm and
3005 #get deep recursion. if we need ACLs here, we need
3006 #an equiv without ACLs
3008 my $owner = new RT::User( $self->CurrentUser );
3009 $owner->Load( $self->__Value('Owner') );
3011 #Return the owner object
3017 # {{{ sub OwnerAsString
3019 =head2 OwnerAsString
3021 Returns the owner's email address
3027 return ( $self->OwnerObj->EmailAddress );
3037 Takes two arguments:
3038 the Id or Name of the owner
3039 and (optionally) the type of the SetOwner Transaction. It defaults
3040 to 'Give'. 'Steal' is also a valid option.
3044 my $root = RT::User->new($RT::SystemUser);
3045 $root->Load('root');
3046 ok ($root->Id, "Loaded the root user");
3047 my $t = RT::Ticket->new($RT::SystemUser);
3049 $t->SetOwner('root');
3050 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
3052 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
3053 my $txns = RT::Transactions->new($RT::SystemUser);
3054 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3055 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
3056 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
3057 $txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
3059 my $steal = $txns->First;
3060 ok($steal->OldValue == $root->Id , "Stolen from root");
3061 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3069 my $NewOwner = shift;
3070 my $Type = shift || "Give";
3072 $RT::Handle->BeginTransaction();
3074 $self->_SetLastUpdated(); # lock the ticket
3075 $self->Load( $self->id ); # in case $self changed while waiting for lock
3077 my $OldOwnerObj = $self->OwnerObj;
3079 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3080 $NewOwnerObj->Load( $NewOwner );
3081 unless ( $NewOwnerObj->Id ) {
3082 $RT::Handle->Rollback();
3083 return ( 0, $self->loc("That user does not exist") );
3087 # must have ModifyTicket rights
3088 # or TakeTicket/StealTicket and $NewOwner is self
3089 # see if it's a take
3090 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3091 unless ( $self->CurrentUserHasRight('ModifyTicket')
3092 || $self->CurrentUserHasRight('TakeTicket') ) {
3093 $RT::Handle->Rollback();
3094 return ( 0, $self->loc("Permission Denied") );
3098 # see if it's a steal
3099 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
3100 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3102 unless ( $self->CurrentUserHasRight('ModifyTicket')
3103 || $self->CurrentUserHasRight('StealTicket') ) {
3104 $RT::Handle->Rollback();
3105 return ( 0, $self->loc("Permission Denied") );
3109 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3110 $RT::Handle->Rollback();
3111 return ( 0, $self->loc("Permission Denied") );
3115 # If we're not stealing and the ticket has an owner and it's not
3117 if ( $Type ne 'Steal' and $Type ne 'Force'
3118 and $OldOwnerObj->Id != $RT::Nobody->Id
3119 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3121 $RT::Handle->Rollback();
3122 return ( 0, $self->loc("You can only take tickets that are unowned") )
3123 if $NewOwnerObj->id == $self->CurrentUser->id;
3126 $self->loc("You can only reassign tickets that you own or that are unowned" )
3130 #If we've specified a new owner and that user can't modify the ticket
3131 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3132 $RT::Handle->Rollback();
3133 return ( 0, $self->loc("That user may not own tickets in that queue") );
3136 # If the ticket has an owner and it's the new owner, we don't need
3138 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3139 $RT::Handle->Rollback();
3140 return ( 0, $self->loc("That user already owns that ticket") );
3143 # Delete the owner in the owner group, then add a new one
3144 # TODO: is this safe? it's not how we really want the API to work
3145 # for most things, but it's fast.
3146 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3148 $RT::Handle->Rollback();
3149 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3152 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3153 PrincipalId => $NewOwnerObj->PrincipalId,
3154 InsideTransaction => 1 );
3156 $RT::Handle->Rollback();
3157 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3160 # We call set twice with slightly different arguments, so
3161 # as to not have an SQL transaction span two RT transactions
3163 my ( $val, $msg ) = $self->_Set(
3165 RecordTransaction => 0,
3166 Value => $NewOwnerObj->Id,
3168 TransactionType => $Type,
3169 CheckACL => 0, # don't check acl
3173 $RT::Handle->Rollback;
3174 return ( 0, $self->loc("Could not change owner. ") . $msg );
3177 ($val, $msg) = $self->_NewTransaction(
3180 NewValue => $NewOwnerObj->Id,
3181 OldValue => $OldOwnerObj->Id,
3186 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3187 $OldOwnerObj->Name, $NewOwnerObj->Name );
3190 $RT::Handle->Rollback();
3194 $RT::Handle->Commit();
3196 return ( $val, $msg );
3205 A convenince method to set the ticket's owner to the current user
3211 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3220 Convenience method to set the owner to 'nobody' if the current user is the owner.
3226 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3235 A convenience method to change the owner of the current ticket to the
3236 current user. Even if it's owned by another user.
3243 if ( $self->IsOwner( $self->CurrentUser ) ) {
3244 return ( 0, $self->loc("You already own this ticket") );
3247 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3257 # {{{ Routines dealing with status
3259 # {{{ sub ValidateStatus
3261 =head2 ValidateStatus STATUS
3263 Takes a string. Returns true if that status is a valid status for this ticket.
3264 Returns false otherwise.
3268 sub ValidateStatus {
3272 #Make sure the status passed in is valid
3273 unless ( $self->QueueObj->IsValidStatus($status) ) {
3285 =head2 SetStatus STATUS
3287 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3289 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.
3293 my $tt = RT::Ticket->new($RT::SystemUser);
3294 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3297 is($tt->Status, 'new', "New ticket is created as new");
3299 ($id, $msg) = $tt->SetStatus('open');
3301 like($msg, qr/open/i, "Status message is correct");
3302 ($id, $msg) = $tt->SetStatus('resolved');
3304 like($msg, qr/resolved/i, "Status message is correct");
3305 ($id, $msg) = $tt->SetStatus('resolved');
3319 $args{Status} = shift;
3326 if ( $args{Status} eq 'deleted') {
3327 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3328 return ( 0, $self->loc('Permission Denied') );
3331 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3332 return ( 0, $self->loc('Permission Denied') );
3336 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3337 return (0, $self->loc('That ticket has unresolved dependencies'));
3340 my $now = RT::Date->new( $self->CurrentUser );
3343 #If we're changing the status from new, record that we've started
3344 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3346 #Set the Started time to "now"
3347 $self->_Set( Field => 'Started',
3349 RecordTransaction => 0 );
3352 #When we close a ticket, set the 'Resolved' attribute to now.
3353 # It's misnamed, but that's just historical.
3354 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3355 $self->_Set( Field => 'Resolved',
3357 RecordTransaction => 0 );
3360 #Actually update the status
3361 my ($val, $msg)= $self->_Set( Field => 'Status',
3362 Value => $args{Status},
3365 TransactionType => 'Status' );
3376 Takes no arguments. Marks this ticket for garbage collection
3382 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3383 return $self->Delete;
3388 return ( $self->SetStatus('deleted') );
3390 # TODO: garbage collection
3399 Sets this ticket's status to stalled
3405 return ( $self->SetStatus('stalled') );
3414 Sets this ticket's status to rejected
3420 return ( $self->SetStatus('rejected') );
3429 Sets this ticket\'s status to Open
3435 return ( $self->SetStatus('open') );
3444 Sets this ticket\'s status to Resolved
3450 return ( $self->SetStatus('resolved') );
3458 # {{{ Actions + Routines dealing with transactions
3460 # {{{ sub SetTold and _SetTold
3462 =head2 SetTold ISO [TIMETAKEN]
3464 Updates the told and records a transaction
3471 $told = shift if (@_);
3472 my $timetaken = shift || 0;
3474 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3475 return ( 0, $self->loc("Permission Denied") );
3478 my $datetold = new RT::Date( $self->CurrentUser );
3480 $datetold->Set( Format => 'iso',
3484 $datetold->SetToNow();
3487 return ( $self->_Set( Field => 'Told',
3488 Value => $datetold->ISO,
3489 TimeTaken => $timetaken,
3490 TransactionType => 'Told' ) );
3495 Updates the told without a transaction or acl check. Useful when we're sending replies.
3502 my $now = new RT::Date( $self->CurrentUser );
3505 #use __Set to get no ACLs ;)
3506 return ( $self->__Set( Field => 'Told',
3507 Value => $now->ISO ) );
3512 =head2 TransactionBatch
3514 Returns an array reference of all transactions created on this ticket during
3515 this ticket object's lifetime, or undef if there were none.
3517 Only works when the $RT::UseTransactionBatch config variable is set to true.
3521 sub TransactionBatch {
3523 return $self->{_TransactionBatch};
3529 # DESTROY methods need to localize $@, or it may unset it. This
3530 # causes $m->abort to not bubble all of the way up. See perlbug
3531 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3534 # The following line eliminates reentrancy.
3535 # It protects against the fact that perl doesn't deal gracefully
3536 # when an object's refcount is changed in its destructor.
3537 return if $self->{_Destroyed}++;
3539 my $batch = $self->TransactionBatch or return;
3540 return unless @$batch;
3543 RT::Scrips->new($RT::SystemUser)->Apply(
3544 Stage => 'TransactionBatch',
3546 TransactionObj => $batch->[0],
3547 Type => join(',', (map { $_->Type } @{$batch}) )
3553 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3555 # {{{ sub _OverlayAccessible
3557 sub _OverlayAccessible {
3559 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3560 Queue => { 'read' => 1, 'write' => 1 },
3561 Requestors => { 'read' => 1, 'write' => 1 },
3562 Owner => { 'read' => 1, 'write' => 1 },
3563 Subject => { 'read' => 1, 'write' => 1 },
3564 InitialPriority => { 'read' => 1, 'write' => 1 },
3565 FinalPriority => { 'read' => 1, 'write' => 1 },
3566 Priority => { 'read' => 1, 'write' => 1 },
3567 Status => { 'read' => 1, 'write' => 1 },
3568 TimeEstimated => { 'read' => 1, 'write' => 1 },
3569 TimeWorked => { 'read' => 1, 'write' => 1 },
3570 TimeLeft => { 'read' => 1, 'write' => 1 },
3571 Told => { 'read' => 1, 'write' => 1 },
3572 Resolved => { 'read' => 1 },
3573 Type => { 'read' => 1 },
3574 Starts => { 'read' => 1, 'write' => 1 },
3575 Started => { 'read' => 1, 'write' => 1 },
3576 Due => { 'read' => 1, 'write' => 1 },
3577 Creator => { 'read' => 1, 'auto' => 1 },
3578 Created => { 'read' => 1, 'auto' => 1 },
3579 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3580 LastUpdated => { 'read' => 1, 'auto' => 1 }
3592 my %args = ( Field => undef,
3595 RecordTransaction => 1,
3598 TransactionType => 'Set',
3601 if ($args{'CheckACL'}) {
3602 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3603 return ( 0, $self->loc("Permission Denied"));
3607 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3608 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3609 return(0, $self->loc("Internal Error"));
3612 #if the user is trying to modify the record
3614 #Take care of the old value we really don't want to get in an ACL loop.
3615 # so ask the super::_Value
3616 my $Old = $self->SUPER::_Value("$args{'Field'}");
3619 if ( $args{'UpdateTicket'} ) {
3622 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3623 Value => $args{'Value'} );
3625 #If we can't actually set the field to the value, don't record
3626 # a transaction. instead, get out of here.
3627 return ( 0, $msg ) unless $ret;
3630 if ( $args{'RecordTransaction'} == 1 ) {
3632 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3633 Type => $args{'TransactionType'},
3634 Field => $args{'Field'},
3635 NewValue => $args{'Value'},
3637 TimeTaken => $args{'TimeTaken'},
3639 return ( $Trans, scalar $TransObj->BriefDescription );
3642 return ( $ret, $msg );
3652 Takes the name of a table column.
3653 Returns its value as a string, if the user passes an ACL check
3662 #if the field is public, return it.
3663 if ( $self->_Accessible( $field, 'public' ) ) {
3665 #$RT::Logger->debug("Skipping ACL check for $field\n");
3666 return ( $self->SUPER::_Value($field) );
3670 #If the current user doesn't have ACLs, don't let em at it.
3672 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3675 return ( $self->SUPER::_Value($field) );
3681 # {{{ sub _UpdateTimeTaken
3683 =head2 _UpdateTimeTaken
3685 This routine will increment the timeworked counter. it should
3686 only be called from _NewTransaction
3690 sub _UpdateTimeTaken {
3692 my $Minutes = shift;
3695 $Total = $self->SUPER::_Value("TimeWorked");
3696 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3698 Field => "TimeWorked",
3709 # {{{ Routines dealing with ACCESS CONTROL
3711 # {{{ sub CurrentUserHasRight
3713 =head2 CurrentUserHasRight
3715 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3716 1 if the user has that right. It returns 0 if the user doesn't have that right.
3720 sub CurrentUserHasRight {
3726 Principal => $self->CurrentUser->UserObj(),
3739 Takes a paramhash with the attributes 'Right' and 'Principal'
3740 'Right' is a ticket-scoped textual right from RT::ACE
3741 'Principal' is an RT::User object
3743 Returns 1 if the principal has the right. Returns undef if not.
3755 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3758 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3763 $args{'Principal'}->HasRight(
3765 Right => $args{'Right'}
3776 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3777 It isn't acutally a searchbuilder collection itself.
3784 unless ($self->{'__reminders'}) {
3785 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3786 $self->{'__reminders'}->Ticket($self->id);
3788 return $self->{'__reminders'};
3794 # {{{ sub Transactions
3798 Returns an RT::Transactions object of all transactions on this ticket
3805 my $transactions = RT::Transactions->new( $self->CurrentUser );
3807 #If the user has no rights, return an empty object
3808 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3809 $transactions->LimitToTicket($self->id);
3811 # if the user may not see comments do not return them
3812 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3813 $transactions->Limit(
3818 $transactions->Limit(
3821 VALUE => "CommentEmailRecord",
3822 ENTRYAGGREGATOR => 'AND'
3828 return ($transactions);
3834 # {{{ TransactionCustomFields
3836 =head2 TransactionCustomFields
3838 Returns the custom fields that transactions on tickets will have.
3842 sub TransactionCustomFields {
3844 return $self->QueueObj->TicketTransactionCustomFields;
3849 # {{{ sub CustomFieldValues
3851 =head2 CustomFieldValues
3853 # Do name => id mapping (if needed) before falling back to
3854 # RT::Record's CustomFieldValues
3860 sub CustomFieldValues {
3864 return $self->SUPER::CustomFieldValues( $field )
3865 if !$field || $field =~ /^\d+$/;
3867 my $cf = RT::CustomField->new( $self->CurrentUser );
3868 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3869 unless ( $cf->id ) {
3870 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3873 # If we didn't find a valid cfid, give up.
3874 return RT::ObjectCustomFieldValues->new( $self->CurrentUser )
3877 return $self->SUPER::CustomFieldValues( $cf->id );
3882 # {{{ sub CustomFieldLookupType
3884 =head2 CustomFieldLookupType
3886 Returns the RT::Ticket lookup type, which can be passed to
3887 RT::CustomField->Create() via the 'LookupType' hash key.
3893 sub CustomFieldLookupType {
3894 "RT::Queue-RT::Ticket";
3901 Jesse Vincent, jesse@bestpractical.com