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 #find any requestors with customer targets
726 my %cust_target = ();
729 grep { $_->Customers->Count }
730 @{ $self->Requestors->UserMembersObj->ItemsArrayRef };
732 foreach my $Requestor ( @Requestors ) {
733 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
734 $cust_target{ $cust_link->Target } = 1;
738 #and then auto-associate this ticket with those customers
740 foreach my $cust_target ( keys %cust_target ) {
742 my @link = ( 'Type' => 'MemberOf',
743 #'Target' => "freeside://freeside/cust_main/$custnum",
744 'Target' => $cust_target,
747 my( $val, $msg ) = $self->AddLink(@link);
748 push @non_fatal_errors, $msg;
756 # {{{ Add all the custom fields
758 foreach my $arg ( keys %args ) {
759 next unless ( $arg =~ /^CustomField-(\d+)$/i );
762 my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
764 next unless ( length($value) );
766 # Allow passing in uploaded LargeContent etc by hash reference
767 $self->_AddCustomFieldValue(
768 (UNIVERSAL::isa( $value => 'HASH' )
773 RecordTransaction => 0,
780 if ( $args{'_RecordTransaction'} ) {
782 # {{{ Add a transaction for the create
783 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
785 TimeTaken => $args{'TimeWorked'},
786 MIMEObj => $args{'MIMEObj'}
789 if ( $self->Id && $Trans ) {
791 $TransObj->UpdateCustomFields(ARGSRef => \%args);
793 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
794 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
795 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
798 $RT::Handle->Rollback();
800 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
801 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
802 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
805 $RT::Handle->Commit();
806 return ( $self->Id, $TransObj->Id, $ErrStr );
812 # Not going to record a transaction
813 $RT::Handle->Commit();
814 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
815 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
816 return ( $self->Id, 0, $ErrStr );
827 =head2 UpdateFrom822 $MESSAGE
829 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
830 Returns an um. ask me again when the code exists
835 my $simple_update = <<EOF;
837 AddRequestor: jesse\@example.com
840 my $ticket = RT::Ticket->new($RT::SystemUser);
841 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
842 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
843 $ticket->UpdateFrom822($simple_update);
844 is($ticket->Subject, 'target', "changed the subject");
845 my $jesse = RT::User->new($RT::SystemUser);
846 $jesse->LoadByEmail('jesse@example.com');
847 ok ($jesse->Id, "There's a user for jesse");
848 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
858 my %args = $self->_Parse822HeadersForAttributes($content);
862 Queue => $args{'queue'},
863 Subject => $args{'subject'},
864 Status => $args{'status'},
866 Starts => $args{'starts'},
867 Started => $args{'started'},
868 Resolved => $args{'resolved'},
869 Owner => $args{'owner'},
870 Requestor => $args{'requestor'},
872 AdminCc => $args{'admincc'},
873 TimeWorked => $args{'timeworked'},
874 TimeEstimated => $args{'timeestimated'},
875 TimeLeft => $args{'timeleft'},
876 InitialPriority => $args{'initialpriority'},
877 Priority => $args{'priority'},
878 FinalPriority => $args{'finalpriority'},
879 Type => $args{'type'},
880 DependsOn => $args{'dependson'},
881 DependedOnBy => $args{'dependedonby'},
882 RefersTo => $args{'refersto'},
883 ReferredToBy => $args{'referredtoby'},
884 Members => $args{'members'},
885 MemberOf => $args{'memberof'},
886 MIMEObj => $args{'mimeobj'}
889 foreach my $type qw(Requestor Cc Admincc) {
891 foreach my $action ( 'Add', 'Del', '' ) {
893 my $lctag = lc($action) . lc($type);
894 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
896 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
897 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
902 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
907 # Add custom field entries to %ticketargs.
908 # TODO: allow named custom fields
910 /^customfield-(\d+)$/
911 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
914 # for each ticket we've been told to update, iterate through the set of
915 # rfc822 headers and perform that update to the ticket.
918 # {{{ Set basic fields
932 # Resolve the queue from a name to a numeric id.
933 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
934 my $tempqueue = RT::Queue->new($RT::SystemUser);
935 $tempqueue->Load( $ticketargs{'Queue'} );
936 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
941 foreach my $attribute (@attribs) {
942 my $value = $ticketargs{$attribute};
944 if ( $value ne $self->$attribute() ) {
946 my $method = "Set$attribute";
947 my ( $code, $msg ) = $self->$method($value);
949 push @results, $self->loc($attribute) . ': ' . $msg;
954 # We special case owner changing, so we can use ForceOwnerChange
955 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
956 my $ChownType = "Give";
957 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
959 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
960 push ( @results, $msg );
964 # Deal with setting watchers
967 # Acceptable arguments:
974 foreach my $type qw(Requestor Cc AdminCc) {
976 # If we've been given a number of delresses to del, do it.
977 foreach my $address (@{$ticketargs{'Del'.$type}}) {
978 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
979 push (@results, $msg) ;
982 # If we've been given a number of addresses to add, do it.
983 foreach my $address (@{$ticketargs{'Add'.$type}}) {
984 $RT::Logger->debug("Adding $address as a $type");
985 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
986 push (@results, $msg) ;
997 # {{{ _Parse822HeadersForAttributes Content
999 =head2 _Parse822HeadersForAttributes Content
1001 Takes an RFC822 style message and parses its attributes into a hash.
1005 sub _Parse822HeadersForAttributes {
1007 my $content = shift;
1010 my @lines = ( split ( /\n/, $content ) );
1011 while ( defined( my $line = shift @lines ) ) {
1012 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1017 if ( defined( $args{$tag} ) )
1018 { #if we're about to get a second value, make it an array
1019 $args{$tag} = [ $args{$tag} ];
1021 if ( ref( $args{$tag} ) )
1022 { #If it's an array, we want to push the value
1023 push @{ $args{$tag} }, $value;
1025 else { #if there's nothing there, just set the value
1026 $args{$tag} = $value;
1028 } elsif ($line =~ /^$/) {
1030 #TODO: this won't work, since "" isn't of the form "foo:value"
1032 while ( defined( my $l = shift @lines ) ) {
1033 push @{ $args{'content'} }, $l;
1039 foreach my $date qw(due starts started resolved) {
1040 my $dateobj = RT::Date->new($RT::SystemUser);
1041 if ( $args{$date} =~ /^\d+$/ ) {
1042 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1045 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1047 $args{$date} = $dateobj->ISO;
1049 $args{'mimeobj'} = MIME::Entity->new();
1050 $args{'mimeobj'}->build(
1051 Type => ( $args{'contenttype'} || 'text/plain' ),
1052 Data => ($args{'content'} || '')
1062 =head2 Import PARAMHASH
1065 Doesn\'t create a transaction.
1066 Doesn\'t supply queue defaults, etc.
1074 my ( $ErrStr, $QueueObj, $Owner );
1078 EffectiveId => undef,
1082 Owner => $RT::Nobody->Id,
1083 Subject => '[no subject]',
1084 InitialPriority => undef,
1085 FinalPriority => undef,
1096 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1097 $QueueObj = RT::Queue->new($RT::SystemUser);
1098 $QueueObj->Load( $args{'Queue'} );
1100 #TODO error check this and return 0 if it\'s not loading properly +++
1102 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1103 $QueueObj = RT::Queue->new($RT::SystemUser);
1104 $QueueObj->Load( $args{'Queue'}->Id );
1108 "$self " . $args{'Queue'} . " not a recognised queue object." );
1111 #Can't create a ticket without a queue.
1112 unless ( defined($QueueObj) and $QueueObj->Id ) {
1113 $RT::Logger->debug("$self No queue given for ticket creation.");
1114 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1117 #Now that we have a queue, Check the ACLS
1119 $self->CurrentUser->HasRight(
1120 Right => 'CreateTicket',
1126 $self->loc("No permission to create tickets in the queue '[_1]'"
1127 , $QueueObj->Name));
1130 # {{{ Deal with setting the owner
1132 # Attempt to take user object, user name or user id.
1133 # Assign to nobody if lookup fails.
1134 if ( defined( $args{'Owner'} ) ) {
1135 if ( ref( $args{'Owner'} ) ) {
1136 $Owner = $args{'Owner'};
1139 $Owner = new RT::User( $self->CurrentUser );
1140 $Owner->Load( $args{'Owner'} );
1141 if ( !defined( $Owner->id ) ) {
1142 $Owner->Load( $RT::Nobody->id );
1147 #If we have a proposed owner and they don't have the right
1148 #to own a ticket, scream about it and make them not the owner
1151 and ( $Owner->Id != $RT::Nobody->Id )
1154 Object => $QueueObj,
1155 Right => 'OwnTicket'
1161 $RT::Logger->warning( "$self user "
1162 . $Owner->Name . "("
1165 . "as a ticket owner but has no rights to own "
1167 . $QueueObj->Name . "'\n" );
1172 #If we haven't been handed a valid owner, make it nobody.
1173 unless ( defined($Owner) ) {
1174 $Owner = new RT::User( $self->CurrentUser );
1175 $Owner->Load( $RT::Nobody->UserObj->Id );
1180 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1181 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1184 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1185 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1186 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1187 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1189 # If we're coming in with an id, set that now.
1190 my $EffectiveId = undef;
1191 if ( $args{'id'} ) {
1192 $EffectiveId = $args{'id'};
1196 my $id = $self->SUPER::Create(
1198 EffectiveId => $EffectiveId,
1199 Queue => $QueueObj->Id,
1200 Owner => $Owner->Id,
1201 Subject => $args{'Subject'}, # loc
1202 InitialPriority => $args{'InitialPriority'}, # loc
1203 FinalPriority => $args{'FinalPriority'}, # loc
1204 Priority => $args{'InitialPriority'}, # loc
1205 Status => $args{'Status'}, # loc
1206 TimeWorked => $args{'TimeWorked'}, # loc
1207 Type => $args{'Type'}, # loc
1208 Created => $args{'Created'}, # loc
1209 Told => $args{'Told'}, # loc
1210 LastUpdated => $args{'Updated'}, # loc
1211 Resolved => $args{'Resolved'}, # loc
1212 Due => $args{'Due'}, # loc
1215 # If the ticket didn't have an id
1216 # Set the ticket's effective ID now that we've created it.
1217 if ( $args{'id'} ) {
1218 $self->Load( $args{'id'} );
1222 $self->__Set( Field => 'EffectiveId', Value => $id );
1226 $self . "->Import couldn't set EffectiveId: $msg\n" );
1230 my $create_groups_ret = $self->_CreateTicketGroups();
1231 unless ($create_groups_ret) {
1233 "Couldn't create ticket groups for ticket " . $self->Id );
1236 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1239 foreach $watcher ( @{ $args{'Cc'} } ) {
1240 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1242 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1243 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1246 foreach $watcher ( @{ $args{'Requestor'} } ) {
1247 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1251 return ( $self->Id, $ErrStr );
1256 # {{{ Routines dealing with watchers.
1258 # {{{ _CreateTicketGroups
1260 =head2 _CreateTicketGroups
1262 Create the ticket groups and links for this ticket.
1263 This routine expects to be called from Ticket->Create _inside of a transaction_
1265 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1267 It will return true on success and undef on failure.
1271 my $ticket = RT::Ticket->new($RT::SystemUser);
1272 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1273 Owner => $RT::SystemUser->Id,
1275 Requestor => ['jesse@example.com'],
1278 ok ($id, "Ticket $id was created");
1279 ok(my $group = RT::Group->new($RT::SystemUser));
1280 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1281 ok ($group->Id, "Found the requestors object for this ticket");
1283 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1284 $jesse->LoadByEmail('jesse@example.com');
1285 ok($jesse->Id, "Found the jesse rt user");
1288 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1289 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1290 ok ($add_id, "Add succeeded: ($add_msg)");
1291 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1292 $bob->LoadByEmail('bob@fsck.com');
1293 ok($bob->Id, "Found the bob rt user");
1294 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1295 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1296 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1299 $group = RT::Group->new($RT::SystemUser);
1300 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1301 ok ($group->Id, "Found the cc object for this ticket");
1302 $group = RT::Group->new($RT::SystemUser);
1303 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1304 ok ($group->Id, "Found the AdminCc object for this ticket");
1305 $group = RT::Group->new($RT::SystemUser);
1306 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1307 ok ($group->Id, "Found the Owner object for this ticket");
1308 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1315 sub _CreateTicketGroups {
1318 my @types = qw(Requestor Owner Cc AdminCc);
1320 foreach my $type (@types) {
1321 my $type_obj = RT::Group->new($self->CurrentUser);
1322 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1323 Instance => $self->Id,
1326 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1327 $self->Id.": ".$msg);
1337 # {{{ sub OwnerGroup
1341 A constructor which returns an RT::Group object containing the owner of this ticket.
1347 my $owner_obj = RT::Group->new($self->CurrentUser);
1348 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1349 return ($owner_obj);
1355 # {{{ sub AddWatcher
1359 AddWatcher takes a parameter hash. The keys are as follows:
1361 Type One of Requestor, Cc, AdminCc
1363 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1365 Email The email address of the new watcher. If a user with this
1366 email address can't be found, a new nonprivileged user will be created.
1368 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.
1376 PrincipalId => undef,
1381 # XXX, FIXME, BUG: if only email is provided then we only check
1382 # for ModifyTicket right, but must try to get PrincipalId and
1383 # check Watch* rights too if user exist
1386 #If the watcher we're trying to add is for the current user
1387 if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0)
1388 or lc( $self->CurrentUser->UserObj->EmailAddress )
1389 eq lc( RT::User->CanonicalizeEmailAddress( $args{'Email'} ) || '' ) )
1391 # If it's an AdminCc and they don't have
1392 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1393 if ( $args{'Type'} eq 'AdminCc' ) {
1394 unless ( $self->CurrentUserHasRight('ModifyTicket')
1395 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1396 return ( 0, $self->loc('Permission Denied'))
1400 # If it's a Requestor or Cc and they don't have
1401 # 'Watch' or 'ModifyTicket', bail
1402 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1404 unless ( $self->CurrentUserHasRight('ModifyTicket')
1405 or $self->CurrentUserHasRight('Watch') ) {
1406 return ( 0, $self->loc('Permission Denied'))
1410 $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1411 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1415 # If the watcher isn't the current user
1416 # and the current user doesn't have 'ModifyTicket'
1419 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1420 return ( 0, $self->loc("Permission Denied") );
1426 return ( $self->_AddWatcher(%args) );
1429 #This contains the meat of AddWatcher. but can be called from a routine like
1430 # Create, which doesn't need the additional acl check
1436 PrincipalId => undef,
1442 my $principal = RT::Principal->new($self->CurrentUser);
1443 if ($args{'Email'}) {
1444 my $user = RT::User->new($RT::SystemUser);
1445 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1446 # If we can't load the user by email address, let's try to load by username
1448 ($pid,$msg) = $user->Load($args{'Email'})
1451 $args{'PrincipalId'} = $pid;
1454 if ($args{'PrincipalId'}) {
1455 $principal->Load($args{'PrincipalId'});
1459 # If we can't find this watcher, we need to bail.
1460 unless ($principal->Id) {
1461 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1462 return(0, $self->loc("Could not find or create that user"));
1466 my $group = RT::Group->new($self->CurrentUser);
1467 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1468 unless ($group->id) {
1469 return(0,$self->loc("Group not found"));
1472 if ( $group->HasMember( $principal)) {
1474 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1478 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1479 InsideTransaction => 1 );
1481 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1483 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1486 unless ( $args{'Silent'} ) {
1487 $self->_NewTransaction(
1488 Type => 'AddWatcher',
1489 NewValue => $principal->Id,
1490 Field => $args{'Type'}
1494 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1500 # {{{ sub DeleteWatcher
1502 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1505 Deletes a Ticket watcher. Takes two arguments:
1507 Type (one of Requestor,Cc,AdminCc)
1511 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1513 Email (the email address of an existing wathcer)
1522 my %args = ( Type => undef,
1523 PrincipalId => undef,
1527 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1528 return ( 0, $self->loc("No principal specified") );
1530 my $principal = RT::Principal->new( $self->CurrentUser );
1531 if ( $args{'PrincipalId'} ) {
1533 $principal->Load( $args{'PrincipalId'} );
1536 my $user = RT::User->new( $self->CurrentUser );
1537 $user->LoadByEmail( $args{'Email'} );
1538 $principal->Load( $user->Id );
1541 # If we can't find this watcher, we need to bail.
1542 unless ( $principal->Id ) {
1543 return ( 0, $self->loc("Could not find that principal") );
1546 my $group = RT::Group->new( $self->CurrentUser );
1547 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1548 unless ( $group->id ) {
1549 return ( 0, $self->loc("Group not found") );
1553 #If the watcher we're trying to add is for the current user
1554 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1556 # If it's an AdminCc and they don't have
1557 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1558 if ( $args{'Type'} eq 'AdminCc' ) {
1559 unless ( $self->CurrentUserHasRight('ModifyTicket')
1560 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1561 return ( 0, $self->loc('Permission Denied') );
1565 # If it's a Requestor or Cc and they don't have
1566 # 'Watch' or 'ModifyTicket', bail
1567 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1569 unless ( $self->CurrentUserHasRight('ModifyTicket')
1570 or $self->CurrentUserHasRight('Watch') ) {
1571 return ( 0, $self->loc('Permission Denied') );
1575 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1577 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1581 # If the watcher isn't the current user
1582 # and the current user doesn't have 'ModifyTicket' bail
1584 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1585 return ( 0, $self->loc("Permission Denied") );
1591 # see if this user is already a watcher.
1593 unless ( $group->HasMember($principal) ) {
1595 $self->loc( 'That principal is not a [_1] for this ticket',
1599 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1601 $RT::Logger->error( "Failed to delete "
1603 . " as a member of group "
1609 'Could not remove that principal as a [_1] for this ticket',
1613 unless ( $args{'Silent'} ) {
1614 $self->_NewTransaction( Type => 'DelWatcher',
1615 OldValue => $principal->Id,
1616 Field => $args{'Type'} );
1620 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1621 $principal->Object->Name,
1630 =head2 SquelchMailTo [EMAIL]
1632 Takes an optional email address to never email about updates to this ticket.
1635 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1639 my $t = RT::Ticket->new($RT::SystemUser);
1640 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1642 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1644 my @returned = $t->SquelchMailTo('nobody@example.com');
1646 is($#returned, 0, "The ticket has one squelched recipients");
1648 my @names = $t->Attributes->Names;
1649 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1650 @returned = $t->SquelchMailTo('nobody@example.com');
1653 is($#returned, 0, "The ticket has one squelched recipients");
1655 @names = $t->Attributes->Names;
1656 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1659 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1660 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1661 @returned = $t->SquelchMailTo();
1662 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1672 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1676 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1677 unless grep { $_->Content eq $attr }
1678 $self->Attributes->Named('SquelchMailTo');
1681 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1684 my @attributes = $self->Attributes->Named('SquelchMailTo');
1685 return (@attributes);
1689 =head2 UnsquelchMailTo ADDRESS
1691 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1693 Returns a tuple of (status, message)
1697 sub UnsquelchMailTo {
1700 my $address = shift;
1701 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1702 return ( 0, $self->loc("Permission Denied") );
1705 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1706 return ($val, $msg);
1710 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1712 =head2 RequestorAddresses
1714 B<Returns> String: All Ticket Requestor email addresses as a string.
1718 sub RequestorAddresses {
1721 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1725 return ( $self->Requestors->MemberEmailAddressesAsString );
1729 =head2 AdminCcAddresses
1731 returns String: All Ticket AdminCc email addresses as a string
1735 sub AdminCcAddresses {
1738 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1742 return ( $self->AdminCc->MemberEmailAddressesAsString )
1748 returns String: All Ticket Ccs as a string of email addresses
1755 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1759 return ( $self->Cc->MemberEmailAddressesAsString);
1765 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1767 # {{{ sub Requestors
1772 Returns this ticket's Requestors as an RT::Group object
1779 my $group = RT::Group->new($self->CurrentUser);
1780 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1781 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1794 Returns an RT::Group object which contains this ticket's Ccs.
1795 If the user doesn't have "ShowTicket" permission, returns an empty group
1802 my $group = RT::Group->new($self->CurrentUser);
1803 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1804 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1817 Returns an RT::Group object which contains this ticket's AdminCcs.
1818 If the user doesn't have "ShowTicket" permission, returns an empty group
1825 my $group = RT::Group->new($self->CurrentUser);
1826 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1827 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1837 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1840 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1842 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1844 Takes a param hash with the attributes Type and either PrincipalId or Email
1846 Type is one of Requestor, Cc, AdminCc and Owner
1848 PrincipalId is an RT::Principal id, and Email is an email address.
1850 Returns true if the specified principal (or the one corresponding to the
1851 specified address) is a member of the group Type for this ticket.
1853 XX TODO: This should be Memoized.
1860 my %args = ( Type => 'Requestor',
1861 PrincipalId => undef,
1866 # Load the relevant group.
1867 my $group = RT::Group->new($self->CurrentUser);
1868 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1870 # Find the relevant principal.
1871 my $principal = RT::Principal->new($self->CurrentUser);
1872 if (!$args{PrincipalId} && $args{Email}) {
1873 # Look up the specified user.
1874 my $user = RT::User->new($self->CurrentUser);
1875 $user->LoadByEmail($args{Email});
1877 $args{PrincipalId} = $user->PrincipalId;
1880 # A non-existent user can't be a group member.
1884 $principal->Load($args{'PrincipalId'});
1886 # Ask if it has the member in question
1887 return ($group->HasMember($principal));
1892 # {{{ sub IsRequestor
1894 =head2 IsRequestor PRINCIPAL_ID
1896 Takes an RT::Principal id
1897 Returns true if the principal is a requestor of the current ticket.
1906 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1914 =head2 IsCc PRINCIPAL_ID
1916 Takes an RT::Principal id.
1917 Returns true if the principal is a requestor of the current ticket.
1926 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1934 =head2 IsAdminCc PRINCIPAL_ID
1936 Takes an RT::Principal id.
1937 Returns true if the principal is a requestor of the current ticket.
1945 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1955 Takes an RT::User object. Returns true if that user is this ticket's owner.
1956 returns undef otherwise
1964 # no ACL check since this is used in acl decisions
1965 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1969 #Tickets won't yet have owners when they're being created.
1970 unless ( $self->OwnerObj->id ) {
1974 if ( $person->id == $self->OwnerObj->id ) {
1988 # {{{ Routines dealing with queues
1990 # {{{ sub ValidateQueue
1997 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
2001 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2002 my $id = $QueueObj->Load($Value);
2018 my $NewQueue = shift;
2020 #Redundant. ACL gets checked in _Set;
2021 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2022 return ( 0, $self->loc("Permission Denied") );
2025 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2026 $NewQueueObj->Load($NewQueue);
2028 unless ( $NewQueueObj->Id() ) {
2029 return ( 0, $self->loc("That queue does not exist") );
2032 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2033 return ( 0, $self->loc('That is the same value') );
2036 $self->CurrentUser->HasRight(
2037 Right => 'CreateTicket',
2038 Object => $NewQueueObj
2042 return ( 0, $self->loc("You may not create requests in that queue.") );
2046 $self->OwnerObj->HasRight(
2047 Right => 'OwnTicket',
2048 Object => $NewQueueObj
2052 my $clone = RT::Ticket->new( $RT::SystemUser );
2053 $clone->Load( $self->Id );
2054 unless ( $clone->Id ) {
2055 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2057 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2058 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2061 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2070 Takes nothing. returns this ticket's queue object
2077 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2079 #We call __Value so that we can avoid the ACL decision and some deep recursion
2080 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2081 return ($queue_obj);
2088 # {{{ Date printing routines
2094 Returns an RT::Date object containing this ticket's due date
2101 my $time = new RT::Date( $self->CurrentUser );
2103 # -1 is RT::Date slang for never
2105 $time->Set( Format => 'sql', Value => $self->Due );
2108 $time->Set( Format => 'unix', Value => -1 );
2116 # {{{ sub DueAsString
2120 Returns this ticket's due date as a human readable string
2126 return $self->DueObj->AsString();
2131 # {{{ sub ResolvedObj
2135 Returns an RT::Date object of this ticket's 'resolved' time.
2142 my $time = new RT::Date( $self->CurrentUser );
2143 $time->Set( Format => 'sql', Value => $self->Resolved );
2149 # {{{ sub SetStarted
2153 Takes a date in ISO format or undef
2154 Returns a transaction id and a message
2155 The client calls "Start" to note that the project was started on the date in $date.
2156 A null date means "now"
2162 my $time = shift || 0;
2164 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2165 return ( 0, $self->loc("Permission Denied") );
2168 #We create a date object to catch date weirdness
2169 my $time_obj = new RT::Date( $self->CurrentUser() );
2171 $time_obj->Set( Format => 'ISO', Value => $time );
2174 $time_obj->SetToNow();
2177 #Now that we're starting, open this ticket
2178 #TODO do we really want to force this as policy? it should be a scrip
2180 #We need $TicketAsSystem, in case the current user doesn't have
2183 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2184 $TicketAsSystem->Load( $self->Id );
2185 if ( $TicketAsSystem->Status eq 'new' ) {
2186 $TicketAsSystem->Open();
2189 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2195 # {{{ sub StartedObj
2199 Returns an RT::Date object which contains this ticket's
2207 my $time = new RT::Date( $self->CurrentUser );
2208 $time->Set( Format => 'sql', Value => $self->Started );
2218 Returns an RT::Date object which contains this ticket's
2226 my $time = new RT::Date( $self->CurrentUser );
2227 $time->Set( Format => 'sql', Value => $self->Starts );
2237 Returns an RT::Date object which contains this ticket's
2245 my $time = new RT::Date( $self->CurrentUser );
2246 $time->Set( Format => 'sql', Value => $self->Told );
2252 # {{{ sub ToldAsString
2256 A convenience method that returns ToldObj->AsString
2258 TODO: This should be deprecated
2264 if ( $self->Told ) {
2265 return $self->ToldObj->AsString();
2274 # {{{ sub TimeWorkedAsString
2276 =head2 TimeWorkedAsString
2278 Returns the amount of time worked on this ticket as a Text String
2282 sub TimeWorkedAsString {
2284 return "0" unless $self->TimeWorked;
2286 #This is not really a date object, but if we diff a number of seconds
2287 #vs the epoch, we'll get a nice description of time worked.
2289 my $worked = new RT::Date( $self->CurrentUser );
2291 #return the #of minutes worked turned into seconds and written as
2292 # a simple text string
2294 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2301 # {{{ Routines dealing with correspondence/comments
2307 Comment on this ticket.
2308 Takes a hashref with the following attributes:
2309 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2312 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2314 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2315 They will, however, be prepared and you'll be able to access them through the TransactionObj
2317 Returns: Transaction id, Error Message, Transaction Object
2318 (note the different order from Create()!)
2325 my %args = ( CcMessageTo => undef,
2326 BccMessageTo => undef,
2333 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2334 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2335 return ( 0, $self->loc("Permission Denied"), undef );
2337 $args{'NoteType'} = 'Comment';
2339 if ($args{'DryRun'}) {
2340 $RT::Handle->BeginTransaction();
2341 $args{'CommitScrips'} = 0;
2344 my @results = $self->_RecordNote(%args);
2345 if ($args{'DryRun'}) {
2346 $RT::Handle->Rollback();
2353 # {{{ sub Correspond
2357 Correspond on this ticket.
2358 Takes a hashref with the following attributes:
2361 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2363 if there's no MIMEObj, Content is used to build a MIME::Entity object
2365 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2366 They will, however, be prepared and you'll be able to access them through the TransactionObj
2368 Returns: Transaction id, Error Message, Transaction Object
2369 (note the different order from Create()!)
2376 my %args = ( CcMessageTo => undef,
2377 BccMessageTo => undef,
2383 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2384 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2385 return ( 0, $self->loc("Permission Denied"), undef );
2388 $args{'NoteType'} = 'Correspond';
2389 if ($args{'DryRun'}) {
2390 $RT::Handle->BeginTransaction();
2391 $args{'CommitScrips'} = 0;
2394 my @results = $self->_RecordNote(%args);
2396 #Set the last told date to now if this isn't mail from the requestor.
2397 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2398 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2400 if ($args{'DryRun'}) {
2401 $RT::Handle->Rollback();
2410 # {{{ sub _RecordNote
2414 the meat of both comment and correspond.
2416 Performs no access control checks. hence, dangerous.
2423 my %args = ( CcMessageTo => undef,
2424 BccMessageTo => undef,
2431 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2432 return ( 0, $self->loc("No message attached"), undef );
2434 unless ( $args{'MIMEObj'} ) {
2435 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2436 ref $args{'Content'}
2438 : [ $args{'Content'} ]
2442 # convert text parts into utf-8
2443 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2445 # If we've been passed in CcMessageTo and BccMessageTo fields,
2446 # add them to the mime object for passing on to the transaction handler
2447 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2451 foreach my $type (qw/Cc Bcc/) {
2452 if ( defined $args{ $type . 'MessageTo' } ) {
2454 my $addresses = join ', ', (
2455 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2456 Mail::Address->parse( $args{ $type . 'MessageTo' } ) );
2457 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2461 # If this is from an external source, we need to come up with its
2462 # internal Message-ID now, so all emails sent because of this
2463 # message have a common Message-ID
2464 unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2465 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2467 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2469 . $RT::VERSION . "-"
2471 . CORE::time() . "-"
2472 . int(rand(2000)) . '.'
2475 . "0" . "@" # Email sent
2480 #Record the correspondence (write the transaction)
2481 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2482 Type => $args{'NoteType'},
2483 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2484 TimeTaken => $args{'TimeTaken'},
2485 MIMEObj => $args{'MIMEObj'},
2486 CommitScrips => $args{'CommitScrips'},
2490 $RT::Logger->err("$self couldn't init a transaction $msg");
2491 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2494 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2506 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2509 my $type = shift || "";
2511 unless ( $self->{"$field$type"} ) {
2512 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2513 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2514 # Maybe this ticket is a merged ticket
2515 my $Tickets = new RT::Tickets( $self->CurrentUser );
2516 # at least to myself
2517 $self->{"$field$type"}->Limit( FIELD => $field,
2518 VALUE => $self->URI,
2519 ENTRYAGGREGATOR => 'OR' );
2520 $Tickets->Limit( FIELD => 'EffectiveId',
2521 VALUE => $self->EffectiveId );
2522 while (my $Ticket = $Tickets->Next) {
2523 $self->{"$field$type"}->Limit( FIELD => $field,
2524 VALUE => $Ticket->URI,
2525 ENTRYAGGREGATOR => 'OR' );
2527 $self->{"$field$type"}->Limit( FIELD => 'Type',
2532 return ( $self->{"$field$type"} );
2537 # {{{ sub DeleteLink
2541 Delete a link. takes a paramhash of Base, Target and Type.
2542 Either Base or Target must be null. The null value will
2543 be replaced with this ticket\'s id
2556 unless ( $args{'Target'} || $args{'Base'} ) {
2557 $RT::Logger->error("Base or Target must be specified\n");
2558 return ( 0, $self->loc('Either base or target must be specified') );
2563 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2564 if ( !$right && $RT::StrictLinkACL ) {
2565 return ( 0, $self->loc("Permission Denied") );
2568 # If the other URI is an RT::Ticket, we want to make sure the user
2569 # can modify it too...
2570 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2571 return (0, $msg) unless $status;
2572 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2575 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2576 ( $RT::StrictLinkACL && $right < 2 ) )
2578 return ( 0, $self->loc("Permission Denied") );
2581 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2584 $RT::Logger->debug("Couldn't find that link\n");
2588 my ($direction, $remote_link);
2590 if ( $args{'Base'} ) {
2591 $remote_link = $args{'Base'};
2592 $direction = 'Target';
2594 elsif ( $args{'Target'} ) {
2595 $remote_link = $args{'Target'};
2599 if ( $args{'Silent'} ) {
2600 return ( $val, $Msg );
2603 my $remote_uri = RT::URI->new( $self->CurrentUser );
2604 $remote_uri->FromURI( $remote_link );
2606 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2607 Type => 'DeleteLink',
2608 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2609 OldValue => $remote_uri->URI || $remote_link,
2613 if ( $remote_uri->IsLocal ) {
2615 my $OtherObj = $remote_uri->Object;
2616 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2617 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2618 : $LINKDIRMAP{$args{'Type'}}->{Target},
2619 OldValue => $self->URI,
2620 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2624 return ( $Trans, $Msg );
2634 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2640 my %args = ( Target => '',
2646 unless ( $args{'Target'} || $args{'Base'} ) {
2647 $RT::Logger->error("Base or Target must be specified\n");
2648 return ( 0, $self->loc('Either base or target must be specified') );
2652 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2653 if ( !$right && $RT::StrictLinkACL ) {
2654 return ( 0, $self->loc("Permission Denied") );
2657 # If the other URI is an RT::Ticket, we want to make sure the user
2658 # can modify it too...
2659 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2660 return (0, $msg) unless $status;
2661 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2664 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2665 ( $RT::StrictLinkACL && $right < 2 ) )
2667 return ( 0, $self->loc("Permission Denied") );
2670 return $self->_AddLink(%args);
2673 sub __GetTicketFromURI {
2675 my %args = ( URI => '', @_ );
2677 # If the other URI is an RT::Ticket, we want to make sure the user
2678 # can modify it too...
2679 my $uri_obj = RT::URI->new( $self->CurrentUser );
2680 $uri_obj->FromURI( $args{'URI'} );
2682 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2683 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2684 $RT::Logger->warning( "$msg\n" );
2687 my $obj = $uri_obj->Resolver->Object;
2688 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2689 return (1, 'Found not a ticket', undef);
2691 return (1, 'Found ticket', $obj);
2696 Private non-acled variant of AddLink so that links can be added during create.
2702 my %args = ( Target => '',
2708 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2709 return ($val, $msg) if !$val || $exist;
2711 my ($direction, $remote_link);
2712 if ( $args{'Target'} ) {
2713 $remote_link = $args{'Target'};
2714 $direction = 'Base';
2715 } elsif ( $args{'Base'} ) {
2716 $remote_link = $args{'Base'};
2717 $direction = 'Target';
2720 # Don't write the transaction if we're doing this on create
2721 if ( $args{'Silent'} ) {
2722 return ( $val, $msg );
2725 my $remote_uri = RT::URI->new( $self->CurrentUser );
2726 $remote_uri->FromURI( $remote_link );
2728 #Write the transaction
2729 my ( $Trans, $Msg, $TransObj ) =
2730 $self->_NewTransaction(Type => 'AddLink',
2731 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2732 NewValue => $remote_uri->URI || $remote_link,
2735 if ( $remote_uri->IsLocal ) {
2737 my $OtherObj = $remote_uri->Object;
2738 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2739 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2740 : $LINKDIRMAP{$args{'Type'}}->{Target},
2741 NewValue => $self->URI,
2742 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2745 return ( $val, $Msg );
2757 MergeInto take the id of the ticket to merge this ticket into.
2762 my $t1 = RT::Ticket->new($RT::SystemUser);
2763 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2765 my $t2 = RT::Ticket->new($RT::SystemUser);
2766 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2768 my ($msg, $val) = $t1->MergeInto($t2->id);
2770 $t1 = RT::Ticket->new($RT::SystemUser);
2771 is ($t1->id, undef, "ok. we've got a blank ticket1");
2774 is ($t1->id, $t2->id);
2776 is ($t1->Requestors->MembersObj->Count, 2);
2785 my $ticket_id = shift;
2787 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2788 return ( 0, $self->loc("Permission Denied") );
2791 # Load up the new ticket.
2792 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2793 $MergeInto->Load($ticket_id);
2795 # make sure it exists.
2796 unless ( $MergeInto->Id ) {
2797 return ( 0, $self->loc("New ticket doesn't exist") );
2800 # Make sure the current user can modify the new ticket.
2801 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2802 return ( 0, $self->loc("Permission Denied") );
2805 $RT::Handle->BeginTransaction();
2807 # We use EffectiveId here even though it duplicates information from
2808 # the links table becasue of the massive performance hit we'd take
2809 # by trying to do a separate database query for merge info everytime
2812 #update this ticket's effective id to the new ticket's id.
2813 my ( $id_val, $id_msg ) = $self->__Set(
2814 Field => 'EffectiveId',
2815 Value => $MergeInto->Id()
2819 $RT::Handle->Rollback();
2820 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2824 if ( $self->__Value('Status') ne 'resolved' ) {
2826 my ( $status_val, $status_msg )
2827 = $self->__Set( Field => 'Status', Value => 'resolved' );
2829 unless ($status_val) {
2830 $RT::Handle->Rollback();
2833 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2837 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2841 # update all the links that point to that old ticket
2842 my $old_links_to = RT::Links->new($self->CurrentUser);
2843 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2846 while (my $link = $old_links_to->Next) {
2847 if (exists $old_seen{$link->Base."-".$link->Type}) {
2850 elsif ($link->Base eq $MergeInto->URI) {
2853 # First, make sure the link doesn't already exist. then move it over.
2854 my $tmp = RT::Link->new($RT::SystemUser);
2855 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2859 $link->SetTarget($MergeInto->URI);
2860 $link->SetLocalTarget($MergeInto->id);
2862 $old_seen{$link->Base."-".$link->Type} =1;
2867 my $old_links_from = RT::Links->new($self->CurrentUser);
2868 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2870 while (my $link = $old_links_from->Next) {
2871 if (exists $old_seen{$link->Type."-".$link->Target}) {
2874 if ($link->Target eq $MergeInto->URI) {
2877 # First, make sure the link doesn't already exist. then move it over.
2878 my $tmp = RT::Link->new($RT::SystemUser);
2879 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2883 $link->SetBase($MergeInto->URI);
2884 $link->SetLocalBase($MergeInto->id);
2885 $old_seen{$link->Type."-".$link->Target} =1;
2891 # Update time fields
2892 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2894 my $mutator = "Set$type";
2895 $MergeInto->$mutator(
2896 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2899 #add all of this ticket's watchers to that ticket.
2900 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2902 my $people = $self->$watcher_type->MembersObj;
2903 my $addwatcher_type = $watcher_type;
2904 $addwatcher_type =~ s/s$//;
2906 while ( my $watcher = $people->Next ) {
2908 my ($val, $msg) = $MergeInto->_AddWatcher(
2909 Type => $addwatcher_type,
2911 PrincipalId => $watcher->MemberId
2914 $RT::Logger->warning($msg);
2920 #find all of the tickets that were merged into this ticket.
2921 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2922 $old_mergees->Limit(
2923 FIELD => 'EffectiveId',
2928 # update their EffectiveId fields to the new ticket's id
2929 while ( my $ticket = $old_mergees->Next() ) {
2930 my ( $val, $msg ) = $ticket->__Set(
2931 Field => 'EffectiveId',
2932 Value => $MergeInto->Id()
2936 #make a new link: this ticket is merged into that other ticket.
2937 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2939 $MergeInto->_SetLastUpdated;
2941 $RT::Handle->Commit();
2942 return ( 1, $self->loc("Merge Successful") );
2949 # {{{ Routines dealing with ownership
2955 Takes nothing and returns an RT::User object of
2963 #If this gets ACLed, we lose on a rights check in User.pm and
2964 #get deep recursion. if we need ACLs here, we need
2965 #an equiv without ACLs
2967 my $owner = new RT::User( $self->CurrentUser );
2968 $owner->Load( $self->__Value('Owner') );
2970 #Return the owner object
2976 # {{{ sub OwnerAsString
2978 =head2 OwnerAsString
2980 Returns the owner's email address
2986 return ( $self->OwnerObj->EmailAddress );
2996 Takes two arguments:
2997 the Id or Name of the owner
2998 and (optionally) the type of the SetOwner Transaction. It defaults
2999 to 'Give'. 'Steal' is also a valid option.
3003 my $root = RT::User->new($RT::SystemUser);
3004 $root->Load('root');
3005 ok ($root->Id, "Loaded the root user");
3006 my $t = RT::Ticket->new($RT::SystemUser);
3008 $t->SetOwner('root');
3009 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
3011 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
3012 my $txns = RT::Transactions->new($RT::SystemUser);
3013 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3014 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
3015 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
3016 $txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
3018 my $steal = $txns->First;
3019 ok($steal->OldValue == $root->Id , "Stolen from root");
3020 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3028 my $NewOwner = shift;
3029 my $Type = shift || "Give";
3031 $RT::Handle->BeginTransaction();
3033 $self->_SetLastUpdated(); # lock the ticket
3034 $self->Load( $self->id ); # in case $self changed while waiting for lock
3036 my $OldOwnerObj = $self->OwnerObj;
3038 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3039 $NewOwnerObj->Load( $NewOwner );
3040 unless ( $NewOwnerObj->Id ) {
3041 $RT::Handle->Rollback();
3042 return ( 0, $self->loc("That user does not exist") );
3046 # must have ModifyTicket rights
3047 # or TakeTicket/StealTicket and $NewOwner is self
3048 # see if it's a take
3049 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3050 unless ( $self->CurrentUserHasRight('ModifyTicket')
3051 || $self->CurrentUserHasRight('TakeTicket') ) {
3052 $RT::Handle->Rollback();
3053 return ( 0, $self->loc("Permission Denied") );
3057 # see if it's a steal
3058 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
3059 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3061 unless ( $self->CurrentUserHasRight('ModifyTicket')
3062 || $self->CurrentUserHasRight('StealTicket') ) {
3063 $RT::Handle->Rollback();
3064 return ( 0, $self->loc("Permission Denied") );
3068 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3069 $RT::Handle->Rollback();
3070 return ( 0, $self->loc("Permission Denied") );
3074 # If we're not stealing and the ticket has an owner and it's not
3076 if ( $Type ne 'Steal' and $Type ne 'Force'
3077 and $OldOwnerObj->Id != $RT::Nobody->Id
3078 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3080 $RT::Handle->Rollback();
3081 return ( 0, $self->loc("You can only take tickets that are unowned") )
3082 if $NewOwnerObj->id == $self->CurrentUser->id;
3085 $self->loc("You can only reassign tickets that you own or that are unowned" )
3089 #If we've specified a new owner and that user can't modify the ticket
3090 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3091 $RT::Handle->Rollback();
3092 return ( 0, $self->loc("That user may not own tickets in that queue") );
3095 # If the ticket has an owner and it's the new owner, we don't need
3097 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3098 $RT::Handle->Rollback();
3099 return ( 0, $self->loc("That user already owns that ticket") );
3102 # Delete the owner in the owner group, then add a new one
3103 # TODO: is this safe? it's not how we really want the API to work
3104 # for most things, but it's fast.
3105 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3107 $RT::Handle->Rollback();
3108 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3111 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3112 PrincipalId => $NewOwnerObj->PrincipalId,
3113 InsideTransaction => 1 );
3115 $RT::Handle->Rollback();
3116 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3119 # We call set twice with slightly different arguments, so
3120 # as to not have an SQL transaction span two RT transactions
3122 my ( $val, $msg ) = $self->_Set(
3124 RecordTransaction => 0,
3125 Value => $NewOwnerObj->Id,
3127 TransactionType => $Type,
3128 CheckACL => 0, # don't check acl
3132 $RT::Handle->Rollback;
3133 return ( 0, $self->loc("Could not change owner. ") . $msg );
3136 ($val, $msg) = $self->_NewTransaction(
3139 NewValue => $NewOwnerObj->Id,
3140 OldValue => $OldOwnerObj->Id,
3145 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3146 $OldOwnerObj->Name, $NewOwnerObj->Name );
3149 $RT::Handle->Rollback();
3153 $RT::Handle->Commit();
3155 return ( $val, $msg );
3164 A convenince method to set the ticket's owner to the current user
3170 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3179 Convenience method to set the owner to 'nobody' if the current user is the owner.
3185 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3194 A convenience method to change the owner of the current ticket to the
3195 current user. Even if it's owned by another user.
3202 if ( $self->IsOwner( $self->CurrentUser ) ) {
3203 return ( 0, $self->loc("You already own this ticket") );
3206 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3216 # {{{ Routines dealing with status
3218 # {{{ sub ValidateStatus
3220 =head2 ValidateStatus STATUS
3222 Takes a string. Returns true if that status is a valid status for this ticket.
3223 Returns false otherwise.
3227 sub ValidateStatus {
3231 #Make sure the status passed in is valid
3232 unless ( $self->QueueObj->IsValidStatus($status) ) {
3244 =head2 SetStatus STATUS
3246 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3248 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.
3252 my $tt = RT::Ticket->new($RT::SystemUser);
3253 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3256 is($tt->Status, 'new', "New ticket is created as new");
3258 ($id, $msg) = $tt->SetStatus('open');
3260 like($msg, qr/open/i, "Status message is correct");
3261 ($id, $msg) = $tt->SetStatus('resolved');
3263 like($msg, qr/resolved/i, "Status message is correct");
3264 ($id, $msg) = $tt->SetStatus('resolved');
3278 $args{Status} = shift;
3285 if ( $args{Status} eq 'deleted') {
3286 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3287 return ( 0, $self->loc('Permission Denied') );
3290 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3291 return ( 0, $self->loc('Permission Denied') );
3295 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3296 return (0, $self->loc('That ticket has unresolved dependencies'));
3299 my $now = RT::Date->new( $self->CurrentUser );
3302 #If we're changing the status from new, record that we've started
3303 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3305 #Set the Started time to "now"
3306 $self->_Set( Field => 'Started',
3308 RecordTransaction => 0 );
3311 #When we close a ticket, set the 'Resolved' attribute to now.
3312 # It's misnamed, but that's just historical.
3313 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3314 $self->_Set( Field => 'Resolved',
3316 RecordTransaction => 0 );
3319 #Actually update the status
3320 my ($val, $msg)= $self->_Set( Field => 'Status',
3321 Value => $args{Status},
3324 TransactionType => 'Status' );
3335 Takes no arguments. Marks this ticket for garbage collection
3341 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3342 return $self->Delete;
3347 return ( $self->SetStatus('deleted') );
3349 # TODO: garbage collection
3358 Sets this ticket's status to stalled
3364 return ( $self->SetStatus('stalled') );
3373 Sets this ticket's status to rejected
3379 return ( $self->SetStatus('rejected') );
3388 Sets this ticket\'s status to Open
3394 return ( $self->SetStatus('open') );
3403 Sets this ticket\'s status to Resolved
3409 return ( $self->SetStatus('resolved') );
3417 # {{{ Actions + Routines dealing with transactions
3419 # {{{ sub SetTold and _SetTold
3421 =head2 SetTold ISO [TIMETAKEN]
3423 Updates the told and records a transaction
3430 $told = shift if (@_);
3431 my $timetaken = shift || 0;
3433 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3434 return ( 0, $self->loc("Permission Denied") );
3437 my $datetold = new RT::Date( $self->CurrentUser );
3439 $datetold->Set( Format => 'iso',
3443 $datetold->SetToNow();
3446 return ( $self->_Set( Field => 'Told',
3447 Value => $datetold->ISO,
3448 TimeTaken => $timetaken,
3449 TransactionType => 'Told' ) );
3454 Updates the told without a transaction or acl check. Useful when we're sending replies.
3461 my $now = new RT::Date( $self->CurrentUser );
3464 #use __Set to get no ACLs ;)
3465 return ( $self->__Set( Field => 'Told',
3466 Value => $now->ISO ) );
3471 =head2 TransactionBatch
3473 Returns an array reference of all transactions created on this ticket during
3474 this ticket object's lifetime, or undef if there were none.
3476 Only works when the $RT::UseTransactionBatch config variable is set to true.
3480 sub TransactionBatch {
3482 return $self->{_TransactionBatch};
3488 # DESTROY methods need to localize $@, or it may unset it. This
3489 # causes $m->abort to not bubble all of the way up. See perlbug
3490 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3493 # The following line eliminates reentrancy.
3494 # It protects against the fact that perl doesn't deal gracefully
3495 # when an object's refcount is changed in its destructor.
3496 return if $self->{_Destroyed}++;
3498 my $batch = $self->TransactionBatch or return;
3499 return unless @$batch;
3502 RT::Scrips->new($RT::SystemUser)->Apply(
3503 Stage => 'TransactionBatch',
3505 TransactionObj => $batch->[0],
3506 Type => join(',', (map { $_->Type } @{$batch}) )
3512 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3514 # {{{ sub _OverlayAccessible
3516 sub _OverlayAccessible {
3518 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3519 Queue => { 'read' => 1, 'write' => 1 },
3520 Requestors => { 'read' => 1, 'write' => 1 },
3521 Owner => { 'read' => 1, 'write' => 1 },
3522 Subject => { 'read' => 1, 'write' => 1 },
3523 InitialPriority => { 'read' => 1, 'write' => 1 },
3524 FinalPriority => { 'read' => 1, 'write' => 1 },
3525 Priority => { 'read' => 1, 'write' => 1 },
3526 Status => { 'read' => 1, 'write' => 1 },
3527 TimeEstimated => { 'read' => 1, 'write' => 1 },
3528 TimeWorked => { 'read' => 1, 'write' => 1 },
3529 TimeLeft => { 'read' => 1, 'write' => 1 },
3530 Told => { 'read' => 1, 'write' => 1 },
3531 Resolved => { 'read' => 1 },
3532 Type => { 'read' => 1 },
3533 Starts => { 'read' => 1, 'write' => 1 },
3534 Started => { 'read' => 1, 'write' => 1 },
3535 Due => { 'read' => 1, 'write' => 1 },
3536 Creator => { 'read' => 1, 'auto' => 1 },
3537 Created => { 'read' => 1, 'auto' => 1 },
3538 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3539 LastUpdated => { 'read' => 1, 'auto' => 1 }
3551 my %args = ( Field => undef,
3554 RecordTransaction => 1,
3557 TransactionType => 'Set',
3560 if ($args{'CheckACL'}) {
3561 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3562 return ( 0, $self->loc("Permission Denied"));
3566 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3567 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3568 return(0, $self->loc("Internal Error"));
3571 #if the user is trying to modify the record
3573 #Take care of the old value we really don't want to get in an ACL loop.
3574 # so ask the super::_Value
3575 my $Old = $self->SUPER::_Value("$args{'Field'}");
3578 if ( $args{'UpdateTicket'} ) {
3581 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3582 Value => $args{'Value'} );
3584 #If we can't actually set the field to the value, don't record
3585 # a transaction. instead, get out of here.
3586 return ( 0, $msg ) unless $ret;
3589 if ( $args{'RecordTransaction'} == 1 ) {
3591 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3592 Type => $args{'TransactionType'},
3593 Field => $args{'Field'},
3594 NewValue => $args{'Value'},
3596 TimeTaken => $args{'TimeTaken'},
3598 return ( $Trans, scalar $TransObj->BriefDescription );
3601 return ( $ret, $msg );
3611 Takes the name of a table column.
3612 Returns its value as a string, if the user passes an ACL check
3621 #if the field is public, return it.
3622 if ( $self->_Accessible( $field, 'public' ) ) {
3624 #$RT::Logger->debug("Skipping ACL check for $field\n");
3625 return ( $self->SUPER::_Value($field) );
3629 #If the current user doesn't have ACLs, don't let em at it.
3631 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3634 return ( $self->SUPER::_Value($field) );
3640 # {{{ sub _UpdateTimeTaken
3642 =head2 _UpdateTimeTaken
3644 This routine will increment the timeworked counter. it should
3645 only be called from _NewTransaction
3649 sub _UpdateTimeTaken {
3651 my $Minutes = shift;
3654 $Total = $self->SUPER::_Value("TimeWorked");
3655 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3657 Field => "TimeWorked",
3668 # {{{ Routines dealing with ACCESS CONTROL
3670 # {{{ sub CurrentUserHasRight
3672 =head2 CurrentUserHasRight
3674 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3675 1 if the user has that right. It returns 0 if the user doesn't have that right.
3679 sub CurrentUserHasRight {
3685 Principal => $self->CurrentUser->UserObj(),
3698 Takes a paramhash with the attributes 'Right' and 'Principal'
3699 'Right' is a ticket-scoped textual right from RT::ACE
3700 'Principal' is an RT::User object
3702 Returns 1 if the principal has the right. Returns undef if not.
3714 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3717 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3722 $args{'Principal'}->HasRight(
3724 Right => $args{'Right'}
3735 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3736 It isn't acutally a searchbuilder collection itself.
3743 unless ($self->{'__reminders'}) {
3744 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3745 $self->{'__reminders'}->Ticket($self->id);
3747 return $self->{'__reminders'};
3753 # {{{ sub Transactions
3757 Returns an RT::Transactions object of all transactions on this ticket
3764 my $transactions = RT::Transactions->new( $self->CurrentUser );
3766 #If the user has no rights, return an empty object
3767 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3768 $transactions->LimitToTicket($self->id);
3770 # if the user may not see comments do not return them
3771 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3772 $transactions->Limit(
3777 $transactions->Limit(
3780 VALUE => "CommentEmailRecord",
3781 ENTRYAGGREGATOR => 'AND'
3787 return ($transactions);
3793 # {{{ TransactionCustomFields
3795 =head2 TransactionCustomFields
3797 Returns the custom fields that transactions on tickets will have.
3801 sub TransactionCustomFields {
3803 return $self->QueueObj->TicketTransactionCustomFields;
3808 # {{{ sub CustomFieldValues
3810 =head2 CustomFieldValues
3812 # Do name => id mapping (if needed) before falling back to
3813 # RT::Record's CustomFieldValues
3819 sub CustomFieldValues {
3822 if ( $field and $field !~ /^\d+$/ ) {
3823 my $cf = RT::CustomField->new( $self->CurrentUser );
3824 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3825 unless ( $cf->id ) {
3826 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3828 unless ( $cf->id ) {
3829 # If we didn't find a valid cfid, give up.
3830 return RT::CustomFieldValues->new($self->CurrentUser);
3833 return $self->SUPER::CustomFieldValues($field);
3838 # {{{ sub CustomFieldLookupType
3840 =head2 CustomFieldLookupType
3842 Returns the RT::Ticket lookup type, which can be passed to
3843 RT::CustomField->Create() via the 'LookupType' hash key.
3849 sub CustomFieldLookupType {
3850 "RT::Queue-RT::Ticket";
3857 Jesse Vincent, jesse@bestpractical.com