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 # {{{ Add all the custom fields
721 foreach my $arg ( keys %args ) {
722 next unless ( $arg =~ /^CustomField-(\d+)$/i );
725 my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
727 next unless ( length($value) );
729 # Allow passing in uploaded LargeContent etc by hash reference
730 $self->_AddCustomFieldValue(
731 (UNIVERSAL::isa( $value => 'HASH' )
736 RecordTransaction => 0,
743 if ( $args{'_RecordTransaction'} ) {
745 # {{{ Add a transaction for the create
746 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
748 TimeTaken => $args{'TimeWorked'},
749 MIMEObj => $args{'MIMEObj'}
752 if ( $self->Id && $Trans ) {
754 $TransObj->UpdateCustomFields(ARGSRef => \%args);
756 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
757 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
758 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
761 $RT::Handle->Rollback();
763 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
764 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
765 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
768 $RT::Handle->Commit();
769 return ( $self->Id, $TransObj->Id, $ErrStr );
775 # Not going to record a transaction
776 $RT::Handle->Commit();
777 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
778 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
779 return ( $self->Id, 0, $ErrStr );
790 =head2 UpdateFrom822 $MESSAGE
792 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
793 Returns an um. ask me again when the code exists
798 my $simple_update = <<EOF;
800 AddRequestor: jesse\@example.com
803 my $ticket = RT::Ticket->new($RT::SystemUser);
804 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
805 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
806 $ticket->UpdateFrom822($simple_update);
807 is($ticket->Subject, 'target', "changed the subject");
808 my $jesse = RT::User->new($RT::SystemUser);
809 $jesse->LoadByEmail('jesse@example.com');
810 ok ($jesse->Id, "There's a user for jesse");
811 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
821 my %args = $self->_Parse822HeadersForAttributes($content);
825 Queue => $args{'queue'},
826 Subject => $args{'subject'},
827 Status => $args{'status'},
829 Starts => $args{'starts'},
830 Started => $args{'started'},
831 Resolved => $args{'resolved'},
832 Owner => $args{'owner'},
833 Requestor => $args{'requestor'},
835 AdminCc => $args{'admincc'},
836 TimeWorked => $args{'timeworked'},
837 TimeEstimated => $args{'timeestimated'},
838 TimeLeft => $args{'timeleft'},
839 InitialPriority => $args{'initialpriority'},
840 Priority => $args{'priority'},
841 FinalPriority => $args{'finalpriority'},
842 Type => $args{'type'},
843 DependsOn => $args{'dependson'},
844 DependedOnBy => $args{'dependedonby'},
845 RefersTo => $args{'refersto'},
846 ReferredToBy => $args{'referredtoby'},
847 Members => $args{'members'},
848 MemberOf => $args{'memberof'},
849 MIMEObj => $args{'mimeobj'}
852 foreach my $type qw(Requestor Cc Admincc) {
854 foreach my $action ( 'Add', 'Del', '' ) {
856 my $lctag = lc($action) . lc($type);
857 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
859 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
860 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
865 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
870 # Add custom field entries to %ticketargs.
871 # TODO: allow named custom fields
873 /^customfield-(\d+)$/
874 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
877 # for each ticket we've been told to update, iterate through the set of
878 # rfc822 headers and perform that update to the ticket.
881 # {{{ Set basic fields
895 # Resolve the queue from a name to a numeric id.
896 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
897 my $tempqueue = RT::Queue->new($RT::SystemUser);
898 $tempqueue->Load( $ticketargs{'Queue'} );
899 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
904 foreach my $attribute (@attribs) {
905 my $value = $ticketargs{$attribute};
907 if ( $value ne $self->$attribute() ) {
909 my $method = "Set$attribute";
910 my ( $code, $msg ) = $self->$method($value);
912 push @results, $self->loc($attribute) . ': ' . $msg;
917 # We special case owner changing, so we can use ForceOwnerChange
918 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
919 my $ChownType = "Give";
920 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
922 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
923 push ( @results, $msg );
927 # Deal with setting watchers
930 # Acceptable arguments:
937 foreach my $type qw(Requestor Cc AdminCc) {
939 # If we've been given a number of delresses to del, do it.
940 foreach my $address (@{$ticketargs{'Del'.$type}}) {
941 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
942 push (@results, $msg) ;
945 # If we've been given a number of addresses to add, do it.
946 foreach my $address (@{$ticketargs{'Add'.$type}}) {
947 $RT::Logger->debug("Adding $address as a $type");
948 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
949 push (@results, $msg) ;
960 # {{{ _Parse822HeadersForAttributes Content
962 =head2 _Parse822HeadersForAttributes Content
964 Takes an RFC822 style message and parses its attributes into a hash.
968 sub _Parse822HeadersForAttributes {
973 my @lines = ( split ( /\n/, $content ) );
974 while ( defined( my $line = shift @lines ) ) {
975 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
980 if ( defined( $args{$tag} ) )
981 { #if we're about to get a second value, make it an array
982 $args{$tag} = [ $args{$tag} ];
984 if ( ref( $args{$tag} ) )
985 { #If it's an array, we want to push the value
986 push @{ $args{$tag} }, $value;
988 else { #if there's nothing there, just set the value
989 $args{$tag} = $value;
991 } elsif ($line =~ /^$/) {
993 #TODO: this won't work, since "" isn't of the form "foo:value"
995 while ( defined( my $l = shift @lines ) ) {
996 push @{ $args{'content'} }, $l;
1002 foreach my $date qw(due starts started resolved) {
1003 my $dateobj = RT::Date->new($RT::SystemUser);
1004 if ( $args{$date} =~ /^\d+$/ ) {
1005 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1008 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1010 $args{$date} = $dateobj->ISO;
1012 $args{'mimeobj'} = MIME::Entity->new();
1013 $args{'mimeobj'}->build(
1014 Type => ( $args{'contenttype'} || 'text/plain' ),
1015 Data => ($args{'content'} || '')
1025 =head2 Import PARAMHASH
1028 Doesn\'t create a transaction.
1029 Doesn\'t supply queue defaults, etc.
1037 my ( $ErrStr, $QueueObj, $Owner );
1041 EffectiveId => undef,
1045 Owner => $RT::Nobody->Id,
1046 Subject => '[no subject]',
1047 InitialPriority => undef,
1048 FinalPriority => undef,
1059 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1060 $QueueObj = RT::Queue->new($RT::SystemUser);
1061 $QueueObj->Load( $args{'Queue'} );
1063 #TODO error check this and return 0 if it\'s not loading properly +++
1065 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1066 $QueueObj = RT::Queue->new($RT::SystemUser);
1067 $QueueObj->Load( $args{'Queue'}->Id );
1071 "$self " . $args{'Queue'} . " not a recognised queue object." );
1074 #Can't create a ticket without a queue.
1075 unless ( defined($QueueObj) and $QueueObj->Id ) {
1076 $RT::Logger->debug("$self No queue given for ticket creation.");
1077 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1080 #Now that we have a queue, Check the ACLS
1082 $self->CurrentUser->HasRight(
1083 Right => 'CreateTicket',
1089 $self->loc("No permission to create tickets in the queue '[_1]'"
1090 , $QueueObj->Name));
1093 # {{{ Deal with setting the owner
1095 # Attempt to take user object, user name or user id.
1096 # Assign to nobody if lookup fails.
1097 if ( defined( $args{'Owner'} ) ) {
1098 if ( ref( $args{'Owner'} ) ) {
1099 $Owner = $args{'Owner'};
1102 $Owner = new RT::User( $self->CurrentUser );
1103 $Owner->Load( $args{'Owner'} );
1104 if ( !defined( $Owner->id ) ) {
1105 $Owner->Load( $RT::Nobody->id );
1110 #If we have a proposed owner and they don't have the right
1111 #to own a ticket, scream about it and make them not the owner
1114 and ( $Owner->Id != $RT::Nobody->Id )
1117 Object => $QueueObj,
1118 Right => 'OwnTicket'
1124 $RT::Logger->warning( "$self user "
1125 . $Owner->Name . "("
1128 . "as a ticket owner but has no rights to own "
1130 . $QueueObj->Name . "'\n" );
1135 #If we haven't been handed a valid owner, make it nobody.
1136 unless ( defined($Owner) ) {
1137 $Owner = new RT::User( $self->CurrentUser );
1138 $Owner->Load( $RT::Nobody->UserObj->Id );
1143 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1144 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1147 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1148 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1149 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1150 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1152 # If we're coming in with an id, set that now.
1153 my $EffectiveId = undef;
1154 if ( $args{'id'} ) {
1155 $EffectiveId = $args{'id'};
1159 my $id = $self->SUPER::Create(
1161 EffectiveId => $EffectiveId,
1162 Queue => $QueueObj->Id,
1163 Owner => $Owner->Id,
1164 Subject => $args{'Subject'}, # loc
1165 InitialPriority => $args{'InitialPriority'}, # loc
1166 FinalPriority => $args{'FinalPriority'}, # loc
1167 Priority => $args{'InitialPriority'}, # loc
1168 Status => $args{'Status'}, # loc
1169 TimeWorked => $args{'TimeWorked'}, # loc
1170 Type => $args{'Type'}, # loc
1171 Created => $args{'Created'}, # loc
1172 Told => $args{'Told'}, # loc
1173 LastUpdated => $args{'Updated'}, # loc
1174 Resolved => $args{'Resolved'}, # loc
1175 Due => $args{'Due'}, # loc
1178 # If the ticket didn't have an id
1179 # Set the ticket's effective ID now that we've created it.
1180 if ( $args{'id'} ) {
1181 $self->Load( $args{'id'} );
1185 $self->__Set( Field => 'EffectiveId', Value => $id );
1189 $self . "->Import couldn't set EffectiveId: $msg\n" );
1193 my $create_groups_ret = $self->_CreateTicketGroups();
1194 unless ($create_groups_ret) {
1196 "Couldn't create ticket groups for ticket " . $self->Id );
1199 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1202 foreach $watcher ( @{ $args{'Cc'} } ) {
1203 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1205 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1206 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1209 foreach $watcher ( @{ $args{'Requestor'} } ) {
1210 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1214 return ( $self->Id, $ErrStr );
1219 # {{{ Routines dealing with watchers.
1221 # {{{ _CreateTicketGroups
1223 =head2 _CreateTicketGroups
1225 Create the ticket groups and links for this ticket.
1226 This routine expects to be called from Ticket->Create _inside of a transaction_
1228 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1230 It will return true on success and undef on failure.
1234 my $ticket = RT::Ticket->new($RT::SystemUser);
1235 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1236 Owner => $RT::SystemUser->Id,
1238 Requestor => ['jesse@example.com'],
1241 ok ($id, "Ticket $id was created");
1242 ok(my $group = RT::Group->new($RT::SystemUser));
1243 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1244 ok ($group->Id, "Found the requestors object for this ticket");
1246 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1247 $jesse->LoadByEmail('jesse@example.com');
1248 ok($jesse->Id, "Found the jesse rt user");
1251 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1252 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1253 ok ($add_id, "Add succeeded: ($add_msg)");
1254 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1255 $bob->LoadByEmail('bob@fsck.com');
1256 ok($bob->Id, "Found the bob rt user");
1257 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1258 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1259 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1262 $group = RT::Group->new($RT::SystemUser);
1263 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1264 ok ($group->Id, "Found the cc object for this ticket");
1265 $group = RT::Group->new($RT::SystemUser);
1266 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1267 ok ($group->Id, "Found the AdminCc object for this ticket");
1268 $group = RT::Group->new($RT::SystemUser);
1269 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1270 ok ($group->Id, "Found the Owner object for this ticket");
1271 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1278 sub _CreateTicketGroups {
1281 my @types = qw(Requestor Owner Cc AdminCc);
1283 foreach my $type (@types) {
1284 my $type_obj = RT::Group->new($self->CurrentUser);
1285 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1286 Instance => $self->Id,
1289 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1290 $self->Id.": ".$msg);
1300 # {{{ sub OwnerGroup
1304 A constructor which returns an RT::Group object containing the owner of this ticket.
1310 my $owner_obj = RT::Group->new($self->CurrentUser);
1311 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1312 return ($owner_obj);
1318 # {{{ sub AddWatcher
1322 AddWatcher takes a parameter hash. The keys are as follows:
1324 Type One of Requestor, Cc, AdminCc
1326 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1328 Email The email address of the new watcher. If a user with this
1329 email address can't be found, a new nonprivileged user will be created.
1331 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.
1339 PrincipalId => undef,
1344 # XXX, FIXME, BUG: if only email is provided then we only check
1345 # for ModifyTicket right, but must try to get PrincipalId and
1346 # check Watch* rights too if user exist
1349 #If the watcher we're trying to add is for the current user
1350 if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0)
1351 or lc( $self->CurrentUser->UserObj->EmailAddress )
1352 eq lc( RT::User::CanonicalizeEmailAddress(undef, $args{'Email'}) || '' ) )
1354 # If it's an AdminCc and they don't have
1355 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1356 if ( $args{'Type'} eq 'AdminCc' ) {
1357 unless ( $self->CurrentUserHasRight('ModifyTicket')
1358 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1359 return ( 0, $self->loc('Permission Denied'))
1363 # If it's a Requestor or Cc and they don't have
1364 # 'Watch' or 'ModifyTicket', bail
1365 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1367 unless ( $self->CurrentUserHasRight('ModifyTicket')
1368 or $self->CurrentUserHasRight('Watch') ) {
1369 return ( 0, $self->loc('Permission Denied'))
1373 $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1374 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1378 # If the watcher isn't the current user
1379 # and the current user doesn't have 'ModifyTicket'
1382 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1383 return ( 0, $self->loc("Permission Denied") );
1389 return ( $self->_AddWatcher(%args) );
1392 #This contains the meat of AddWatcher. but can be called from a routine like
1393 # Create, which doesn't need the additional acl check
1399 PrincipalId => undef,
1405 my $principal = RT::Principal->new($self->CurrentUser);
1406 if ($args{'Email'}) {
1407 my $user = RT::User->new($RT::SystemUser);
1408 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1409 # If we can't load the user by email address, let's try to load by username
1411 ($pid,$msg) = $user->Load($args{'Email'})
1414 $args{'PrincipalId'} = $pid;
1417 if ($args{'PrincipalId'}) {
1418 $principal->Load($args{'PrincipalId'});
1422 # If we can't find this watcher, we need to bail.
1423 unless ($principal->Id) {
1424 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1425 return(0, $self->loc("Could not find or create that user"));
1429 my $group = RT::Group->new($self->CurrentUser);
1430 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1431 unless ($group->id) {
1432 return(0,$self->loc("Group not found"));
1435 if ( $group->HasMember( $principal)) {
1437 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1441 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1442 InsideTransaction => 1 );
1444 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1446 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1449 unless ( $args{'Silent'} ) {
1450 $self->_NewTransaction(
1451 Type => 'AddWatcher',
1452 NewValue => $principal->Id,
1453 Field => $args{'Type'}
1457 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1463 # {{{ sub DeleteWatcher
1465 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1468 Deletes a Ticket watcher. Takes two arguments:
1470 Type (one of Requestor,Cc,AdminCc)
1474 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1476 Email (the email address of an existing wathcer)
1485 my %args = ( Type => undef,
1486 PrincipalId => undef,
1490 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1491 return ( 0, $self->loc("No principal specified") );
1493 my $principal = RT::Principal->new( $self->CurrentUser );
1494 if ( $args{'PrincipalId'} ) {
1496 $principal->Load( $args{'PrincipalId'} );
1499 my $user = RT::User->new( $self->CurrentUser );
1500 $user->LoadByEmail( $args{'Email'} );
1501 $principal->Load( $user->Id );
1504 # If we can't find this watcher, we need to bail.
1505 unless ( $principal->Id ) {
1506 return ( 0, $self->loc("Could not find that principal") );
1509 my $group = RT::Group->new( $self->CurrentUser );
1510 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1511 unless ( $group->id ) {
1512 return ( 0, $self->loc("Group not found") );
1516 #If the watcher we're trying to add is for the current user
1517 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1519 # If it's an AdminCc and they don't have
1520 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1521 if ( $args{'Type'} eq 'AdminCc' ) {
1522 unless ( $self->CurrentUserHasRight('ModifyTicket')
1523 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1524 return ( 0, $self->loc('Permission Denied') );
1528 # If it's a Requestor or Cc and they don't have
1529 # 'Watch' or 'ModifyTicket', bail
1530 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1532 unless ( $self->CurrentUserHasRight('ModifyTicket')
1533 or $self->CurrentUserHasRight('Watch') ) {
1534 return ( 0, $self->loc('Permission Denied') );
1538 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1540 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1544 # If the watcher isn't the current user
1545 # and the current user doesn't have 'ModifyTicket' bail
1547 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1548 return ( 0, $self->loc("Permission Denied") );
1554 # see if this user is already a watcher.
1556 unless ( $group->HasMember($principal) ) {
1558 $self->loc( 'That principal is not a [_1] for this ticket',
1562 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1564 $RT::Logger->error( "Failed to delete "
1566 . " as a member of group "
1572 'Could not remove that principal as a [_1] for this ticket',
1576 unless ( $args{'Silent'} ) {
1577 $self->_NewTransaction( Type => 'DelWatcher',
1578 OldValue => $principal->Id,
1579 Field => $args{'Type'} );
1583 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1584 $principal->Object->Name,
1593 =head2 SquelchMailTo [EMAIL]
1595 Takes an optional email address to never email about updates to this ticket.
1598 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1602 my $t = RT::Ticket->new($RT::SystemUser);
1603 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1605 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1607 my @returned = $t->SquelchMailTo('nobody@example.com');
1609 is($#returned, 0, "The ticket has one squelched recipients");
1611 my @names = $t->Attributes->Names;
1612 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1613 @returned = $t->SquelchMailTo('nobody@example.com');
1616 is($#returned, 0, "The ticket has one squelched recipients");
1618 @names = $t->Attributes->Names;
1619 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1622 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1623 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1624 @returned = $t->SquelchMailTo();
1625 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1635 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1639 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1640 unless grep { $_->Content eq $attr }
1641 $self->Attributes->Named('SquelchMailTo');
1644 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1647 my @attributes = $self->Attributes->Named('SquelchMailTo');
1648 return (@attributes);
1652 =head2 UnsquelchMailTo ADDRESS
1654 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1656 Returns a tuple of (status, message)
1660 sub UnsquelchMailTo {
1663 my $address = shift;
1664 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1665 return ( 0, $self->loc("Permission Denied") );
1668 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1669 return ($val, $msg);
1673 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1675 =head2 RequestorAddresses
1677 B<Returns> String: All Ticket Requestor email addresses as a string.
1681 sub RequestorAddresses {
1684 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1688 return ( $self->Requestors->MemberEmailAddressesAsString );
1692 =head2 AdminCcAddresses
1694 returns String: All Ticket AdminCc email addresses as a string
1698 sub AdminCcAddresses {
1701 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1705 return ( $self->AdminCc->MemberEmailAddressesAsString )
1711 returns String: All Ticket Ccs as a string of email addresses
1718 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1722 return ( $self->Cc->MemberEmailAddressesAsString);
1728 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1730 # {{{ sub Requestors
1735 Returns this ticket's Requestors as an RT::Group object
1742 my $group = RT::Group->new($self->CurrentUser);
1743 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1744 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1757 Returns an RT::Group object which contains this ticket's Ccs.
1758 If the user doesn't have "ShowTicket" permission, returns an empty group
1765 my $group = RT::Group->new($self->CurrentUser);
1766 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1767 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1780 Returns an RT::Group object which contains this ticket's AdminCcs.
1781 If the user doesn't have "ShowTicket" permission, returns an empty group
1788 my $group = RT::Group->new($self->CurrentUser);
1789 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1790 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1800 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1803 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1805 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1807 Takes a param hash with the attributes Type and either PrincipalId or Email
1809 Type is one of Requestor, Cc, AdminCc and Owner
1811 PrincipalId is an RT::Principal id, and Email is an email address.
1813 Returns true if the specified principal (or the one corresponding to the
1814 specified address) is a member of the group Type for this ticket.
1816 XX TODO: This should be Memoized.
1823 my %args = ( Type => 'Requestor',
1824 PrincipalId => undef,
1829 # Load the relevant group.
1830 my $group = RT::Group->new($self->CurrentUser);
1831 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1833 # Find the relevant principal.
1834 my $principal = RT::Principal->new($self->CurrentUser);
1835 if (!$args{PrincipalId} && $args{Email}) {
1836 # Look up the specified user.
1837 my $user = RT::User->new($self->CurrentUser);
1838 $user->LoadByEmail($args{Email});
1840 $args{PrincipalId} = $user->PrincipalId;
1843 # A non-existent user can't be a group member.
1847 $principal->Load($args{'PrincipalId'});
1849 # Ask if it has the member in question
1850 return ($group->HasMember($principal));
1855 # {{{ sub IsRequestor
1857 =head2 IsRequestor PRINCIPAL_ID
1859 Takes an RT::Principal id
1860 Returns true if the principal is a requestor of the current ticket.
1869 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1877 =head2 IsCc PRINCIPAL_ID
1879 Takes an RT::Principal id.
1880 Returns true if the principal is a requestor of the current ticket.
1889 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1897 =head2 IsAdminCc PRINCIPAL_ID
1899 Takes an RT::Principal id.
1900 Returns true if the principal is a requestor of the current ticket.
1908 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1918 Takes an RT::User object. Returns true if that user is this ticket's owner.
1919 returns undef otherwise
1927 # no ACL check since this is used in acl decisions
1928 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1932 #Tickets won't yet have owners when they're being created.
1933 unless ( $self->OwnerObj->id ) {
1937 if ( $person->id == $self->OwnerObj->id ) {
1951 # {{{ Routines dealing with queues
1953 # {{{ sub ValidateQueue
1960 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1964 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1965 my $id = $QueueObj->Load($Value);
1981 my $NewQueue = shift;
1983 #Redundant. ACL gets checked in _Set;
1984 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1985 return ( 0, $self->loc("Permission Denied") );
1988 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1989 $NewQueueObj->Load($NewQueue);
1991 unless ( $NewQueueObj->Id() ) {
1992 return ( 0, $self->loc("That queue does not exist") );
1995 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1996 return ( 0, $self->loc('That is the same value') );
1999 $self->CurrentUser->HasRight(
2000 Right => 'CreateTicket',
2001 Object => $NewQueueObj
2005 return ( 0, $self->loc("You may not create requests in that queue.") );
2009 $self->OwnerObj->HasRight(
2010 Right => 'OwnTicket',
2011 Object => $NewQueueObj
2015 my $clone = RT::Ticket->new( $RT::SystemUser );
2016 $clone->Load( $self->Id );
2017 unless ( $clone->Id ) {
2018 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2020 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2021 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2024 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2033 Takes nothing. returns this ticket's queue object
2040 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2042 #We call __Value so that we can avoid the ACL decision and some deep recursion
2043 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2044 return ($queue_obj);
2051 # {{{ Date printing routines
2057 Returns an RT::Date object containing this ticket's due date
2064 my $time = new RT::Date( $self->CurrentUser );
2066 # -1 is RT::Date slang for never
2068 $time->Set( Format => 'sql', Value => $self->Due );
2071 $time->Set( Format => 'unix', Value => -1 );
2079 # {{{ sub DueAsString
2083 Returns this ticket's due date as a human readable string
2089 return $self->DueObj->AsString();
2094 # {{{ sub ResolvedObj
2098 Returns an RT::Date object of this ticket's 'resolved' time.
2105 my $time = new RT::Date( $self->CurrentUser );
2106 $time->Set( Format => 'sql', Value => $self->Resolved );
2112 # {{{ sub SetStarted
2116 Takes a date in ISO format or undef
2117 Returns a transaction id and a message
2118 The client calls "Start" to note that the project was started on the date in $date.
2119 A null date means "now"
2125 my $time = shift || 0;
2127 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2128 return ( 0, $self->loc("Permission Denied") );
2131 #We create a date object to catch date weirdness
2132 my $time_obj = new RT::Date( $self->CurrentUser() );
2134 $time_obj->Set( Format => 'ISO', Value => $time );
2137 $time_obj->SetToNow();
2140 #Now that we're starting, open this ticket
2141 #TODO do we really want to force this as policy? it should be a scrip
2143 #We need $TicketAsSystem, in case the current user doesn't have
2146 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2147 $TicketAsSystem->Load( $self->Id );
2148 if ( $TicketAsSystem->Status eq 'new' ) {
2149 $TicketAsSystem->Open();
2152 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2158 # {{{ sub StartedObj
2162 Returns an RT::Date object which contains this ticket's
2170 my $time = new RT::Date( $self->CurrentUser );
2171 $time->Set( Format => 'sql', Value => $self->Started );
2181 Returns an RT::Date object which contains this ticket's
2189 my $time = new RT::Date( $self->CurrentUser );
2190 $time->Set( Format => 'sql', Value => $self->Starts );
2200 Returns an RT::Date object which contains this ticket's
2208 my $time = new RT::Date( $self->CurrentUser );
2209 $time->Set( Format => 'sql', Value => $self->Told );
2215 # {{{ sub ToldAsString
2219 A convenience method that returns ToldObj->AsString
2221 TODO: This should be deprecated
2227 if ( $self->Told ) {
2228 return $self->ToldObj->AsString();
2237 # {{{ sub TimeWorkedAsString
2239 =head2 TimeWorkedAsString
2241 Returns the amount of time worked on this ticket as a Text String
2245 sub TimeWorkedAsString {
2247 return "0" unless $self->TimeWorked;
2249 #This is not really a date object, but if we diff a number of seconds
2250 #vs the epoch, we'll get a nice description of time worked.
2252 my $worked = new RT::Date( $self->CurrentUser );
2254 #return the #of minutes worked turned into seconds and written as
2255 # a simple text string
2257 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2264 # {{{ Routines dealing with correspondence/comments
2270 Comment on this ticket.
2271 Takes a hashref with the following attributes:
2272 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2275 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2277 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2278 They will, however, be prepared and you'll be able to access them through the TransactionObj
2280 Returns: Transaction id, Error Message, Transaction Object
2281 (note the different order from Create()!)
2288 my %args = ( CcMessageTo => undef,
2289 BccMessageTo => undef,
2296 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2297 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2298 return ( 0, $self->loc("Permission Denied"), undef );
2300 $args{'NoteType'} = 'Comment';
2302 if ($args{'DryRun'}) {
2303 $RT::Handle->BeginTransaction();
2304 $args{'CommitScrips'} = 0;
2307 my @results = $self->_RecordNote(%args);
2308 if ($args{'DryRun'}) {
2309 $RT::Handle->Rollback();
2316 # {{{ sub Correspond
2320 Correspond on this ticket.
2321 Takes a hashref with the following attributes:
2324 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2326 if there's no MIMEObj, Content is used to build a MIME::Entity object
2328 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2329 They will, however, be prepared and you'll be able to access them through the TransactionObj
2331 Returns: Transaction id, Error Message, Transaction Object
2332 (note the different order from Create()!)
2339 my %args = ( CcMessageTo => undef,
2340 BccMessageTo => undef,
2346 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2347 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2348 return ( 0, $self->loc("Permission Denied"), undef );
2351 $args{'NoteType'} = 'Correspond';
2352 if ($args{'DryRun'}) {
2353 $RT::Handle->BeginTransaction();
2354 $args{'CommitScrips'} = 0;
2357 my @results = $self->_RecordNote(%args);
2359 #Set the last told date to now if this isn't mail from the requestor.
2360 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2361 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2363 if ($args{'DryRun'}) {
2364 $RT::Handle->Rollback();
2373 # {{{ sub _RecordNote
2377 the meat of both comment and correspond.
2379 Performs no access control checks. hence, dangerous.
2386 my %args = ( CcMessageTo => undef,
2387 BccMessageTo => undef,
2394 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2395 return ( 0, $self->loc("No message attached"), undef );
2397 unless ( $args{'MIMEObj'} ) {
2398 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2399 ref $args{'Content'}
2401 : [ $args{'Content'} ]
2405 # convert text parts into utf-8
2406 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2408 # If we've been passed in CcMessageTo and BccMessageTo fields,
2409 # add them to the mime object for passing on to the transaction handler
2410 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2413 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2414 undef, $args{'CcMessageTo'}
2416 if defined $args{'CcMessageTo'};
2417 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2418 RT::User::CanonicalizeEmailAddress(
2419 undef, $args{'BccMessageTo'}
2421 if defined $args{'BccMessageTo'};
2423 # If this is from an external source, we need to come up with its
2424 # internal Message-ID now, so all emails sent because of this
2425 # message have a common Message-ID
2426 unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2427 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2429 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2431 . $RT::VERSION . "-"
2433 . CORE::time() . "-"
2434 . int(rand(2000)) . '.'
2437 . "0" . "@" # Email sent
2442 #Record the correspondence (write the transaction)
2443 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2444 Type => $args{'NoteType'},
2445 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2446 TimeTaken => $args{'TimeTaken'},
2447 MIMEObj => $args{'MIMEObj'},
2448 CommitScrips => $args{'CommitScrips'},
2452 $RT::Logger->err("$self couldn't init a transaction $msg");
2453 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2456 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2468 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2471 my $type = shift || "";
2473 unless ( $self->{"$field$type"} ) {
2474 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2475 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2476 # Maybe this ticket is a merged ticket
2477 my $Tickets = new RT::Tickets( $self->CurrentUser );
2478 # at least to myself
2479 $self->{"$field$type"}->Limit( FIELD => $field,
2480 VALUE => $self->URI,
2481 ENTRYAGGREGATOR => 'OR' );
2482 $Tickets->Limit( FIELD => 'EffectiveId',
2483 VALUE => $self->EffectiveId );
2484 while (my $Ticket = $Tickets->Next) {
2485 $self->{"$field$type"}->Limit( FIELD => $field,
2486 VALUE => $Ticket->URI,
2487 ENTRYAGGREGATOR => 'OR' );
2489 $self->{"$field$type"}->Limit( FIELD => 'Type',
2494 return ( $self->{"$field$type"} );
2499 # {{{ sub DeleteLink
2503 Delete a link. takes a paramhash of Base, Target and Type.
2504 Either Base or Target must be null. The null value will
2505 be replaced with this ticket\'s id
2518 unless ( $args{'Target'} || $args{'Base'} ) {
2519 $RT::Logger->error("Base or Target must be specified\n");
2520 return ( 0, $self->loc('Either base or target must be specified') );
2525 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2526 if ( !$right && $RT::StrictLinkACL ) {
2527 return ( 0, $self->loc("Permission Denied") );
2530 # If the other URI is an RT::Ticket, we want to make sure the user
2531 # can modify it too...
2532 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2533 return (0, $msg) unless $status;
2534 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2537 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2538 ( $RT::StrictLinkACL && $right < 2 ) )
2540 return ( 0, $self->loc("Permission Denied") );
2543 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2546 $RT::Logger->debug("Couldn't find that link\n");
2550 my ($direction, $remote_link);
2552 if ( $args{'Base'} ) {
2553 $remote_link = $args{'Base'};
2554 $direction = 'Target';
2556 elsif ( $args{'Target'} ) {
2557 $remote_link = $args{'Target'};
2561 if ( $args{'Silent'} ) {
2562 return ( $val, $Msg );
2565 my $remote_uri = RT::URI->new( $self->CurrentUser );
2566 $remote_uri->FromURI( $remote_link );
2568 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2569 Type => 'DeleteLink',
2570 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2571 OldValue => $remote_uri->URI || $remote_link,
2575 if ( $remote_uri->IsLocal ) {
2577 my $OtherObj = $remote_uri->Object;
2578 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2579 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2580 : $LINKDIRMAP{$args{'Type'}}->{Target},
2581 OldValue => $self->URI,
2582 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2586 return ( $Trans, $Msg );
2596 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2602 my %args = ( Target => '',
2608 unless ( $args{'Target'} || $args{'Base'} ) {
2609 $RT::Logger->error("Base or Target must be specified\n");
2610 return ( 0, $self->loc('Either base or target must be specified') );
2614 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2615 if ( !$right && $RT::StrictLinkACL ) {
2616 return ( 0, $self->loc("Permission Denied") );
2619 # If the other URI is an RT::Ticket, we want to make sure the user
2620 # can modify it too...
2621 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2622 return (0, $msg) unless $status;
2623 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2626 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2627 ( $RT::StrictLinkACL && $right < 2 ) )
2629 return ( 0, $self->loc("Permission Denied") );
2632 return $self->_AddLink(%args);
2635 sub __GetTicketFromURI {
2637 my %args = ( URI => '', @_ );
2639 # If the other URI is an RT::Ticket, we want to make sure the user
2640 # can modify it too...
2641 my $uri_obj = RT::URI->new( $self->CurrentUser );
2642 $uri_obj->FromURI( $args{'URI'} );
2644 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2645 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2646 $RT::Logger->warning( "$msg\n" );
2649 my $obj = $uri_obj->Resolver->Object;
2650 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2651 return (1, 'Found not a ticket', undef);
2653 return (1, 'Found ticket', $obj);
2658 Private non-acled variant of AddLink so that links can be added during create.
2664 my %args = ( Target => '',
2670 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2671 return ($val, $msg) if !$val || $exist;
2673 my ($direction, $remote_link);
2674 if ( $args{'Target'} ) {
2675 $remote_link = $args{'Target'};
2676 $direction = 'Base';
2677 } elsif ( $args{'Base'} ) {
2678 $remote_link = $args{'Base'};
2679 $direction = 'Target';
2682 # Don't write the transaction if we're doing this on create
2683 if ( $args{'Silent'} ) {
2684 return ( $val, $msg );
2687 my $remote_uri = RT::URI->new( $self->CurrentUser );
2688 $remote_uri->FromURI( $remote_link );
2690 #Write the transaction
2691 my ( $Trans, $Msg, $TransObj ) =
2692 $self->_NewTransaction(Type => 'AddLink',
2693 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2694 NewValue => $remote_uri->URI || $remote_link,
2697 if ( $remote_uri->IsLocal ) {
2699 my $OtherObj = $remote_uri->Object;
2700 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2701 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2702 : $LINKDIRMAP{$args{'Type'}}->{Target},
2703 NewValue => $self->URI,
2704 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2707 return ( $val, $Msg );
2719 MergeInto take the id of the ticket to merge this ticket into.
2724 my $t1 = RT::Ticket->new($RT::SystemUser);
2725 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2727 my $t2 = RT::Ticket->new($RT::SystemUser);
2728 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2730 my ($msg, $val) = $t1->MergeInto($t2->id);
2732 $t1 = RT::Ticket->new($RT::SystemUser);
2733 is ($t1->id, undef, "ok. we've got a blank ticket1");
2736 is ($t1->id, $t2->id);
2738 is ($t1->Requestors->MembersObj->Count, 2);
2747 my $ticket_id = shift;
2749 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2750 return ( 0, $self->loc("Permission Denied") );
2753 # Load up the new ticket.
2754 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2755 $MergeInto->Load($ticket_id);
2757 # make sure it exists.
2758 unless ( $MergeInto->Id ) {
2759 return ( 0, $self->loc("New ticket doesn't exist") );
2762 # Make sure the current user can modify the new ticket.
2763 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2764 return ( 0, $self->loc("Permission Denied") );
2767 $RT::Handle->BeginTransaction();
2769 # We use EffectiveId here even though it duplicates information from
2770 # the links table becasue of the massive performance hit we'd take
2771 # by trying to do a separate database query for merge info everytime
2774 #update this ticket's effective id to the new ticket's id.
2775 my ( $id_val, $id_msg ) = $self->__Set(
2776 Field => 'EffectiveId',
2777 Value => $MergeInto->Id()
2781 $RT::Handle->Rollback();
2782 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2786 if ( $self->__Value('Status') ne 'resolved' ) {
2788 my ( $status_val, $status_msg )
2789 = $self->__Set( Field => 'Status', Value => 'resolved' );
2791 unless ($status_val) {
2792 $RT::Handle->Rollback();
2795 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2799 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2803 # update all the links that point to that old ticket
2804 my $old_links_to = RT::Links->new($self->CurrentUser);
2805 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2808 while (my $link = $old_links_to->Next) {
2809 if (exists $old_seen{$link->Base."-".$link->Type}) {
2812 elsif ($link->Base eq $MergeInto->URI) {
2815 # First, make sure the link doesn't already exist. then move it over.
2816 my $tmp = RT::Link->new($RT::SystemUser);
2817 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2821 $link->SetTarget($MergeInto->URI);
2822 $link->SetLocalTarget($MergeInto->id);
2824 $old_seen{$link->Base."-".$link->Type} =1;
2829 my $old_links_from = RT::Links->new($self->CurrentUser);
2830 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2832 while (my $link = $old_links_from->Next) {
2833 if (exists $old_seen{$link->Type."-".$link->Target}) {
2836 if ($link->Target eq $MergeInto->URI) {
2839 # First, make sure the link doesn't already exist. then move it over.
2840 my $tmp = RT::Link->new($RT::SystemUser);
2841 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2845 $link->SetBase($MergeInto->URI);
2846 $link->SetLocalBase($MergeInto->id);
2847 $old_seen{$link->Type."-".$link->Target} =1;
2853 # Update time fields
2854 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2856 my $mutator = "Set$type";
2857 $MergeInto->$mutator(
2858 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2861 #add all of this ticket's watchers to that ticket.
2862 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2864 my $people = $self->$watcher_type->MembersObj;
2865 my $addwatcher_type = $watcher_type;
2866 $addwatcher_type =~ s/s$//;
2868 while ( my $watcher = $people->Next ) {
2870 my ($val, $msg) = $MergeInto->_AddWatcher(
2871 Type => $addwatcher_type,
2873 PrincipalId => $watcher->MemberId
2876 $RT::Logger->warning($msg);
2882 #find all of the tickets that were merged into this ticket.
2883 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2884 $old_mergees->Limit(
2885 FIELD => 'EffectiveId',
2890 # update their EffectiveId fields to the new ticket's id
2891 while ( my $ticket = $old_mergees->Next() ) {
2892 my ( $val, $msg ) = $ticket->__Set(
2893 Field => 'EffectiveId',
2894 Value => $MergeInto->Id()
2898 #make a new link: this ticket is merged into that other ticket.
2899 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2901 $MergeInto->_SetLastUpdated;
2903 $RT::Handle->Commit();
2904 return ( 1, $self->loc("Merge Successful") );
2911 # {{{ Routines dealing with ownership
2917 Takes nothing and returns an RT::User object of
2925 #If this gets ACLed, we lose on a rights check in User.pm and
2926 #get deep recursion. if we need ACLs here, we need
2927 #an equiv without ACLs
2929 my $owner = new RT::User( $self->CurrentUser );
2930 $owner->Load( $self->__Value('Owner') );
2932 #Return the owner object
2938 # {{{ sub OwnerAsString
2940 =head2 OwnerAsString
2942 Returns the owner's email address
2948 return ( $self->OwnerObj->EmailAddress );
2958 Takes two arguments:
2959 the Id or Name of the owner
2960 and (optionally) the type of the SetOwner Transaction. It defaults
2961 to 'Give'. 'Steal' is also a valid option.
2965 my $root = RT::User->new($RT::SystemUser);
2966 $root->Load('root');
2967 ok ($root->Id, "Loaded the root user");
2968 my $t = RT::Ticket->new($RT::SystemUser);
2970 $t->SetOwner('root');
2971 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2973 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2974 my $txns = RT::Transactions->new($RT::SystemUser);
2975 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2976 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2977 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2978 $txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
2980 my $steal = $txns->First;
2981 ok($steal->OldValue == $root->Id , "Stolen from root");
2982 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2990 my $NewOwner = shift;
2991 my $Type = shift || "Give";
2993 $RT::Handle->BeginTransaction();
2995 $self->_SetLastUpdated(); # lock the ticket
2996 $self->Load( $self->id ); # in case $self changed while waiting for lock
2998 my $OldOwnerObj = $self->OwnerObj;
3000 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3001 $NewOwnerObj->Load( $NewOwner );
3002 unless ( $NewOwnerObj->Id ) {
3003 $RT::Handle->Rollback();
3004 return ( 0, $self->loc("That user does not exist") );
3008 # must have ModifyTicket rights
3009 # or TakeTicket/StealTicket and $NewOwner is self
3010 # see if it's a take
3011 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3012 unless ( $self->CurrentUserHasRight('ModifyTicket')
3013 || $self->CurrentUserHasRight('TakeTicket') ) {
3014 $RT::Handle->Rollback();
3015 return ( 0, $self->loc("Permission Denied") );
3019 # see if it's a steal
3020 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
3021 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3023 unless ( $self->CurrentUserHasRight('ModifyTicket')
3024 || $self->CurrentUserHasRight('StealTicket') ) {
3025 $RT::Handle->Rollback();
3026 return ( 0, $self->loc("Permission Denied") );
3030 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3031 $RT::Handle->Rollback();
3032 return ( 0, $self->loc("Permission Denied") );
3036 # If we're not stealing and the ticket has an owner and it's not
3038 if ( $Type ne 'Steal' and $Type ne 'Force'
3039 and $OldOwnerObj->Id != $RT::Nobody->Id
3040 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3042 $RT::Handle->Rollback();
3043 return ( 0, $self->loc("You can only take tickets that are unowned") )
3044 if $NewOwnerObj->id == $self->CurrentUser->id;
3047 $self->loc("You can only reassign tickets that you own or that are unowned" )
3051 #If we've specified a new owner and that user can't modify the ticket
3052 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3053 $RT::Handle->Rollback();
3054 return ( 0, $self->loc("That user may not own tickets in that queue") );
3057 # If the ticket has an owner and it's the new owner, we don't need
3059 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3060 $RT::Handle->Rollback();
3061 return ( 0, $self->loc("That user already owns that ticket") );
3064 # Delete the owner in the owner group, then add a new one
3065 # TODO: is this safe? it's not how we really want the API to work
3066 # for most things, but it's fast.
3067 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3069 $RT::Handle->Rollback();
3070 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3073 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3074 PrincipalId => $NewOwnerObj->PrincipalId,
3075 InsideTransaction => 1 );
3077 $RT::Handle->Rollback();
3078 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3081 # We call set twice with slightly different arguments, so
3082 # as to not have an SQL transaction span two RT transactions
3084 my ( $val, $msg ) = $self->_Set(
3086 RecordTransaction => 0,
3087 Value => $NewOwnerObj->Id,
3089 TransactionType => $Type,
3090 CheckACL => 0, # don't check acl
3094 $RT::Handle->Rollback;
3095 return ( 0, $self->loc("Could not change owner. ") . $msg );
3098 ($val, $msg) = $self->_NewTransaction(
3101 NewValue => $NewOwnerObj->Id,
3102 OldValue => $OldOwnerObj->Id,
3107 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3108 $OldOwnerObj->Name, $NewOwnerObj->Name );
3111 $RT::Handle->Rollback();
3115 $RT::Handle->Commit();
3117 return ( $val, $msg );
3126 A convenince method to set the ticket's owner to the current user
3132 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3141 Convenience method to set the owner to 'nobody' if the current user is the owner.
3147 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3156 A convenience method to change the owner of the current ticket to the
3157 current user. Even if it's owned by another user.
3164 if ( $self->IsOwner( $self->CurrentUser ) ) {
3165 return ( 0, $self->loc("You already own this ticket") );
3168 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3178 # {{{ Routines dealing with status
3180 # {{{ sub ValidateStatus
3182 =head2 ValidateStatus STATUS
3184 Takes a string. Returns true if that status is a valid status for this ticket.
3185 Returns false otherwise.
3189 sub ValidateStatus {
3193 #Make sure the status passed in is valid
3194 unless ( $self->QueueObj->IsValidStatus($status) ) {
3206 =head2 SetStatus STATUS
3208 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3210 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.
3214 my $tt = RT::Ticket->new($RT::SystemUser);
3215 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3218 is($tt->Status, 'new', "New ticket is created as new");
3220 ($id, $msg) = $tt->SetStatus('open');
3222 like($msg, qr/open/i, "Status message is correct");
3223 ($id, $msg) = $tt->SetStatus('resolved');
3225 like($msg, qr/resolved/i, "Status message is correct");
3226 ($id, $msg) = $tt->SetStatus('resolved');
3240 $args{Status} = shift;
3247 if ( $args{Status} eq 'deleted') {
3248 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3249 return ( 0, $self->loc('Permission Denied') );
3252 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3253 return ( 0, $self->loc('Permission Denied') );
3257 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3258 return (0, $self->loc('That ticket has unresolved dependencies'));
3261 my $now = RT::Date->new( $self->CurrentUser );
3264 #If we're changing the status from new, record that we've started
3265 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3267 #Set the Started time to "now"
3268 $self->_Set( Field => 'Started',
3270 RecordTransaction => 0 );
3273 #When we close a ticket, set the 'Resolved' attribute to now.
3274 # It's misnamed, but that's just historical.
3275 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3276 $self->_Set( Field => 'Resolved',
3278 RecordTransaction => 0 );
3281 #Actually update the status
3282 my ($val, $msg)= $self->_Set( Field => 'Status',
3283 Value => $args{Status},
3286 TransactionType => 'Status' );
3297 Takes no arguments. Marks this ticket for garbage collection
3303 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3304 return $self->Delete;
3309 return ( $self->SetStatus('deleted') );
3311 # TODO: garbage collection
3320 Sets this ticket's status to stalled
3326 return ( $self->SetStatus('stalled') );
3335 Sets this ticket's status to rejected
3341 return ( $self->SetStatus('rejected') );
3350 Sets this ticket\'s status to Open
3356 return ( $self->SetStatus('open') );
3365 Sets this ticket\'s status to Resolved
3371 return ( $self->SetStatus('resolved') );
3379 # {{{ Actions + Routines dealing with transactions
3381 # {{{ sub SetTold and _SetTold
3383 =head2 SetTold ISO [TIMETAKEN]
3385 Updates the told and records a transaction
3392 $told = shift if (@_);
3393 my $timetaken = shift || 0;
3395 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3396 return ( 0, $self->loc("Permission Denied") );
3399 my $datetold = new RT::Date( $self->CurrentUser );
3401 $datetold->Set( Format => 'iso',
3405 $datetold->SetToNow();
3408 return ( $self->_Set( Field => 'Told',
3409 Value => $datetold->ISO,
3410 TimeTaken => $timetaken,
3411 TransactionType => 'Told' ) );
3416 Updates the told without a transaction or acl check. Useful when we're sending replies.
3423 my $now = new RT::Date( $self->CurrentUser );
3426 #use __Set to get no ACLs ;)
3427 return ( $self->__Set( Field => 'Told',
3428 Value => $now->ISO ) );
3433 =head2 TransactionBatch
3435 Returns an array reference of all transactions created on this ticket during
3436 this ticket object's lifetime, or undef if there were none.
3438 Only works when the $RT::UseTransactionBatch config variable is set to true.
3442 sub TransactionBatch {
3444 return $self->{_TransactionBatch};
3450 # DESTROY methods need to localize $@, or it may unset it. This
3451 # causes $m->abort to not bubble all of the way up. See perlbug
3452 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3455 # The following line eliminates reentrancy.
3456 # It protects against the fact that perl doesn't deal gracefully
3457 # when an object's refcount is changed in its destructor.
3458 return if $self->{_Destroyed}++;
3460 my $batch = $self->TransactionBatch or return;
3461 return unless @$batch;
3464 RT::Scrips->new($RT::SystemUser)->Apply(
3465 Stage => 'TransactionBatch',
3467 TransactionObj => $batch->[0],
3468 Type => join(',', (map { $_->Type } @{$batch}) )
3474 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3476 # {{{ sub _OverlayAccessible
3478 sub _OverlayAccessible {
3480 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3481 Queue => { 'read' => 1, 'write' => 1 },
3482 Requestors => { 'read' => 1, 'write' => 1 },
3483 Owner => { 'read' => 1, 'write' => 1 },
3484 Subject => { 'read' => 1, 'write' => 1 },
3485 InitialPriority => { 'read' => 1, 'write' => 1 },
3486 FinalPriority => { 'read' => 1, 'write' => 1 },
3487 Priority => { 'read' => 1, 'write' => 1 },
3488 Status => { 'read' => 1, 'write' => 1 },
3489 TimeEstimated => { 'read' => 1, 'write' => 1 },
3490 TimeWorked => { 'read' => 1, 'write' => 1 },
3491 TimeLeft => { 'read' => 1, 'write' => 1 },
3492 Told => { 'read' => 1, 'write' => 1 },
3493 Resolved => { 'read' => 1 },
3494 Type => { 'read' => 1 },
3495 Starts => { 'read' => 1, 'write' => 1 },
3496 Started => { 'read' => 1, 'write' => 1 },
3497 Due => { 'read' => 1, 'write' => 1 },
3498 Creator => { 'read' => 1, 'auto' => 1 },
3499 Created => { 'read' => 1, 'auto' => 1 },
3500 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3501 LastUpdated => { 'read' => 1, 'auto' => 1 }
3513 my %args = ( Field => undef,
3516 RecordTransaction => 1,
3519 TransactionType => 'Set',
3522 if ($args{'CheckACL'}) {
3523 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3524 return ( 0, $self->loc("Permission Denied"));
3528 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3529 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3530 return(0, $self->loc("Internal Error"));
3533 #if the user is trying to modify the record
3535 #Take care of the old value we really don't want to get in an ACL loop.
3536 # so ask the super::_Value
3537 my $Old = $self->SUPER::_Value("$args{'Field'}");
3540 if ( $args{'UpdateTicket'} ) {
3543 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3544 Value => $args{'Value'} );
3546 #If we can't actually set the field to the value, don't record
3547 # a transaction. instead, get out of here.
3548 return ( 0, $msg ) unless $ret;
3551 if ( $args{'RecordTransaction'} == 1 ) {
3553 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3554 Type => $args{'TransactionType'},
3555 Field => $args{'Field'},
3556 NewValue => $args{'Value'},
3558 TimeTaken => $args{'TimeTaken'},
3560 return ( $Trans, scalar $TransObj->BriefDescription );
3563 return ( $ret, $msg );
3573 Takes the name of a table column.
3574 Returns its value as a string, if the user passes an ACL check
3583 #if the field is public, return it.
3584 if ( $self->_Accessible( $field, 'public' ) ) {
3586 #$RT::Logger->debug("Skipping ACL check for $field\n");
3587 return ( $self->SUPER::_Value($field) );
3591 #If the current user doesn't have ACLs, don't let em at it.
3593 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3596 return ( $self->SUPER::_Value($field) );
3602 # {{{ sub _UpdateTimeTaken
3604 =head2 _UpdateTimeTaken
3606 This routine will increment the timeworked counter. it should
3607 only be called from _NewTransaction
3611 sub _UpdateTimeTaken {
3613 my $Minutes = shift;
3616 $Total = $self->SUPER::_Value("TimeWorked");
3617 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3619 Field => "TimeWorked",
3630 # {{{ Routines dealing with ACCESS CONTROL
3632 # {{{ sub CurrentUserHasRight
3634 =head2 CurrentUserHasRight
3636 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3637 1 if the user has that right. It returns 0 if the user doesn't have that right.
3641 sub CurrentUserHasRight {
3647 Principal => $self->CurrentUser->UserObj(),
3660 Takes a paramhash with the attributes 'Right' and 'Principal'
3661 'Right' is a ticket-scoped textual right from RT::ACE
3662 'Principal' is an RT::User object
3664 Returns 1 if the principal has the right. Returns undef if not.
3676 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3679 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3684 $args{'Principal'}->HasRight(
3686 Right => $args{'Right'}
3697 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3698 It isn't acutally a searchbuilder collection itself.
3705 unless ($self->{'__reminders'}) {
3706 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3707 $self->{'__reminders'}->Ticket($self->id);
3709 return $self->{'__reminders'};
3715 # {{{ sub Transactions
3719 Returns an RT::Transactions object of all transactions on this ticket
3726 my $transactions = RT::Transactions->new( $self->CurrentUser );
3728 #If the user has no rights, return an empty object
3729 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3730 $transactions->LimitToTicket($self->id);
3732 # if the user may not see comments do not return them
3733 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3734 $transactions->Limit(
3739 $transactions->Limit(
3742 VALUE => "CommentEmailRecord",
3743 ENTRYAGGREGATOR => 'AND'
3749 return ($transactions);
3755 # {{{ TransactionCustomFields
3757 =head2 TransactionCustomFields
3759 Returns the custom fields that transactions on tickets will have.
3763 sub TransactionCustomFields {
3765 return $self->QueueObj->TicketTransactionCustomFields;
3770 # {{{ sub CustomFieldValues
3772 =head2 CustomFieldValues
3774 # Do name => id mapping (if needed) before falling back to
3775 # RT::Record's CustomFieldValues
3781 sub CustomFieldValues {
3784 if ( $field and $field !~ /^\d+$/ ) {
3785 my $cf = RT::CustomField->new( $self->CurrentUser );
3786 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3787 unless ( $cf->id ) {
3788 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3790 unless ( $cf->id ) {
3791 # If we didn't find a valid cfid, give up.
3792 return RT::CustomFieldValues->new($self->CurrentUser);
3795 return $self->SUPER::CustomFieldValues($field);
3800 # {{{ sub CustomFieldLookupType
3802 =head2 CustomFieldLookupType
3804 Returns the RT::Ticket lookup type, which can be passed to
3805 RT::CustomField->Create() via the 'LookupType' hash key.
3811 sub CustomFieldLookupType {
3812 "RT::Queue-RT::Ticket";
3819 Jesse Vincent, jesse@bestpractical.com