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);
1819 Returns an RT::Group object which contains this ticket's Ccs.
1820 If the user doesn't have "ShowTicket" permission, returns an empty group
1827 my $group = RT::Group->new($self->CurrentUser);
1828 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1829 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1842 Returns an RT::Group object which contains this ticket's AdminCcs.
1843 If the user doesn't have "ShowTicket" permission, returns an empty group
1850 my $group = RT::Group->new($self->CurrentUser);
1851 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1852 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1862 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1865 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1867 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1869 Takes a param hash with the attributes Type and either PrincipalId or Email
1871 Type is one of Requestor, Cc, AdminCc and Owner
1873 PrincipalId is an RT::Principal id, and Email is an email address.
1875 Returns true if the specified principal (or the one corresponding to the
1876 specified address) is a member of the group Type for this ticket.
1878 XX TODO: This should be Memoized.
1885 my %args = ( Type => 'Requestor',
1886 PrincipalId => undef,
1891 # Load the relevant group.
1892 my $group = RT::Group->new($self->CurrentUser);
1893 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1895 # Find the relevant principal.
1896 my $principal = RT::Principal->new($self->CurrentUser);
1897 if (!$args{PrincipalId} && $args{Email}) {
1898 # Look up the specified user.
1899 my $user = RT::User->new($self->CurrentUser);
1900 $user->LoadByEmail($args{Email});
1902 $args{PrincipalId} = $user->PrincipalId;
1905 # A non-existent user can't be a group member.
1909 $principal->Load($args{'PrincipalId'});
1911 # Ask if it has the member in question
1912 return ($group->HasMember($principal));
1917 # {{{ sub IsRequestor
1919 =head2 IsRequestor PRINCIPAL_ID
1921 Takes an RT::Principal id
1922 Returns true if the principal is a requestor of the current ticket.
1931 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1939 =head2 IsCc PRINCIPAL_ID
1941 Takes an RT::Principal id.
1942 Returns true if the principal is a requestor of the current ticket.
1951 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1959 =head2 IsAdminCc PRINCIPAL_ID
1961 Takes an RT::Principal id.
1962 Returns true if the principal is a requestor of the current ticket.
1970 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1980 Takes an RT::User object. Returns true if that user is this ticket's owner.
1981 returns undef otherwise
1989 # no ACL check since this is used in acl decisions
1990 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1994 #Tickets won't yet have owners when they're being created.
1995 unless ( $self->OwnerObj->id ) {
1999 if ( $person->id == $self->OwnerObj->id ) {
2013 # {{{ Routines dealing with queues
2015 # {{{ sub ValidateQueue
2022 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
2026 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2027 my $id = $QueueObj->Load($Value);
2043 my $NewQueue = shift;
2045 #Redundant. ACL gets checked in _Set;
2046 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2047 return ( 0, $self->loc("Permission Denied") );
2050 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2051 $NewQueueObj->Load($NewQueue);
2053 unless ( $NewQueueObj->Id() ) {
2054 return ( 0, $self->loc("That queue does not exist") );
2057 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2058 return ( 0, $self->loc('That is the same value') );
2061 $self->CurrentUser->HasRight(
2062 Right => 'CreateTicket',
2063 Object => $NewQueueObj
2067 return ( 0, $self->loc("You may not create requests in that queue.") );
2071 $self->OwnerObj->HasRight(
2072 Right => 'OwnTicket',
2073 Object => $NewQueueObj
2077 my $clone = RT::Ticket->new( $RT::SystemUser );
2078 $clone->Load( $self->Id );
2079 unless ( $clone->Id ) {
2080 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2082 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2083 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2086 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2095 Takes nothing. returns this ticket's queue object
2102 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2104 #We call __Value so that we can avoid the ACL decision and some deep recursion
2105 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2106 return ($queue_obj);
2113 # {{{ Date printing routines
2119 Returns an RT::Date object containing this ticket's due date
2126 my $time = new RT::Date( $self->CurrentUser );
2128 # -1 is RT::Date slang for never
2130 $time->Set( Format => 'sql', Value => $self->Due );
2133 $time->Set( Format => 'unix', Value => -1 );
2141 # {{{ sub DueAsString
2145 Returns this ticket's due date as a human readable string
2151 return $self->DueObj->AsString();
2156 # {{{ sub ResolvedObj
2160 Returns an RT::Date object of this ticket's 'resolved' time.
2167 my $time = new RT::Date( $self->CurrentUser );
2168 $time->Set( Format => 'sql', Value => $self->Resolved );
2174 # {{{ sub SetStarted
2178 Takes a date in ISO format or undef
2179 Returns a transaction id and a message
2180 The client calls "Start" to note that the project was started on the date in $date.
2181 A null date means "now"
2187 my $time = shift || 0;
2189 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2190 return ( 0, $self->loc("Permission Denied") );
2193 #We create a date object to catch date weirdness
2194 my $time_obj = new RT::Date( $self->CurrentUser() );
2196 $time_obj->Set( Format => 'ISO', Value => $time );
2199 $time_obj->SetToNow();
2202 #Now that we're starting, open this ticket
2203 #TODO do we really want to force this as policy? it should be a scrip
2205 #We need $TicketAsSystem, in case the current user doesn't have
2208 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2209 $TicketAsSystem->Load( $self->Id );
2210 if ( $TicketAsSystem->Status eq 'new' ) {
2211 $TicketAsSystem->Open();
2214 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2220 # {{{ sub StartedObj
2224 Returns an RT::Date object which contains this ticket's
2232 my $time = new RT::Date( $self->CurrentUser );
2233 $time->Set( Format => 'sql', Value => $self->Started );
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->Starts );
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->Told );
2277 # {{{ sub ToldAsString
2281 A convenience method that returns ToldObj->AsString
2283 TODO: This should be deprecated
2289 if ( $self->Told ) {
2290 return $self->ToldObj->AsString();
2299 # {{{ sub TimeWorkedAsString
2301 =head2 TimeWorkedAsString
2303 Returns the amount of time worked on this ticket as a Text String
2307 sub TimeWorkedAsString {
2309 return "0" unless $self->TimeWorked;
2311 #This is not really a date object, but if we diff a number of seconds
2312 #vs the epoch, we'll get a nice description of time worked.
2314 my $worked = new RT::Date( $self->CurrentUser );
2316 #return the #of minutes worked turned into seconds and written as
2317 # a simple text string
2319 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2326 # {{{ Routines dealing with correspondence/comments
2332 Comment on this ticket.
2333 Takes a hashref with the following attributes:
2334 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2337 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2339 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2340 They will, however, be prepared and you'll be able to access them through the TransactionObj
2342 Returns: Transaction id, Error Message, Transaction Object
2343 (note the different order from Create()!)
2350 my %args = ( CcMessageTo => undef,
2351 BccMessageTo => undef,
2358 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2359 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2360 return ( 0, $self->loc("Permission Denied"), undef );
2362 $args{'NoteType'} = 'Comment';
2364 if ($args{'DryRun'}) {
2365 $RT::Handle->BeginTransaction();
2366 $args{'CommitScrips'} = 0;
2369 my @results = $self->_RecordNote(%args);
2370 if ($args{'DryRun'}) {
2371 $RT::Handle->Rollback();
2378 # {{{ sub Correspond
2382 Correspond on this ticket.
2383 Takes a hashref with the following attributes:
2386 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2388 if there's no MIMEObj, Content is used to build a MIME::Entity object
2390 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2391 They will, however, be prepared and you'll be able to access them through the TransactionObj
2393 Returns: Transaction id, Error Message, Transaction Object
2394 (note the different order from Create()!)
2401 my %args = ( CcMessageTo => undef,
2402 BccMessageTo => undef,
2408 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2409 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2410 return ( 0, $self->loc("Permission Denied"), undef );
2413 $args{'NoteType'} = 'Correspond';
2414 if ($args{'DryRun'}) {
2415 $RT::Handle->BeginTransaction();
2416 $args{'CommitScrips'} = 0;
2419 my @results = $self->_RecordNote(%args);
2421 #Set the last told date to now if this isn't mail from the requestor.
2422 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2423 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2425 if ($args{'DryRun'}) {
2426 $RT::Handle->Rollback();
2435 # {{{ sub _RecordNote
2439 the meat of both comment and correspond.
2441 Performs no access control checks. hence, dangerous.
2448 my %args = ( CcMessageTo => undef,
2449 BccMessageTo => undef,
2456 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2457 return ( 0, $self->loc("No message attached"), undef );
2459 unless ( $args{'MIMEObj'} ) {
2460 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2461 ref $args{'Content'}
2463 : [ $args{'Content'} ]
2467 # convert text parts into utf-8
2468 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2470 # If we've been passed in CcMessageTo and BccMessageTo fields,
2471 # add them to the mime object for passing on to the transaction handler
2472 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2476 foreach my $type (qw/Cc Bcc/) {
2477 if ( defined $args{ $type . 'MessageTo' } ) {
2479 my $addresses = join ', ', (
2480 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2481 Mail::Address->parse( $args{ $type . 'MessageTo' } ) );
2482 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2486 # If this is from an external source, we need to come up with its
2487 # internal Message-ID now, so all emails sent because of this
2488 # message have a common Message-ID
2489 unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2490 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2492 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2494 . $RT::VERSION . "-"
2496 . CORE::time() . "-"
2497 . int(rand(2000)) . '.'
2500 . "0" . "@" # Email sent
2505 #Record the correspondence (write the transaction)
2506 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2507 Type => $args{'NoteType'},
2508 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2509 TimeTaken => $args{'TimeTaken'},
2510 MIMEObj => $args{'MIMEObj'},
2511 CommitScrips => $args{'CommitScrips'},
2515 $RT::Logger->err("$self couldn't init a transaction $msg");
2516 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2519 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2531 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2534 my $type = shift || "";
2536 unless ( $self->{"$field$type"} ) {
2537 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2538 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2539 # Maybe this ticket is a merged ticket
2540 my $Tickets = new RT::Tickets( $self->CurrentUser );
2541 # at least to myself
2542 $self->{"$field$type"}->Limit( FIELD => $field,
2543 VALUE => $self->URI,
2544 ENTRYAGGREGATOR => 'OR' );
2545 $Tickets->Limit( FIELD => 'EffectiveId',
2546 VALUE => $self->EffectiveId );
2547 while (my $Ticket = $Tickets->Next) {
2548 $self->{"$field$type"}->Limit( FIELD => $field,
2549 VALUE => $Ticket->URI,
2550 ENTRYAGGREGATOR => 'OR' );
2552 $self->{"$field$type"}->Limit( FIELD => 'Type',
2557 return ( $self->{"$field$type"} );
2562 # {{{ sub DeleteLink
2566 Delete a link. takes a paramhash of Base, Target and Type.
2567 Either Base or Target must be null. The null value will
2568 be replaced with this ticket\'s id
2581 unless ( $args{'Target'} || $args{'Base'} ) {
2582 $RT::Logger->error("Base or Target must be specified\n");
2583 return ( 0, $self->loc('Either base or target must be specified') );
2588 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2589 if ( !$right && $RT::StrictLinkACL ) {
2590 return ( 0, $self->loc("Permission Denied") );
2593 # If the other URI is an RT::Ticket, we want to make sure the user
2594 # can modify it too...
2595 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2596 return (0, $msg) unless $status;
2597 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2600 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2601 ( $RT::StrictLinkACL && $right < 2 ) )
2603 return ( 0, $self->loc("Permission Denied") );
2606 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2609 $RT::Logger->debug("Couldn't find that link\n");
2613 my ($direction, $remote_link);
2615 if ( $args{'Base'} ) {
2616 $remote_link = $args{'Base'};
2617 $direction = 'Target';
2619 elsif ( $args{'Target'} ) {
2620 $remote_link = $args{'Target'};
2624 if ( $args{'Silent'} ) {
2625 return ( $val, $Msg );
2628 my $remote_uri = RT::URI->new( $self->CurrentUser );
2629 $remote_uri->FromURI( $remote_link );
2631 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2632 Type => 'DeleteLink',
2633 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2634 OldValue => $remote_uri->URI || $remote_link,
2638 if ( $remote_uri->IsLocal ) {
2640 my $OtherObj = $remote_uri->Object;
2641 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2642 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2643 : $LINKDIRMAP{$args{'Type'}}->{Target},
2644 OldValue => $self->URI,
2645 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2649 return ( $Trans, $Msg );
2659 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2665 my %args = ( Target => '',
2671 unless ( $args{'Target'} || $args{'Base'} ) {
2672 $RT::Logger->error("Base or Target must be specified\n");
2673 return ( 0, $self->loc('Either base or target must be specified') );
2677 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2678 if ( !$right && $RT::StrictLinkACL ) {
2679 return ( 0, $self->loc("Permission Denied") );
2682 # If the other URI is an RT::Ticket, we want to make sure the user
2683 # can modify it too...
2684 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2685 return (0, $msg) unless $status;
2686 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2689 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2690 ( $RT::StrictLinkACL && $right < 2 ) )
2692 return ( 0, $self->loc("Permission Denied") );
2695 return $self->_AddLink(%args);
2698 sub __GetTicketFromURI {
2700 my %args = ( URI => '', @_ );
2702 # If the other URI is an RT::Ticket, we want to make sure the user
2703 # can modify it too...
2704 my $uri_obj = RT::URI->new( $self->CurrentUser );
2705 $uri_obj->FromURI( $args{'URI'} );
2707 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2708 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2709 $RT::Logger->warning( "$msg\n" );
2712 my $obj = $uri_obj->Resolver->Object;
2713 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2714 return (1, 'Found not a ticket', undef);
2716 return (1, 'Found ticket', $obj);
2721 Private non-acled variant of AddLink so that links can be added during create.
2727 my %args = ( Target => '',
2733 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2734 return ($val, $msg) if !$val || $exist;
2736 my ($direction, $remote_link);
2737 if ( $args{'Target'} ) {
2738 $remote_link = $args{'Target'};
2739 $direction = 'Base';
2740 } elsif ( $args{'Base'} ) {
2741 $remote_link = $args{'Base'};
2742 $direction = 'Target';
2745 # Don't write the transaction if we're doing this on create
2746 if ( $args{'Silent'} ) {
2747 return ( $val, $msg );
2750 my $remote_uri = RT::URI->new( $self->CurrentUser );
2751 $remote_uri->FromURI( $remote_link );
2753 #Write the transaction
2754 my ( $Trans, $Msg, $TransObj ) =
2755 $self->_NewTransaction(Type => 'AddLink',
2756 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2757 NewValue => $remote_uri->URI || $remote_link,
2760 if ( $remote_uri->IsLocal ) {
2762 my $OtherObj = $remote_uri->Object;
2763 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2764 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2765 : $LINKDIRMAP{$args{'Type'}}->{Target},
2766 NewValue => $self->URI,
2767 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2770 return ( $val, $Msg );
2782 MergeInto take the id of the ticket to merge this ticket into.
2787 my $t1 = RT::Ticket->new($RT::SystemUser);
2788 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2790 my $t2 = RT::Ticket->new($RT::SystemUser);
2791 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2793 my ($msg, $val) = $t1->MergeInto($t2->id);
2795 $t1 = RT::Ticket->new($RT::SystemUser);
2796 is ($t1->id, undef, "ok. we've got a blank ticket1");
2799 is ($t1->id, $t2->id);
2801 is ($t1->Requestors->MembersObj->Count, 2);
2810 my $ticket_id = shift;
2812 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2813 return ( 0, $self->loc("Permission Denied") );
2816 # Load up the new ticket.
2817 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2818 $MergeInto->Load($ticket_id);
2820 # make sure it exists.
2821 unless ( $MergeInto->Id ) {
2822 return ( 0, $self->loc("New ticket doesn't exist") );
2825 # Make sure the current user can modify the new ticket.
2826 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2827 return ( 0, $self->loc("Permission Denied") );
2830 $RT::Handle->BeginTransaction();
2832 # We use EffectiveId here even though it duplicates information from
2833 # the links table becasue of the massive performance hit we'd take
2834 # by trying to do a separate database query for merge info everytime
2837 #update this ticket's effective id to the new ticket's id.
2838 my ( $id_val, $id_msg ) = $self->__Set(
2839 Field => 'EffectiveId',
2840 Value => $MergeInto->Id()
2844 $RT::Handle->Rollback();
2845 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2849 if ( $self->__Value('Status') ne 'resolved' ) {
2851 my ( $status_val, $status_msg )
2852 = $self->__Set( Field => 'Status', Value => 'resolved' );
2854 unless ($status_val) {
2855 $RT::Handle->Rollback();
2858 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2862 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2866 # update all the links that point to that old ticket
2867 my $old_links_to = RT::Links->new($self->CurrentUser);
2868 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2871 while (my $link = $old_links_to->Next) {
2872 if (exists $old_seen{$link->Base."-".$link->Type}) {
2875 elsif ($link->Base eq $MergeInto->URI) {
2878 # First, make sure the link doesn't already exist. then move it over.
2879 my $tmp = RT::Link->new($RT::SystemUser);
2880 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2884 $link->SetTarget($MergeInto->URI);
2885 $link->SetLocalTarget($MergeInto->id);
2887 $old_seen{$link->Base."-".$link->Type} =1;
2892 my $old_links_from = RT::Links->new($self->CurrentUser);
2893 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2895 while (my $link = $old_links_from->Next) {
2896 if (exists $old_seen{$link->Type."-".$link->Target}) {
2899 if ($link->Target eq $MergeInto->URI) {
2902 # First, make sure the link doesn't already exist. then move it over.
2903 my $tmp = RT::Link->new($RT::SystemUser);
2904 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2908 $link->SetBase($MergeInto->URI);
2909 $link->SetLocalBase($MergeInto->id);
2910 $old_seen{$link->Type."-".$link->Target} =1;
2916 # Update time fields
2917 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2919 my $mutator = "Set$type";
2920 $MergeInto->$mutator(
2921 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2924 #add all of this ticket's watchers to that ticket.
2925 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2927 my $people = $self->$watcher_type->MembersObj;
2928 my $addwatcher_type = $watcher_type;
2929 $addwatcher_type =~ s/s$//;
2931 while ( my $watcher = $people->Next ) {
2933 my ($val, $msg) = $MergeInto->_AddWatcher(
2934 Type => $addwatcher_type,
2936 PrincipalId => $watcher->MemberId
2939 $RT::Logger->warning($msg);
2945 #find all of the tickets that were merged into this ticket.
2946 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2947 $old_mergees->Limit(
2948 FIELD => 'EffectiveId',
2953 # update their EffectiveId fields to the new ticket's id
2954 while ( my $ticket = $old_mergees->Next() ) {
2955 my ( $val, $msg ) = $ticket->__Set(
2956 Field => 'EffectiveId',
2957 Value => $MergeInto->Id()
2961 #make a new link: this ticket is merged into that other ticket.
2962 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2964 $MergeInto->_SetLastUpdated;
2966 $RT::Handle->Commit();
2967 return ( 1, $self->loc("Merge Successful") );
2974 # {{{ Routines dealing with ownership
2980 Takes nothing and returns an RT::User object of
2988 #If this gets ACLed, we lose on a rights check in User.pm and
2989 #get deep recursion. if we need ACLs here, we need
2990 #an equiv without ACLs
2992 my $owner = new RT::User( $self->CurrentUser );
2993 $owner->Load( $self->__Value('Owner') );
2995 #Return the owner object
3001 # {{{ sub OwnerAsString
3003 =head2 OwnerAsString
3005 Returns the owner's email address
3011 return ( $self->OwnerObj->EmailAddress );
3021 Takes two arguments:
3022 the Id or Name of the owner
3023 and (optionally) the type of the SetOwner Transaction. It defaults
3024 to 'Give'. 'Steal' is also a valid option.
3028 my $root = RT::User->new($RT::SystemUser);
3029 $root->Load('root');
3030 ok ($root->Id, "Loaded the root user");
3031 my $t = RT::Ticket->new($RT::SystemUser);
3033 $t->SetOwner('root');
3034 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
3036 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
3037 my $txns = RT::Transactions->new($RT::SystemUser);
3038 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3039 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
3040 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
3041 $txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
3043 my $steal = $txns->First;
3044 ok($steal->OldValue == $root->Id , "Stolen from root");
3045 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3053 my $NewOwner = shift;
3054 my $Type = shift || "Give";
3056 $RT::Handle->BeginTransaction();
3058 $self->_SetLastUpdated(); # lock the ticket
3059 $self->Load( $self->id ); # in case $self changed while waiting for lock
3061 my $OldOwnerObj = $self->OwnerObj;
3063 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3064 $NewOwnerObj->Load( $NewOwner );
3065 unless ( $NewOwnerObj->Id ) {
3066 $RT::Handle->Rollback();
3067 return ( 0, $self->loc("That user does not exist") );
3071 # must have ModifyTicket rights
3072 # or TakeTicket/StealTicket and $NewOwner is self
3073 # see if it's a take
3074 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3075 unless ( $self->CurrentUserHasRight('ModifyTicket')
3076 || $self->CurrentUserHasRight('TakeTicket') ) {
3077 $RT::Handle->Rollback();
3078 return ( 0, $self->loc("Permission Denied") );
3082 # see if it's a steal
3083 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
3084 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3086 unless ( $self->CurrentUserHasRight('ModifyTicket')
3087 || $self->CurrentUserHasRight('StealTicket') ) {
3088 $RT::Handle->Rollback();
3089 return ( 0, $self->loc("Permission Denied") );
3093 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3094 $RT::Handle->Rollback();
3095 return ( 0, $self->loc("Permission Denied") );
3099 # If we're not stealing and the ticket has an owner and it's not
3101 if ( $Type ne 'Steal' and $Type ne 'Force'
3102 and $OldOwnerObj->Id != $RT::Nobody->Id
3103 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3105 $RT::Handle->Rollback();
3106 return ( 0, $self->loc("You can only take tickets that are unowned") )
3107 if $NewOwnerObj->id == $self->CurrentUser->id;
3110 $self->loc("You can only reassign tickets that you own or that are unowned" )
3114 #If we've specified a new owner and that user can't modify the ticket
3115 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3116 $RT::Handle->Rollback();
3117 return ( 0, $self->loc("That user may not own tickets in that queue") );
3120 # If the ticket has an owner and it's the new owner, we don't need
3122 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3123 $RT::Handle->Rollback();
3124 return ( 0, $self->loc("That user already owns that ticket") );
3127 # Delete the owner in the owner group, then add a new one
3128 # TODO: is this safe? it's not how we really want the API to work
3129 # for most things, but it's fast.
3130 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3132 $RT::Handle->Rollback();
3133 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3136 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3137 PrincipalId => $NewOwnerObj->PrincipalId,
3138 InsideTransaction => 1 );
3140 $RT::Handle->Rollback();
3141 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3144 # We call set twice with slightly different arguments, so
3145 # as to not have an SQL transaction span two RT transactions
3147 my ( $val, $msg ) = $self->_Set(
3149 RecordTransaction => 0,
3150 Value => $NewOwnerObj->Id,
3152 TransactionType => $Type,
3153 CheckACL => 0, # don't check acl
3157 $RT::Handle->Rollback;
3158 return ( 0, $self->loc("Could not change owner. ") . $msg );
3161 ($val, $msg) = $self->_NewTransaction(
3164 NewValue => $NewOwnerObj->Id,
3165 OldValue => $OldOwnerObj->Id,
3170 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3171 $OldOwnerObj->Name, $NewOwnerObj->Name );
3174 $RT::Handle->Rollback();
3178 $RT::Handle->Commit();
3180 return ( $val, $msg );
3189 A convenince method to set the ticket's owner to the current user
3195 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3204 Convenience method to set the owner to 'nobody' if the current user is the owner.
3210 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3219 A convenience method to change the owner of the current ticket to the
3220 current user. Even if it's owned by another user.
3227 if ( $self->IsOwner( $self->CurrentUser ) ) {
3228 return ( 0, $self->loc("You already own this ticket") );
3231 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3241 # {{{ Routines dealing with status
3243 # {{{ sub ValidateStatus
3245 =head2 ValidateStatus STATUS
3247 Takes a string. Returns true if that status is a valid status for this ticket.
3248 Returns false otherwise.
3252 sub ValidateStatus {
3256 #Make sure the status passed in is valid
3257 unless ( $self->QueueObj->IsValidStatus($status) ) {
3269 =head2 SetStatus STATUS
3271 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3273 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.
3277 my $tt = RT::Ticket->new($RT::SystemUser);
3278 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3281 is($tt->Status, 'new', "New ticket is created as new");
3283 ($id, $msg) = $tt->SetStatus('open');
3285 like($msg, qr/open/i, "Status message is correct");
3286 ($id, $msg) = $tt->SetStatus('resolved');
3288 like($msg, qr/resolved/i, "Status message is correct");
3289 ($id, $msg) = $tt->SetStatus('resolved');
3303 $args{Status} = shift;
3310 if ( $args{Status} eq 'deleted') {
3311 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3312 return ( 0, $self->loc('Permission Denied') );
3315 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3316 return ( 0, $self->loc('Permission Denied') );
3320 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3321 return (0, $self->loc('That ticket has unresolved dependencies'));
3324 my $now = RT::Date->new( $self->CurrentUser );
3327 #If we're changing the status from new, record that we've started
3328 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3330 #Set the Started time to "now"
3331 $self->_Set( Field => 'Started',
3333 RecordTransaction => 0 );
3336 #When we close a ticket, set the 'Resolved' attribute to now.
3337 # It's misnamed, but that's just historical.
3338 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3339 $self->_Set( Field => 'Resolved',
3341 RecordTransaction => 0 );
3344 #Actually update the status
3345 my ($val, $msg)= $self->_Set( Field => 'Status',
3346 Value => $args{Status},
3349 TransactionType => 'Status' );
3360 Takes no arguments. Marks this ticket for garbage collection
3366 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3367 return $self->Delete;
3372 return ( $self->SetStatus('deleted') );
3374 # TODO: garbage collection
3383 Sets this ticket's status to stalled
3389 return ( $self->SetStatus('stalled') );
3398 Sets this ticket's status to rejected
3404 return ( $self->SetStatus('rejected') );
3413 Sets this ticket\'s status to Open
3419 return ( $self->SetStatus('open') );
3428 Sets this ticket\'s status to Resolved
3434 return ( $self->SetStatus('resolved') );
3442 # {{{ Actions + Routines dealing with transactions
3444 # {{{ sub SetTold and _SetTold
3446 =head2 SetTold ISO [TIMETAKEN]
3448 Updates the told and records a transaction
3455 $told = shift if (@_);
3456 my $timetaken = shift || 0;
3458 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3459 return ( 0, $self->loc("Permission Denied") );
3462 my $datetold = new RT::Date( $self->CurrentUser );
3464 $datetold->Set( Format => 'iso',
3468 $datetold->SetToNow();
3471 return ( $self->_Set( Field => 'Told',
3472 Value => $datetold->ISO,
3473 TimeTaken => $timetaken,
3474 TransactionType => 'Told' ) );
3479 Updates the told without a transaction or acl check. Useful when we're sending replies.
3486 my $now = new RT::Date( $self->CurrentUser );
3489 #use __Set to get no ACLs ;)
3490 return ( $self->__Set( Field => 'Told',
3491 Value => $now->ISO ) );
3496 =head2 TransactionBatch
3498 Returns an array reference of all transactions created on this ticket during
3499 this ticket object's lifetime, or undef if there were none.
3501 Only works when the $RT::UseTransactionBatch config variable is set to true.
3505 sub TransactionBatch {
3507 return $self->{_TransactionBatch};
3513 # DESTROY methods need to localize $@, or it may unset it. This
3514 # causes $m->abort to not bubble all of the way up. See perlbug
3515 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3518 # The following line eliminates reentrancy.
3519 # It protects against the fact that perl doesn't deal gracefully
3520 # when an object's refcount is changed in its destructor.
3521 return if $self->{_Destroyed}++;
3523 my $batch = $self->TransactionBatch or return;
3524 return unless @$batch;
3527 RT::Scrips->new($RT::SystemUser)->Apply(
3528 Stage => 'TransactionBatch',
3530 TransactionObj => $batch->[0],
3531 Type => join(',', (map { $_->Type } @{$batch}) )
3537 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3539 # {{{ sub _OverlayAccessible
3541 sub _OverlayAccessible {
3543 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3544 Queue => { 'read' => 1, 'write' => 1 },
3545 Requestors => { 'read' => 1, 'write' => 1 },
3546 Owner => { 'read' => 1, 'write' => 1 },
3547 Subject => { 'read' => 1, 'write' => 1 },
3548 InitialPriority => { 'read' => 1, 'write' => 1 },
3549 FinalPriority => { 'read' => 1, 'write' => 1 },
3550 Priority => { 'read' => 1, 'write' => 1 },
3551 Status => { 'read' => 1, 'write' => 1 },
3552 TimeEstimated => { 'read' => 1, 'write' => 1 },
3553 TimeWorked => { 'read' => 1, 'write' => 1 },
3554 TimeLeft => { 'read' => 1, 'write' => 1 },
3555 Told => { 'read' => 1, 'write' => 1 },
3556 Resolved => { 'read' => 1 },
3557 Type => { 'read' => 1 },
3558 Starts => { 'read' => 1, 'write' => 1 },
3559 Started => { 'read' => 1, 'write' => 1 },
3560 Due => { 'read' => 1, 'write' => 1 },
3561 Creator => { 'read' => 1, 'auto' => 1 },
3562 Created => { 'read' => 1, 'auto' => 1 },
3563 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3564 LastUpdated => { 'read' => 1, 'auto' => 1 }
3576 my %args = ( Field => undef,
3579 RecordTransaction => 1,
3582 TransactionType => 'Set',
3585 if ($args{'CheckACL'}) {
3586 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3587 return ( 0, $self->loc("Permission Denied"));
3591 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3592 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3593 return(0, $self->loc("Internal Error"));
3596 #if the user is trying to modify the record
3598 #Take care of the old value we really don't want to get in an ACL loop.
3599 # so ask the super::_Value
3600 my $Old = $self->SUPER::_Value("$args{'Field'}");
3603 if ( $args{'UpdateTicket'} ) {
3606 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3607 Value => $args{'Value'} );
3609 #If we can't actually set the field to the value, don't record
3610 # a transaction. instead, get out of here.
3611 return ( 0, $msg ) unless $ret;
3614 if ( $args{'RecordTransaction'} == 1 ) {
3616 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3617 Type => $args{'TransactionType'},
3618 Field => $args{'Field'},
3619 NewValue => $args{'Value'},
3621 TimeTaken => $args{'TimeTaken'},
3623 return ( $Trans, scalar $TransObj->BriefDescription );
3626 return ( $ret, $msg );
3636 Takes the name of a table column.
3637 Returns its value as a string, if the user passes an ACL check
3646 #if the field is public, return it.
3647 if ( $self->_Accessible( $field, 'public' ) ) {
3649 #$RT::Logger->debug("Skipping ACL check for $field\n");
3650 return ( $self->SUPER::_Value($field) );
3654 #If the current user doesn't have ACLs, don't let em at it.
3656 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3659 return ( $self->SUPER::_Value($field) );
3665 # {{{ sub _UpdateTimeTaken
3667 =head2 _UpdateTimeTaken
3669 This routine will increment the timeworked counter. it should
3670 only be called from _NewTransaction
3674 sub _UpdateTimeTaken {
3676 my $Minutes = shift;
3679 $Total = $self->SUPER::_Value("TimeWorked");
3680 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3682 Field => "TimeWorked",
3693 # {{{ Routines dealing with ACCESS CONTROL
3695 # {{{ sub CurrentUserHasRight
3697 =head2 CurrentUserHasRight
3699 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3700 1 if the user has that right. It returns 0 if the user doesn't have that right.
3704 sub CurrentUserHasRight {
3710 Principal => $self->CurrentUser->UserObj(),
3723 Takes a paramhash with the attributes 'Right' and 'Principal'
3724 'Right' is a ticket-scoped textual right from RT::ACE
3725 'Principal' is an RT::User object
3727 Returns 1 if the principal has the right. Returns undef if not.
3739 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3742 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3747 $args{'Principal'}->HasRight(
3749 Right => $args{'Right'}
3760 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3761 It isn't acutally a searchbuilder collection itself.
3768 unless ($self->{'__reminders'}) {
3769 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3770 $self->{'__reminders'}->Ticket($self->id);
3772 return $self->{'__reminders'};
3778 # {{{ sub Transactions
3782 Returns an RT::Transactions object of all transactions on this ticket
3789 my $transactions = RT::Transactions->new( $self->CurrentUser );
3791 #If the user has no rights, return an empty object
3792 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3793 $transactions->LimitToTicket($self->id);
3795 # if the user may not see comments do not return them
3796 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3797 $transactions->Limit(
3802 $transactions->Limit(
3805 VALUE => "CommentEmailRecord",
3806 ENTRYAGGREGATOR => 'AND'
3812 return ($transactions);
3818 # {{{ TransactionCustomFields
3820 =head2 TransactionCustomFields
3822 Returns the custom fields that transactions on tickets will have.
3826 sub TransactionCustomFields {
3828 return $self->QueueObj->TicketTransactionCustomFields;
3833 # {{{ sub CustomFieldValues
3835 =head2 CustomFieldValues
3837 # Do name => id mapping (if needed) before falling back to
3838 # RT::Record's CustomFieldValues
3844 sub CustomFieldValues {
3847 if ( $field and $field !~ /^\d+$/ ) {
3848 my $cf = RT::CustomField->new( $self->CurrentUser );
3849 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3850 unless ( $cf->id ) {
3851 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3853 unless ( $cf->id ) {
3854 # If we didn't find a valid cfid, give up.
3855 return RT::CustomFieldValues->new($self->CurrentUser);
3858 return $self->SUPER::CustomFieldValues($field);
3863 # {{{ sub CustomFieldLookupType
3865 =head2 CustomFieldLookupType
3867 Returns the RT::Ticket lookup type, which can be passed to
3868 RT::CustomField->Create() via the 'LookupType' hash key.
3874 sub CustomFieldLookupType {
3875 "RT::Queue-RT::Ticket";
3882 Jesse Vincent, jesse@bestpractical.com