1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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 # XXX, FIXME, BUG: if only email is provided then we only check
1407 # for ModifyTicket right, but must try to get PrincipalId and
1408 # check Watch* rights too if user exist
1411 #If the watcher we're trying to add is for the current user
1412 if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0)
1413 or lc( $self->CurrentUser->UserObj->EmailAddress )
1414 eq lc( RT::User->CanonicalizeEmailAddress( $args{'Email'} ) || '' ) )
1416 # If it's an AdminCc and they don't have
1417 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1418 if ( $args{'Type'} eq 'AdminCc' ) {
1419 unless ( $self->CurrentUserHasRight('ModifyTicket')
1420 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1421 return ( 0, $self->loc('Permission Denied'))
1425 # If it's a Requestor or Cc and they don't have
1426 # 'Watch' or 'ModifyTicket', bail
1427 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1429 unless ( $self->CurrentUserHasRight('ModifyTicket')
1430 or $self->CurrentUserHasRight('Watch') ) {
1431 return ( 0, $self->loc('Permission Denied'))
1435 $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1436 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1440 # If the watcher isn't the current user
1441 # and the current user doesn't have 'ModifyTicket'
1444 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1445 return ( 0, $self->loc("Permission Denied") );
1451 return ( $self->_AddWatcher(%args) );
1454 #This contains the meat of AddWatcher. but can be called from a routine like
1455 # Create, which doesn't need the additional acl check
1461 PrincipalId => undef,
1467 my $principal = RT::Principal->new($self->CurrentUser);
1468 if ($args{'Email'}) {
1469 my $user = RT::User->new($RT::SystemUser);
1470 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1471 # If we can't load the user by email address, let's try to load by username
1473 ($pid,$msg) = $user->Load($args{'Email'})
1476 $args{'PrincipalId'} = $pid;
1479 if ($args{'PrincipalId'}) {
1480 $principal->Load($args{'PrincipalId'});
1484 # If we can't find this watcher, we need to bail.
1485 unless ($principal->Id) {
1486 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1487 return(0, $self->loc("Could not find or create that user"));
1491 my $group = RT::Group->new($self->CurrentUser);
1492 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1493 unless ($group->id) {
1494 return(0,$self->loc("Group not found"));
1497 if ( $group->HasMember( $principal)) {
1499 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1503 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1504 InsideTransaction => 1 );
1506 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1508 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1511 unless ( $args{'Silent'} ) {
1512 $self->_NewTransaction(
1513 Type => 'AddWatcher',
1514 NewValue => $principal->Id,
1515 Field => $args{'Type'}
1519 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1525 # {{{ sub DeleteWatcher
1527 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1530 Deletes a Ticket watcher. Takes two arguments:
1532 Type (one of Requestor,Cc,AdminCc)
1536 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1538 Email (the email address of an existing wathcer)
1547 my %args = ( Type => undef,
1548 PrincipalId => undef,
1552 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1553 return ( 0, $self->loc("No principal specified") );
1555 my $principal = RT::Principal->new( $self->CurrentUser );
1556 if ( $args{'PrincipalId'} ) {
1558 $principal->Load( $args{'PrincipalId'} );
1561 my $user = RT::User->new( $self->CurrentUser );
1562 $user->LoadByEmail( $args{'Email'} );
1563 $principal->Load( $user->Id );
1566 # If we can't find this watcher, we need to bail.
1567 unless ( $principal->Id ) {
1568 return ( 0, $self->loc("Could not find that principal") );
1571 my $group = RT::Group->new( $self->CurrentUser );
1572 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1573 unless ( $group->id ) {
1574 return ( 0, $self->loc("Group not found") );
1578 #If the watcher we're trying to add is for the current user
1579 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1581 # If it's an AdminCc and they don't have
1582 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1583 if ( $args{'Type'} eq 'AdminCc' ) {
1584 unless ( $self->CurrentUserHasRight('ModifyTicket')
1585 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1586 return ( 0, $self->loc('Permission Denied') );
1590 # If it's a Requestor or Cc and they don't have
1591 # 'Watch' or 'ModifyTicket', bail
1592 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1594 unless ( $self->CurrentUserHasRight('ModifyTicket')
1595 or $self->CurrentUserHasRight('Watch') ) {
1596 return ( 0, $self->loc('Permission Denied') );
1600 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1602 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1606 # If the watcher isn't the current user
1607 # and the current user doesn't have 'ModifyTicket' bail
1609 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1610 return ( 0, $self->loc("Permission Denied") );
1616 # see if this user is already a watcher.
1618 unless ( $group->HasMember($principal) ) {
1620 $self->loc( 'That principal is not a [_1] for this ticket',
1624 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1626 $RT::Logger->error( "Failed to delete "
1628 . " as a member of group "
1634 'Could not remove that principal as a [_1] for this ticket',
1638 unless ( $args{'Silent'} ) {
1639 $self->_NewTransaction( Type => 'DelWatcher',
1640 OldValue => $principal->Id,
1641 Field => $args{'Type'} );
1645 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1646 $principal->Object->Name,
1655 =head2 SquelchMailTo [EMAIL]
1657 Takes an optional email address to never email about updates to this ticket.
1660 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1664 my $t = RT::Ticket->new($RT::SystemUser);
1665 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1667 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1669 my @returned = $t->SquelchMailTo('nobody@example.com');
1671 is($#returned, 0, "The ticket has one squelched recipients");
1673 my @names = $t->Attributes->Names;
1674 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1675 @returned = $t->SquelchMailTo('nobody@example.com');
1678 is($#returned, 0, "The ticket has one squelched recipients");
1680 @names = $t->Attributes->Names;
1681 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1684 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1685 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1686 @returned = $t->SquelchMailTo();
1687 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1697 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1701 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1702 unless grep { $_->Content eq $attr }
1703 $self->Attributes->Named('SquelchMailTo');
1706 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1709 my @attributes = $self->Attributes->Named('SquelchMailTo');
1710 return (@attributes);
1714 =head2 UnsquelchMailTo ADDRESS
1716 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1718 Returns a tuple of (status, message)
1722 sub UnsquelchMailTo {
1725 my $address = shift;
1726 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1727 return ( 0, $self->loc("Permission Denied") );
1730 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1731 return ($val, $msg);
1735 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1737 =head2 RequestorAddresses
1739 B<Returns> String: All Ticket Requestor email addresses as a string.
1743 sub RequestorAddresses {
1746 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1750 return ( $self->Requestors->MemberEmailAddressesAsString );
1754 =head2 AdminCcAddresses
1756 returns String: All Ticket AdminCc email addresses as a string
1760 sub AdminCcAddresses {
1763 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1767 return ( $self->AdminCc->MemberEmailAddressesAsString )
1773 returns String: All Ticket Ccs as a string of email addresses
1780 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1784 return ( $self->Cc->MemberEmailAddressesAsString);
1790 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1792 # {{{ sub Requestors
1797 Returns this ticket's Requestors as an RT::Group object
1804 my $group = RT::Group->new($self->CurrentUser);
1805 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1806 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1814 # {{{ sub _Requestors
1818 Private non-ACLed variant of Reqeustors so that we can look them up for the
1819 purposes of customer auto-association during create.
1826 my $group = RT::Group->new($RT::SystemUser);
1827 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1838 Returns an RT::Group object which contains this ticket's Ccs.
1839 If the user doesn't have "ShowTicket" permission, returns an empty group
1846 my $group = RT::Group->new($self->CurrentUser);
1847 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1848 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1861 Returns an RT::Group object which contains this ticket's AdminCcs.
1862 If the user doesn't have "ShowTicket" permission, returns an empty group
1869 my $group = RT::Group->new($self->CurrentUser);
1870 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1871 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1881 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1884 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1886 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1888 Takes a param hash with the attributes Type and either PrincipalId or Email
1890 Type is one of Requestor, Cc, AdminCc and Owner
1892 PrincipalId is an RT::Principal id, and Email is an email address.
1894 Returns true if the specified principal (or the one corresponding to the
1895 specified address) is a member of the group Type for this ticket.
1897 XX TODO: This should be Memoized.
1904 my %args = ( Type => 'Requestor',
1905 PrincipalId => undef,
1910 # Load the relevant group.
1911 my $group = RT::Group->new($self->CurrentUser);
1912 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1914 # Find the relevant principal.
1915 my $principal = RT::Principal->new($self->CurrentUser);
1916 if (!$args{PrincipalId} && $args{Email}) {
1917 # Look up the specified user.
1918 my $user = RT::User->new($self->CurrentUser);
1919 $user->LoadByEmail($args{Email});
1921 $args{PrincipalId} = $user->PrincipalId;
1924 # A non-existent user can't be a group member.
1928 $principal->Load($args{'PrincipalId'});
1930 # Ask if it has the member in question
1931 return ($group->HasMember($principal));
1936 # {{{ sub IsRequestor
1938 =head2 IsRequestor PRINCIPAL_ID
1940 Takes an RT::Principal id
1941 Returns true if the principal is a requestor of the current ticket.
1950 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1958 =head2 IsCc PRINCIPAL_ID
1960 Takes an RT::Principal id.
1961 Returns true if the principal is a requestor of the current ticket.
1970 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1978 =head2 IsAdminCc PRINCIPAL_ID
1980 Takes an RT::Principal id.
1981 Returns true if the principal is a requestor of the current ticket.
1989 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1999 Takes an RT::User object. Returns true if that user is this ticket's owner.
2000 returns undef otherwise
2008 # no ACL check since this is used in acl decisions
2009 # unless ($self->CurrentUserHasRight('ShowTicket')) {
2013 #Tickets won't yet have owners when they're being created.
2014 unless ( $self->OwnerObj->id ) {
2018 if ( $person->id == $self->OwnerObj->id ) {
2032 # {{{ Routines dealing with queues
2034 # {{{ sub ValidateQueue
2041 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
2045 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2046 my $id = $QueueObj->Load($Value);
2062 my $NewQueue = shift;
2064 #Redundant. ACL gets checked in _Set;
2065 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2066 return ( 0, $self->loc("Permission Denied") );
2069 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2070 $NewQueueObj->Load($NewQueue);
2072 unless ( $NewQueueObj->Id() ) {
2073 return ( 0, $self->loc("That queue does not exist") );
2076 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2077 return ( 0, $self->loc('That is the same value') );
2080 $self->CurrentUser->HasRight(
2081 Right => 'CreateTicket',
2082 Object => $NewQueueObj
2086 return ( 0, $self->loc("You may not create requests in that queue.") );
2090 $self->OwnerObj->HasRight(
2091 Right => 'OwnTicket',
2092 Object => $NewQueueObj
2096 my $clone = RT::Ticket->new( $RT::SystemUser );
2097 $clone->Load( $self->Id );
2098 unless ( $clone->Id ) {
2099 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2101 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2102 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2105 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2114 Takes nothing. returns this ticket's queue object
2121 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2123 #We call __Value so that we can avoid the ACL decision and some deep recursion
2124 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2125 return ($queue_obj);
2132 # {{{ Date printing routines
2138 Returns an RT::Date object containing this ticket's due date
2145 my $time = new RT::Date( $self->CurrentUser );
2147 # -1 is RT::Date slang for never
2149 $time->Set( Format => 'sql', Value => $self->Due );
2152 $time->Set( Format => 'unix', Value => -1 );
2160 # {{{ sub DueAsString
2164 Returns this ticket's due date as a human readable string
2170 return $self->DueObj->AsString();
2175 # {{{ sub ResolvedObj
2179 Returns an RT::Date object of this ticket's 'resolved' time.
2186 my $time = new RT::Date( $self->CurrentUser );
2187 $time->Set( Format => 'sql', Value => $self->Resolved );
2193 # {{{ sub SetStarted
2197 Takes a date in ISO format or undef
2198 Returns a transaction id and a message
2199 The client calls "Start" to note that the project was started on the date in $date.
2200 A null date means "now"
2206 my $time = shift || 0;
2208 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2209 return ( 0, $self->loc("Permission Denied") );
2212 #We create a date object to catch date weirdness
2213 my $time_obj = new RT::Date( $self->CurrentUser() );
2215 $time_obj->Set( Format => 'ISO', Value => $time );
2218 $time_obj->SetToNow();
2221 #Now that we're starting, open this ticket
2222 #TODO do we really want to force this as policy? it should be a scrip
2224 #We need $TicketAsSystem, in case the current user doesn't have
2227 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2228 $TicketAsSystem->Load( $self->Id );
2229 if ( $TicketAsSystem->Status eq 'new' ) {
2230 $TicketAsSystem->Open();
2233 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2239 # {{{ sub StartedObj
2243 Returns an RT::Date object which contains this ticket's
2251 my $time = new RT::Date( $self->CurrentUser );
2252 $time->Set( Format => 'sql', Value => $self->Started );
2262 Returns an RT::Date object which contains this ticket's
2270 my $time = new RT::Date( $self->CurrentUser );
2271 $time->Set( Format => 'sql', Value => $self->Starts );
2281 Returns an RT::Date object which contains this ticket's
2289 my $time = new RT::Date( $self->CurrentUser );
2290 $time->Set( Format => 'sql', Value => $self->Told );
2296 # {{{ sub ToldAsString
2300 A convenience method that returns ToldObj->AsString
2302 TODO: This should be deprecated
2308 if ( $self->Told ) {
2309 return $self->ToldObj->AsString();
2318 # {{{ sub TimeWorkedAsString
2320 =head2 TimeWorkedAsString
2322 Returns the amount of time worked on this ticket as a Text String
2326 sub TimeWorkedAsString {
2328 return "0" unless $self->TimeWorked;
2330 #This is not really a date object, but if we diff a number of seconds
2331 #vs the epoch, we'll get a nice description of time worked.
2333 my $worked = new RT::Date( $self->CurrentUser );
2335 #return the #of minutes worked turned into seconds and written as
2336 # a simple text string
2338 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2345 # {{{ Routines dealing with correspondence/comments
2351 Comment on this ticket.
2352 Takes a hashref with the following attributes:
2353 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2356 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2358 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2359 They will, however, be prepared and you'll be able to access them through the TransactionObj
2361 Returns: Transaction id, Error Message, Transaction Object
2362 (note the different order from Create()!)
2369 my %args = ( CcMessageTo => undef,
2370 BccMessageTo => undef,
2377 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2378 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2379 return ( 0, $self->loc("Permission Denied"), undef );
2381 $args{'NoteType'} = 'Comment';
2383 if ($args{'DryRun'}) {
2384 $RT::Handle->BeginTransaction();
2385 $args{'CommitScrips'} = 0;
2388 my @results = $self->_RecordNote(%args);
2389 if ($args{'DryRun'}) {
2390 $RT::Handle->Rollback();
2397 # {{{ sub Correspond
2401 Correspond on this ticket.
2402 Takes a hashref with the following attributes:
2405 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2407 if there's no MIMEObj, Content is used to build a MIME::Entity object
2409 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2410 They will, however, be prepared and you'll be able to access them through the TransactionObj
2412 Returns: Transaction id, Error Message, Transaction Object
2413 (note the different order from Create()!)
2420 my %args = ( CcMessageTo => undef,
2421 BccMessageTo => undef,
2427 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2428 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2429 return ( 0, $self->loc("Permission Denied"), undef );
2432 $args{'NoteType'} = 'Correspond';
2433 if ($args{'DryRun'}) {
2434 $RT::Handle->BeginTransaction();
2435 $args{'CommitScrips'} = 0;
2438 my @results = $self->_RecordNote(%args);
2440 #Set the last told date to now if this isn't mail from the requestor.
2441 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2442 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2444 if ($args{'DryRun'}) {
2445 $RT::Handle->Rollback();
2454 # {{{ sub _RecordNote
2458 the meat of both comment and correspond.
2460 Performs no access control checks. hence, dangerous.
2467 my %args = ( CcMessageTo => undef,
2468 BccMessageTo => undef,
2475 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2476 return ( 0, $self->loc("No message attached"), undef );
2478 unless ( $args{'MIMEObj'} ) {
2479 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2480 ref $args{'Content'}
2482 : [ $args{'Content'} ]
2486 # convert text parts into utf-8
2487 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2489 # If we've been passed in CcMessageTo and BccMessageTo fields,
2490 # add them to the mime object for passing on to the transaction handler
2491 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2495 foreach my $type (qw/Cc Bcc/) {
2496 if ( defined $args{ $type . 'MessageTo' } ) {
2498 my $addresses = join ', ', (
2499 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2500 Mail::Address->parse( $args{ $type . 'MessageTo' } ) );
2501 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2505 # If this is from an external source, we need to come up with its
2506 # internal Message-ID now, so all emails sent because of this
2507 # message have a common Message-ID
2508 unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2509 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2511 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2513 . $RT::VERSION . "-"
2515 . CORE::time() . "-"
2516 . int(rand(2000)) . '.'
2519 . "0" . "@" # Email sent
2524 #Record the correspondence (write the transaction)
2525 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2526 Type => $args{'NoteType'},
2527 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2528 TimeTaken => $args{'TimeTaken'},
2529 MIMEObj => $args{'MIMEObj'},
2530 CommitScrips => $args{'CommitScrips'},
2534 $RT::Logger->err("$self couldn't init a transaction $msg");
2535 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2538 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2550 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2553 my $type = shift || "";
2555 unless ( $self->{"$field$type"} ) {
2556 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2558 #not sure what this ACL was supposed to do... but returning the
2559 # bare (unlimited) RT::Links certainly seems wrong, it causes the
2560 # $Ticket->Customers method during creation to return results for every
2562 #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2564 # Maybe this ticket is a merged ticket
2565 my $Tickets = new RT::Tickets( $self->CurrentUser );
2566 # at least to myself
2567 $self->{"$field$type"}->Limit( FIELD => $field,
2568 VALUE => $self->URI,
2569 ENTRYAGGREGATOR => 'OR' );
2570 $Tickets->Limit( FIELD => 'EffectiveId',
2571 VALUE => $self->EffectiveId );
2572 while (my $Ticket = $Tickets->Next) {
2573 $self->{"$field$type"}->Limit( FIELD => $field,
2574 VALUE => $Ticket->URI,
2575 ENTRYAGGREGATOR => 'OR' );
2577 $self->{"$field$type"}->Limit( FIELD => 'Type',
2582 return ( $self->{"$field$type"} );
2587 # {{{ sub DeleteLink
2591 Delete a link. takes a paramhash of Base, Target and Type.
2592 Either Base or Target must be null. The null value will
2593 be replaced with this ticket\'s id
2606 unless ( $args{'Target'} || $args{'Base'} ) {
2607 $RT::Logger->error("Base or Target must be specified\n");
2608 return ( 0, $self->loc('Either base or target must be specified') );
2613 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2614 if ( !$right && $RT::StrictLinkACL ) {
2615 return ( 0, $self->loc("Permission Denied") );
2618 # If the other URI is an RT::Ticket, we want to make sure the user
2619 # can modify it too...
2620 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2621 return (0, $msg) unless $status;
2622 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2625 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2626 ( $RT::StrictLinkACL && $right < 2 ) )
2628 return ( 0, $self->loc("Permission Denied") );
2631 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2634 $RT::Logger->debug("Couldn't find that link\n");
2638 my ($direction, $remote_link);
2640 if ( $args{'Base'} ) {
2641 $remote_link = $args{'Base'};
2642 $direction = 'Target';
2644 elsif ( $args{'Target'} ) {
2645 $remote_link = $args{'Target'};
2649 if ( $args{'Silent'} ) {
2650 return ( $val, $Msg );
2653 my $remote_uri = RT::URI->new( $self->CurrentUser );
2654 $remote_uri->FromURI( $remote_link );
2656 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2657 Type => 'DeleteLink',
2658 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2659 OldValue => $remote_uri->URI || $remote_link,
2663 if ( $remote_uri->IsLocal ) {
2665 my $OtherObj = $remote_uri->Object;
2666 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2667 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2668 : $LINKDIRMAP{$args{'Type'}}->{Target},
2669 OldValue => $self->URI,
2670 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2674 return ( $Trans, $Msg );
2684 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2690 my %args = ( Target => '',
2696 unless ( $args{'Target'} || $args{'Base'} ) {
2697 $RT::Logger->error("Base or Target must be specified\n");
2698 return ( 0, $self->loc('Either base or target must be specified') );
2702 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2703 if ( !$right && $RT::StrictLinkACL ) {
2704 return ( 0, $self->loc("Permission Denied") );
2707 # If the other URI is an RT::Ticket, we want to make sure the user
2708 # can modify it too...
2709 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2710 return (0, $msg) unless $status;
2711 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2714 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2715 ( $RT::StrictLinkACL && $right < 2 ) )
2717 return ( 0, $self->loc("Permission Denied") );
2720 return $self->_AddLink(%args);
2723 sub __GetTicketFromURI {
2725 my %args = ( URI => '', @_ );
2727 # If the other URI is an RT::Ticket, we want to make sure the user
2728 # can modify it too...
2729 my $uri_obj = RT::URI->new( $self->CurrentUser );
2730 $uri_obj->FromURI( $args{'URI'} );
2732 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2733 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2734 $RT::Logger->warning( "$msg\n" );
2737 my $obj = $uri_obj->Resolver->Object;
2738 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2739 return (1, 'Found not a ticket', undef);
2741 return (1, 'Found ticket', $obj);
2746 Private non-acled variant of AddLink so that links can be added during create.
2752 my %args = ( Target => '',
2758 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2759 return ($val, $msg) if !$val || $exist;
2761 my ($direction, $remote_link);
2762 if ( $args{'Target'} ) {
2763 $remote_link = $args{'Target'};
2764 $direction = 'Base';
2765 } elsif ( $args{'Base'} ) {
2766 $remote_link = $args{'Base'};
2767 $direction = 'Target';
2770 # Don't write the transaction if we're doing this on create
2771 if ( $args{'Silent'} ) {
2772 return ( $val, $msg );
2775 my $remote_uri = RT::URI->new( $self->CurrentUser );
2776 $remote_uri->FromURI( $remote_link );
2778 #Write the transaction
2779 my ( $Trans, $Msg, $TransObj ) =
2780 $self->_NewTransaction(Type => 'AddLink',
2781 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2782 NewValue => $remote_uri->URI || $remote_link,
2785 if ( $remote_uri->IsLocal ) {
2787 my $OtherObj = $remote_uri->Object;
2788 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2789 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2790 : $LINKDIRMAP{$args{'Type'}}->{Target},
2791 NewValue => $self->URI,
2792 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2795 return ( $val, $Msg );
2807 MergeInto take the id of the ticket to merge this ticket into.
2812 my $t1 = RT::Ticket->new($RT::SystemUser);
2813 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2815 my $t2 = RT::Ticket->new($RT::SystemUser);
2816 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2818 my ($msg, $val) = $t1->MergeInto($t2->id);
2820 $t1 = RT::Ticket->new($RT::SystemUser);
2821 is ($t1->id, undef, "ok. we've got a blank ticket1");
2824 is ($t1->id, $t2->id);
2826 is ($t1->Requestors->MembersObj->Count, 2);
2835 my $ticket_id = shift;
2837 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2838 return ( 0, $self->loc("Permission Denied") );
2841 # Load up the new ticket.
2842 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2843 $MergeInto->Load($ticket_id);
2845 # make sure it exists.
2846 unless ( $MergeInto->Id ) {
2847 return ( 0, $self->loc("New ticket doesn't exist") );
2850 # Make sure the current user can modify the new ticket.
2851 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2852 return ( 0, $self->loc("Permission Denied") );
2855 $RT::Handle->BeginTransaction();
2857 # We use EffectiveId here even though it duplicates information from
2858 # the links table becasue of the massive performance hit we'd take
2859 # by trying to do a separate database query for merge info everytime
2862 #update this ticket's effective id to the new ticket's id.
2863 my ( $id_val, $id_msg ) = $self->__Set(
2864 Field => 'EffectiveId',
2865 Value => $MergeInto->Id()
2869 $RT::Handle->Rollback();
2870 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2874 if ( $self->__Value('Status') ne 'resolved' ) {
2876 my ( $status_val, $status_msg )
2877 = $self->__Set( Field => 'Status', Value => 'resolved' );
2879 unless ($status_val) {
2880 $RT::Handle->Rollback();
2883 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2887 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2891 # update all the links that point to that old ticket
2892 my $old_links_to = RT::Links->new($self->CurrentUser);
2893 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2896 while (my $link = $old_links_to->Next) {
2897 if (exists $old_seen{$link->Base."-".$link->Type}) {
2900 elsif ($link->Base eq $MergeInto->URI) {
2903 # First, make sure the link doesn't already exist. then move it over.
2904 my $tmp = RT::Link->new($RT::SystemUser);
2905 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2909 $link->SetTarget($MergeInto->URI);
2910 $link->SetLocalTarget($MergeInto->id);
2912 $old_seen{$link->Base."-".$link->Type} =1;
2917 my $old_links_from = RT::Links->new($self->CurrentUser);
2918 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2920 while (my $link = $old_links_from->Next) {
2921 if (exists $old_seen{$link->Type."-".$link->Target}) {
2924 if ($link->Target eq $MergeInto->URI) {
2927 # First, make sure the link doesn't already exist. then move it over.
2928 my $tmp = RT::Link->new($RT::SystemUser);
2929 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2933 $link->SetBase($MergeInto->URI);
2934 $link->SetLocalBase($MergeInto->id);
2935 $old_seen{$link->Type."-".$link->Target} =1;
2941 # Update time fields
2942 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2944 my $mutator = "Set$type";
2945 $MergeInto->$mutator(
2946 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2949 #add all of this ticket's watchers to that ticket.
2950 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2952 my $people = $self->$watcher_type->MembersObj;
2953 my $addwatcher_type = $watcher_type;
2954 $addwatcher_type =~ s/s$//;
2956 while ( my $watcher = $people->Next ) {
2958 my ($val, $msg) = $MergeInto->_AddWatcher(
2959 Type => $addwatcher_type,
2961 PrincipalId => $watcher->MemberId
2964 $RT::Logger->warning($msg);
2970 #find all of the tickets that were merged into this ticket.
2971 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2972 $old_mergees->Limit(
2973 FIELD => 'EffectiveId',
2978 # update their EffectiveId fields to the new ticket's id
2979 while ( my $ticket = $old_mergees->Next() ) {
2980 my ( $val, $msg ) = $ticket->__Set(
2981 Field => 'EffectiveId',
2982 Value => $MergeInto->Id()
2986 #make a new link: this ticket is merged into that other ticket.
2987 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2989 $MergeInto->_SetLastUpdated;
2991 $RT::Handle->Commit();
2992 return ( 1, $self->loc("Merge Successful") );
2999 # {{{ Routines dealing with ownership
3005 Takes nothing and returns an RT::User object of
3013 #If this gets ACLed, we lose on a rights check in User.pm and
3014 #get deep recursion. if we need ACLs here, we need
3015 #an equiv without ACLs
3017 my $owner = new RT::User( $self->CurrentUser );
3018 $owner->Load( $self->__Value('Owner') );
3020 #Return the owner object
3026 # {{{ sub OwnerAsString
3028 =head2 OwnerAsString
3030 Returns the owner's email address
3036 return ( $self->OwnerObj->EmailAddress );
3046 Takes two arguments:
3047 the Id or Name of the owner
3048 and (optionally) the type of the SetOwner Transaction. It defaults
3049 to 'Give'. 'Steal' is also a valid option.
3053 my $root = RT::User->new($RT::SystemUser);
3054 $root->Load('root');
3055 ok ($root->Id, "Loaded the root user");
3056 my $t = RT::Ticket->new($RT::SystemUser);
3058 $t->SetOwner('root');
3059 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
3061 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
3062 my $txns = RT::Transactions->new($RT::SystemUser);
3063 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3064 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
3065 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
3066 $txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
3068 my $steal = $txns->First;
3069 ok($steal->OldValue == $root->Id , "Stolen from root");
3070 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3078 my $NewOwner = shift;
3079 my $Type = shift || "Give";
3081 $RT::Handle->BeginTransaction();
3083 $self->_SetLastUpdated(); # lock the ticket
3084 $self->Load( $self->id ); # in case $self changed while waiting for lock
3086 my $OldOwnerObj = $self->OwnerObj;
3088 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3089 $NewOwnerObj->Load( $NewOwner );
3090 unless ( $NewOwnerObj->Id ) {
3091 $RT::Handle->Rollback();
3092 return ( 0, $self->loc("That user does not exist") );
3096 # must have ModifyTicket rights
3097 # or TakeTicket/StealTicket and $NewOwner is self
3098 # see if it's a take
3099 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3100 unless ( $self->CurrentUserHasRight('ModifyTicket')
3101 || $self->CurrentUserHasRight('TakeTicket') ) {
3102 $RT::Handle->Rollback();
3103 return ( 0, $self->loc("Permission Denied") );
3107 # see if it's a steal
3108 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
3109 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3111 unless ( $self->CurrentUserHasRight('ModifyTicket')
3112 || $self->CurrentUserHasRight('StealTicket') ) {
3113 $RT::Handle->Rollback();
3114 return ( 0, $self->loc("Permission Denied") );
3118 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3119 $RT::Handle->Rollback();
3120 return ( 0, $self->loc("Permission Denied") );
3124 # If we're not stealing and the ticket has an owner and it's not
3126 if ( $Type ne 'Steal' and $Type ne 'Force'
3127 and $OldOwnerObj->Id != $RT::Nobody->Id
3128 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3130 $RT::Handle->Rollback();
3131 return ( 0, $self->loc("You can only take tickets that are unowned") )
3132 if $NewOwnerObj->id == $self->CurrentUser->id;
3135 $self->loc("You can only reassign tickets that you own or that are unowned" )
3139 #If we've specified a new owner and that user can't modify the ticket
3140 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3141 $RT::Handle->Rollback();
3142 return ( 0, $self->loc("That user may not own tickets in that queue") );
3145 # If the ticket has an owner and it's the new owner, we don't need
3147 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3148 $RT::Handle->Rollback();
3149 return ( 0, $self->loc("That user already owns that ticket") );
3152 # Delete the owner in the owner group, then add a new one
3153 # TODO: is this safe? it's not how we really want the API to work
3154 # for most things, but it's fast.
3155 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3157 $RT::Handle->Rollback();
3158 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3161 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3162 PrincipalId => $NewOwnerObj->PrincipalId,
3163 InsideTransaction => 1 );
3165 $RT::Handle->Rollback();
3166 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3169 # We call set twice with slightly different arguments, so
3170 # as to not have an SQL transaction span two RT transactions
3172 my ( $val, $msg ) = $self->_Set(
3174 RecordTransaction => 0,
3175 Value => $NewOwnerObj->Id,
3177 TransactionType => $Type,
3178 CheckACL => 0, # don't check acl
3182 $RT::Handle->Rollback;
3183 return ( 0, $self->loc("Could not change owner. ") . $msg );
3186 ($val, $msg) = $self->_NewTransaction(
3189 NewValue => $NewOwnerObj->Id,
3190 OldValue => $OldOwnerObj->Id,
3195 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3196 $OldOwnerObj->Name, $NewOwnerObj->Name );
3199 $RT::Handle->Rollback();
3203 $RT::Handle->Commit();
3205 return ( $val, $msg );
3214 A convenince method to set the ticket's owner to the current user
3220 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3229 Convenience method to set the owner to 'nobody' if the current user is the owner.
3235 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3244 A convenience method to change the owner of the current ticket to the
3245 current user. Even if it's owned by another user.
3252 if ( $self->IsOwner( $self->CurrentUser ) ) {
3253 return ( 0, $self->loc("You already own this ticket") );
3256 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3266 # {{{ Routines dealing with status
3268 # {{{ sub ValidateStatus
3270 =head2 ValidateStatus STATUS
3272 Takes a string. Returns true if that status is a valid status for this ticket.
3273 Returns false otherwise.
3277 sub ValidateStatus {
3281 #Make sure the status passed in is valid
3282 unless ( $self->QueueObj->IsValidStatus($status) ) {
3294 =head2 SetStatus STATUS
3296 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3298 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.
3302 my $tt = RT::Ticket->new($RT::SystemUser);
3303 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3306 is($tt->Status, 'new', "New ticket is created as new");
3308 ($id, $msg) = $tt->SetStatus('open');
3310 like($msg, qr/open/i, "Status message is correct");
3311 ($id, $msg) = $tt->SetStatus('resolved');
3313 like($msg, qr/resolved/i, "Status message is correct");
3314 ($id, $msg) = $tt->SetStatus('resolved');
3328 $args{Status} = shift;
3335 if ( $args{Status} eq 'deleted') {
3336 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3337 return ( 0, $self->loc('Permission Denied') );
3340 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3341 return ( 0, $self->loc('Permission Denied') );
3345 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3346 return (0, $self->loc('That ticket has unresolved dependencies'));
3349 my $now = RT::Date->new( $self->CurrentUser );
3352 #If we're changing the status from new, record that we've started
3353 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3355 #Set the Started time to "now"
3356 $self->_Set( Field => 'Started',
3358 RecordTransaction => 0 );
3361 #When we close a ticket, set the 'Resolved' attribute to now.
3362 # It's misnamed, but that's just historical.
3363 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3364 $self->_Set( Field => 'Resolved',
3366 RecordTransaction => 0 );
3369 #Actually update the status
3370 my ($val, $msg)= $self->_Set( Field => 'Status',
3371 Value => $args{Status},
3374 TransactionType => 'Status' );
3385 Takes no arguments. Marks this ticket for garbage collection
3391 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3392 return $self->Delete;
3397 return ( $self->SetStatus('deleted') );
3399 # TODO: garbage collection
3408 Sets this ticket's status to stalled
3414 return ( $self->SetStatus('stalled') );
3423 Sets this ticket's status to rejected
3429 return ( $self->SetStatus('rejected') );
3438 Sets this ticket\'s status to Open
3444 return ( $self->SetStatus('open') );
3453 Sets this ticket\'s status to Resolved
3459 return ( $self->SetStatus('resolved') );
3467 # {{{ Actions + Routines dealing with transactions
3469 # {{{ sub SetTold and _SetTold
3471 =head2 SetTold ISO [TIMETAKEN]
3473 Updates the told and records a transaction
3480 $told = shift if (@_);
3481 my $timetaken = shift || 0;
3483 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3484 return ( 0, $self->loc("Permission Denied") );
3487 my $datetold = new RT::Date( $self->CurrentUser );
3489 $datetold->Set( Format => 'iso',
3493 $datetold->SetToNow();
3496 return ( $self->_Set( Field => 'Told',
3497 Value => $datetold->ISO,
3498 TimeTaken => $timetaken,
3499 TransactionType => 'Told' ) );
3504 Updates the told without a transaction or acl check. Useful when we're sending replies.
3511 my $now = new RT::Date( $self->CurrentUser );
3514 #use __Set to get no ACLs ;)
3515 return ( $self->__Set( Field => 'Told',
3516 Value => $now->ISO ) );
3521 =head2 TransactionBatch
3523 Returns an array reference of all transactions created on this ticket during
3524 this ticket object's lifetime, or undef if there were none.
3526 Only works when the $RT::UseTransactionBatch config variable is set to true.
3530 sub TransactionBatch {
3532 return $self->{_TransactionBatch};
3538 # DESTROY methods need to localize $@, or it may unset it. This
3539 # causes $m->abort to not bubble all of the way up. See perlbug
3540 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3543 # The following line eliminates reentrancy.
3544 # It protects against the fact that perl doesn't deal gracefully
3545 # when an object's refcount is changed in its destructor.
3546 return if $self->{_Destroyed}++;
3548 my $batch = $self->TransactionBatch or return;
3549 return unless @$batch;
3552 RT::Scrips->new($RT::SystemUser)->Apply(
3553 Stage => 'TransactionBatch',
3555 TransactionObj => $batch->[0],
3556 Type => join(',', (map { $_->Type } @{$batch}) )
3562 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3564 # {{{ sub _OverlayAccessible
3566 sub _OverlayAccessible {
3568 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3569 Queue => { 'read' => 1, 'write' => 1 },
3570 Requestors => { 'read' => 1, 'write' => 1 },
3571 Owner => { 'read' => 1, 'write' => 1 },
3572 Subject => { 'read' => 1, 'write' => 1 },
3573 InitialPriority => { 'read' => 1, 'write' => 1 },
3574 FinalPriority => { 'read' => 1, 'write' => 1 },
3575 Priority => { 'read' => 1, 'write' => 1 },
3576 Status => { 'read' => 1, 'write' => 1 },
3577 TimeEstimated => { 'read' => 1, 'write' => 1 },
3578 TimeWorked => { 'read' => 1, 'write' => 1 },
3579 TimeLeft => { 'read' => 1, 'write' => 1 },
3580 Told => { 'read' => 1, 'write' => 1 },
3581 Resolved => { 'read' => 1 },
3582 Type => { 'read' => 1 },
3583 Starts => { 'read' => 1, 'write' => 1 },
3584 Started => { 'read' => 1, 'write' => 1 },
3585 Due => { 'read' => 1, 'write' => 1 },
3586 Creator => { 'read' => 1, 'auto' => 1 },
3587 Created => { 'read' => 1, 'auto' => 1 },
3588 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3589 LastUpdated => { 'read' => 1, 'auto' => 1 }
3601 my %args = ( Field => undef,
3604 RecordTransaction => 1,
3607 TransactionType => 'Set',
3610 if ($args{'CheckACL'}) {
3611 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3612 return ( 0, $self->loc("Permission Denied"));
3616 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3617 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3618 return(0, $self->loc("Internal Error"));
3621 #if the user is trying to modify the record
3623 #Take care of the old value we really don't want to get in an ACL loop.
3624 # so ask the super::_Value
3625 my $Old = $self->SUPER::_Value("$args{'Field'}");
3628 if ( $args{'UpdateTicket'} ) {
3631 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3632 Value => $args{'Value'} );
3634 #If we can't actually set the field to the value, don't record
3635 # a transaction. instead, get out of here.
3636 return ( 0, $msg ) unless $ret;
3639 if ( $args{'RecordTransaction'} == 1 ) {
3641 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3642 Type => $args{'TransactionType'},
3643 Field => $args{'Field'},
3644 NewValue => $args{'Value'},
3646 TimeTaken => $args{'TimeTaken'},
3648 return ( $Trans, scalar $TransObj->BriefDescription );
3651 return ( $ret, $msg );
3661 Takes the name of a table column.
3662 Returns its value as a string, if the user passes an ACL check
3671 #if the field is public, return it.
3672 if ( $self->_Accessible( $field, 'public' ) ) {
3674 #$RT::Logger->debug("Skipping ACL check for $field\n");
3675 return ( $self->SUPER::_Value($field) );
3679 #If the current user doesn't have ACLs, don't let em at it.
3681 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3684 return ( $self->SUPER::_Value($field) );
3690 # {{{ sub _UpdateTimeTaken
3692 =head2 _UpdateTimeTaken
3694 This routine will increment the timeworked counter. it should
3695 only be called from _NewTransaction
3699 sub _UpdateTimeTaken {
3701 my $Minutes = shift;
3704 $Total = $self->SUPER::_Value("TimeWorked");
3705 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3707 Field => "TimeWorked",
3718 # {{{ Routines dealing with ACCESS CONTROL
3720 # {{{ sub CurrentUserHasRight
3722 =head2 CurrentUserHasRight
3724 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3725 1 if the user has that right. It returns 0 if the user doesn't have that right.
3729 sub CurrentUserHasRight {
3735 Principal => $self->CurrentUser->UserObj(),
3748 Takes a paramhash with the attributes 'Right' and 'Principal'
3749 'Right' is a ticket-scoped textual right from RT::ACE
3750 'Principal' is an RT::User object
3752 Returns 1 if the principal has the right. Returns undef if not.
3764 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3767 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3772 $args{'Principal'}->HasRight(
3774 Right => $args{'Right'}
3785 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3786 It isn't acutally a searchbuilder collection itself.
3793 unless ($self->{'__reminders'}) {
3794 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3795 $self->{'__reminders'}->Ticket($self->id);
3797 return $self->{'__reminders'};
3803 # {{{ sub Transactions
3807 Returns an RT::Transactions object of all transactions on this ticket
3814 my $transactions = RT::Transactions->new( $self->CurrentUser );
3816 #If the user has no rights, return an empty object
3817 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3818 $transactions->LimitToTicket($self->id);
3820 # if the user may not see comments do not return them
3821 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3822 $transactions->Limit(
3827 $transactions->Limit(
3830 VALUE => "CommentEmailRecord",
3831 ENTRYAGGREGATOR => 'AND'
3837 return ($transactions);
3843 # {{{ TransactionCustomFields
3845 =head2 TransactionCustomFields
3847 Returns the custom fields that transactions on tickets will have.
3851 sub TransactionCustomFields {
3853 return $self->QueueObj->TicketTransactionCustomFields;
3858 # {{{ sub CustomFieldValues
3860 =head2 CustomFieldValues
3862 # Do name => id mapping (if needed) before falling back to
3863 # RT::Record's CustomFieldValues
3869 sub CustomFieldValues {
3872 if ( $field and $field !~ /^\d+$/ ) {
3873 my $cf = RT::CustomField->new( $self->CurrentUser );
3874 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3875 unless ( $cf->id ) {
3876 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3878 unless ( $cf->id ) {
3879 # If we didn't find a valid cfid, give up.
3880 return RT::CustomFieldValues->new($self->CurrentUser);
3883 return $self->SUPER::CustomFieldValues($field);
3888 # {{{ sub CustomFieldLookupType
3890 =head2 CustomFieldLookupType
3892 Returns the RT::Ticket lookup type, which can be passed to
3893 RT::CustomField->Create() via the 'LookupType' hash key.
3899 sub CustomFieldLookupType {
3900 "RT::Queue-RT::Ticket";
3907 Jesse Vincent, jesse@bestpractical.com