1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
53 my $ticket = new RT::Ticket($CurrentUser);
54 $ticket->Load($ticket_id);
58 This module lets you manipulate RT\'s ticket object.
66 ok(my $testqueue = RT::Queue->new($RT::SystemUser));
67 ok($testqueue->Create( Name => 'ticket tests'));
68 ok($testqueue->Id != 0);
69 use_ok(RT::CustomField);
70 ok(my $testcf = RT::CustomField->new($RT::SystemUser));
71 my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti',
72 Queue => $testqueue->id,
73 Type => 'SelectMultiple');
74 ok($ret,"Created the custom field - ".$cmsg);
75 ($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1',
77 Description => 'A testing value');
79 ok($ret, "Added a value - ".$cmsg);
81 ok($testcf->AddValue ( Name => 'Value2',
83 Description => 'Another testing value'));
84 ok($testcf->AddValue ( Name => 'Value3',
86 Description => 'Yet Another testing value'));
88 ok($testcf->Values->Count == 3);
92 my $u = RT::User->new($RT::SystemUser);
94 ok ($u->Id, "Found the root user");
95 ok(my $t = RT::Ticket->new($RT::SystemUser));
96 ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
101 ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
102 ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
104 ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
105 ok($t->CustomFieldValues($testcf->Id)->Count == 1);
106 ok($t->CustomFieldValues($testcf->Id)->First &&
107 $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
109 ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
111 ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
112 ok($t->CustomFieldValues($testcf->Id)->Count == 0);
114 ok(my $t2 = RT::Ticket->new($RT::SystemUser));
116 is($t2->Subject, 'Testing');
117 is($t2->QueueObj->Id, $testqueue->id);
118 ok($t2->OwnerObj->Id == $u->Id);
120 my $t3 = RT::Ticket->new($RT::SystemUser);
121 my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
122 Subject => 'Testing',
124 my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
126 ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
127 my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
129 ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
130 my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
132 ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
133 ok($t->CustomFieldValues($testcf->Id)->Count == 2,
134 "This ticket has 2 custom field values");
135 ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
136 "This ticket has 1 custom field value");
146 no warnings qw(redefine);
153 use RT::CustomFields;
155 use RT::Transactions;
157 use RT::URI::fsck_com_rt;
164 ok(require RT::Ticket, "Loading the RT::Ticket library");
173 # A helper table for links mapping to make it easier
174 # to build and parse links between tickets
176 use vars '%LINKTYPEMAP';
179 MemberOf => { Type => 'MemberOf',
181 Parents => { Type => 'MemberOf',
183 Members => { Type => 'MemberOf',
185 Children => { Type => 'MemberOf',
187 HasMember => { Type => 'MemberOf',
189 RefersTo => { Type => 'RefersTo',
191 ReferredToBy => { Type => 'RefersTo',
193 DependsOn => { Type => 'DependsOn',
195 DependedOnBy => { Type => 'DependsOn',
197 MergedInto => { Type => 'MergedInto',
205 # A helper table for links mapping to make it easier
206 # to build and parse links between tickets
208 use vars '%LINKDIRMAP';
211 MemberOf => { Base => 'MemberOf',
212 Target => 'HasMember', },
213 RefersTo => { Base => 'RefersTo',
214 Target => 'ReferredToBy', },
215 DependsOn => { Base => 'DependsOn',
216 Target => 'DependedOnBy', },
217 MergedInto => { Base => 'MergedInto',
218 Target => 'MergedInto', },
224 sub LINKTYPEMAP { return \%LINKTYPEMAP }
225 sub LINKDIRMAP { return \%LINKDIRMAP }
231 Takes a single argument. This can be a ticket id, ticket alias or
232 local ticket uri. If the ticket can't be loaded, returns undef.
233 Otherwise, returns the ticket id.
241 #TODO modify this routine to look at EffectiveId and do the recursive load
242 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
245 #If it's a local URI, turn it into a ticket id
246 if ( $RT::TicketBaseURI && $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
250 #If it's a remote URI, we're going to punt for now
251 elsif ( $id =~ '://' ) {
255 #If we have an integer URI, load the ticket
256 if ( $id =~ /^\d+$/ ) {
257 my ($ticketid,$msg) = $self->LoadById($id);
260 $RT::Logger->crit("$self tried to load a bogus ticket: $id\n");
265 #It's not a URI. It's not a numerical ticket ID. Punt!
267 $RT::Logger->warning("Tried to load a bogus ticket id: '$id'");
271 #If we're merged, resolve the merge.
272 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
273 $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
274 return ( $self->Load( $self->EffectiveId ) );
277 #Ok. we're loaded. lets get outa here.
278 return ( $self->Id );
288 Given a local ticket URI, loads the specified ticket.
296 if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
298 return ( $self->Load($id) );
311 Arguments: ARGS is a hash of named parameters. Valid parameters are:
314 Queue - Either a Queue object or a Queue Name
315 Requestor - A reference to a list of email addresses or RT user Names
316 Cc - A reference to a list of email addresses or Names
317 AdminCc - A reference to a list of email addresses or Names
318 Type -- The ticket\'s type. ignore this for now
319 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
320 Subject -- A string describing the subject of the ticket
321 Priority -- an integer from 0 to 99
322 InitialPriority -- an integer from 0 to 99
323 FinalPriority -- an integer from 0 to 99
324 Status -- any valid status (Defined in RT::Queue)
325 TimeEstimated -- an integer. estimated time for this task in minutes
326 TimeWorked -- an integer. time worked so far in minutes
327 TimeLeft -- an integer. time remaining in minutes
328 Starts -- an ISO date describing the ticket\'s start date and time in GMT
329 Due -- an ISO date describing the ticket\'s due date and time in GMT
330 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
331 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
333 Ticket links can be set up during create by passing the link type as a hask key and
334 the ticket id to be linked to as a value (or a URI when linking to other objects).
335 Multiple links of the same type can be created by passing an array ref. For example:
338 DependsOn => [ 15, 22 ],
339 RefersTo => 'http://www.bestpractical.com',
341 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
342 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
343 C<Members> and C<Children> are aliases for C<HasMember>.
345 Returns: TICKETID, Transaction Object, Error Message
349 my $t = RT::Ticket->new($RT::SystemUser);
351 ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created");
353 ok ( my $id = $t->Id, "Got ticket id");
354 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
355 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
356 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
367 EffectiveId => undef,
375 InitialPriority => undef,
376 FinalPriority => undef,
387 _RecordTransaction => 1,
391 my ( $ErrStr, $Owner, $resolved );
392 my (@non_fatal_errors);
394 my $QueueObj = RT::Queue->new($RT::SystemUser);
396 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
397 $QueueObj->Load( $args{'Queue'} );
399 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
400 $QueueObj->Load( $args{'Queue'}->Id );
403 $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object." );
406 #Can't create a ticket without a queue.
407 unless ( defined($QueueObj) && $QueueObj->Id ) {
408 $RT::Logger->debug("$self No queue given for ticket creation.");
409 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
412 #Now that we have a queue, Check the ACLS
414 $self->CurrentUser->HasRight(
415 Right => 'CreateTicket',
422 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
425 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
426 return ( 0, 0, $self->loc('Invalid value for status') );
429 #Since we have a queue, we can set queue defaults
432 # If there's no queue default initial priority and it's not set, set it to 0
433 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
434 unless ( $args{'InitialPriority'} );
438 # If there's no queue default final priority and it's not set, set it to 0
439 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
440 unless ( $args{'FinalPriority'} );
442 # Priority may have changed from InitialPriority, for the case
443 # where we're importing tickets (eg, from an older RT version.)
444 my $priority = $args{'Priority'} || $args{'InitialPriority'};
447 #TODO we should see what sort of due date we're getting, rather +
448 # than assuming it's in ISO format.
450 #Set the due date. if we didn't get fed one, use the queue default due in
451 my $Due = new RT::Date( $self->CurrentUser );
453 if ( $args{'Due'} ) {
454 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
456 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
458 $Due->AddDays( $due_in );
461 my $Starts = new RT::Date( $self->CurrentUser );
462 if ( defined $args{'Starts'} ) {
463 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
466 my $Started = new RT::Date( $self->CurrentUser );
467 if ( defined $args{'Started'} ) {
468 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
471 my $Resolved = new RT::Date( $self->CurrentUser );
472 if ( defined $args{'Resolved'} ) {
473 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
476 #If the status is an inactive status, set the resolved date
477 if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} )
479 $RT::Logger->debug( "Got a ". $args{'Status'}
480 ." ticket with undefined resolved date. Setting to now."
487 # {{{ Dealing with time fields
489 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
490 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
491 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
495 # {{{ Deal with setting the owner
497 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
498 $Owner = $args{'Owner'};
501 #If we've been handed something else, try to load the user.
502 elsif ( $args{'Owner'} ) {
503 $Owner = RT::User->new( $self->CurrentUser );
504 $Owner->Load( $args{'Owner'} );
506 push( @non_fatal_errors,
507 $self->loc("Owner could not be set.") . " "
508 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} )
510 unless ( $Owner->Id );
513 #If we have a proposed owner and they don't have the right
514 #to own a ticket, scream about it and make them not the owner
518 and ( $Owner->Id != $RT::Nobody->Id )
528 $RT::Logger->warning( "User "
532 . "as a ticket owner but has no rights to own "
536 push @non_fatal_errors,
537 $self->loc( "Owner '[_1]' does not have rights to own this ticket.",
544 #If we haven't been handed a valid owner, make it nobody.
545 unless ( defined($Owner) && $Owner->Id ) {
546 $Owner = new RT::User( $self->CurrentUser );
547 $Owner->Load( $RT::Nobody->Id );
552 # We attempt to load or create each of the people who might have a role for this ticket
553 # _outside_ the transaction, so we don't get into ticket creation races
554 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
555 next unless ( defined $args{$type} );
556 foreach my $watcher (
557 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
559 my $user = RT::User->new($RT::SystemUser);
560 $user->LoadOrCreateByEmail($watcher)
561 if ( $watcher && $watcher !~ /^\d+$/ );
565 $RT::Handle->BeginTransaction();
568 Queue => $QueueObj->Id,
570 Subject => $args{'Subject'},
571 InitialPriority => $args{'InitialPriority'},
572 FinalPriority => $args{'FinalPriority'},
573 Priority => $priority,
574 Status => $args{'Status'},
575 TimeWorked => $args{'TimeWorked'},
576 TimeEstimated => $args{'TimeEstimated'},
577 TimeLeft => $args{'TimeLeft'},
578 Type => $args{'Type'},
579 Starts => $Starts->ISO,
580 Started => $Started->ISO,
581 Resolved => $Resolved->ISO,
585 # Parameters passed in during an import that we probably don't want to touch, otherwise
586 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
587 $params{$attr} = $args{$attr} if ( $args{$attr} );
590 # Delete null integer parameters
592 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
593 delete $params{$attr}
594 unless ( exists $params{$attr} && $params{$attr} );
597 # Delete the time worked if we're counting it in the transaction
598 delete $params{TimeWorked} if $args{'_RecordTransaction'};
600 my ($id,$ticket_message) = $self->SUPER::Create( %params);
602 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
603 $RT::Handle->Rollback();
605 $self->loc("Ticket could not be created due to an internal error")
609 #Set the ticket's effective ID now that we've created it.
610 my ( $val, $msg ) = $self->__Set(
611 Field => 'EffectiveId',
612 Value => ( $args{'EffectiveId'} || $id )
616 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
617 $RT::Handle->Rollback();
619 $self->loc("Ticket could not be created due to an internal error")
623 my $create_groups_ret = $self->_CreateTicketGroups();
624 unless ($create_groups_ret) {
625 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
627 . ". aborting Ticket creation." );
628 $RT::Handle->Rollback();
630 $self->loc("Ticket could not be created due to an internal error")
634 # Set the owner in the Groups table
635 # We denormalize it into the Ticket table too because doing otherwise would
636 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
638 $self->OwnerGroup->_AddMember(
639 PrincipalId => $Owner->PrincipalId,
640 InsideTransaction => 1
643 # {{{ Deal with setting up watchers
645 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
646 next unless ( defined $args{$type} );
647 foreach my $watcher (
648 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
651 # If there is an empty entry in the list, let's get out of here.
652 next unless $watcher;
654 # we reason that all-digits number must be a principal id, not email
655 # this is the only way to can add
657 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
661 if ( $type eq 'AdminCc' ) {
663 # Note that we're using AddWatcher, rather than _AddWatcher, as we
664 # actually _want_ that ACL check. Otherwise, random ticket creators
665 # could make themselves adminccs and maybe get ticket rights. that would
667 ( $wval, $wmsg ) = $self->AddWatcher(
674 ( $wval, $wmsg ) = $self->_AddWatcher(
681 push @non_fatal_errors, $wmsg unless ($wval);
686 # {{{ Deal with setting up links
688 foreach my $type ( keys %LINKTYPEMAP ) {
689 next unless ( defined $args{$type} );
691 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
693 # Check rights on the other end of the link if we must
694 # then run _AddLink that doesn't check for ACLs
695 if ( $RT::StrictLinkACL ) {
696 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
698 push @non_fatal_errors, $msg;
701 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
702 push @non_fatal_errors, $self->loc('Linking. Permission denied');
707 my ( $wval, $wmsg ) = $self->_AddLink(
708 Type => $LINKTYPEMAP{$type}->{'Type'},
709 $LINKTYPEMAP{$type}->{'Mode'} => $link,
713 push @non_fatal_errors, $wmsg unless ($wval);
719 # {{{ 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 return ( 0, "No principal specified" )
1345 unless $args{'Email'} or $args{'PrincipalId'};
1347 if ( !$args{'PrincipalId'} and $args{'Email'} ) {
1348 my $user = RT::User->new( $self->CurrentUser );
1349 $user->LoadByEmail( $args{'Email'} );
1351 $args{'PrincipalId'} = $user->PrincipalId;
1352 delete $args{'Email'};
1357 # ModifyTicket allow you to add any watcher
1358 return $self->_AddWatcher(%args)
1359 if $self->CurrentUserHasRight('ModifyTicket');
1361 #If the watcher we're trying to add is for the current user
1362 if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0) ) {
1363 # If it's an AdminCc and they have 'WatchAsAdminCc'
1364 if ( $args{'Type'} eq 'AdminCc' ) {
1365 return $self->_AddWatcher( %args )
1366 if $self->CurrentUserHasRight('WatchAsAdminCc');
1369 # If it's a Requestor or Cc and they have 'Watch'
1370 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1371 return $self->_AddWatcher( %args )
1372 if $self->CurrentUserHasRight('Watch');
1375 $RT::Logger->warning( "AddWatcher got passed a bogus type" );
1376 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1380 return ( 0, $self->loc("Permission Denied") );
1383 #This contains the meat of AddWatcher. but can be called from a routine like
1384 # Create, which doesn't need the additional acl check
1390 PrincipalId => undef,
1396 my $principal = RT::Principal->new($self->CurrentUser);
1397 if ($args{'Email'}) {
1398 my $user = RT::User->new($RT::SystemUser);
1399 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1400 # If we can't load the user by email address, let's try to load by username
1402 ($pid,$msg) = $user->Load($args{'Email'})
1405 $args{'PrincipalId'} = $pid;
1408 if ($args{'PrincipalId'}) {
1409 $principal->Load($args{'PrincipalId'});
1413 # If we can't find this watcher, we need to bail.
1414 unless ($principal->Id) {
1415 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1416 return(0, $self->loc("Could not find or create that user"));
1420 my $group = RT::Group->new($self->CurrentUser);
1421 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1422 unless ($group->id) {
1423 return(0,$self->loc("Group not found"));
1426 if ( $group->HasMember( $principal)) {
1428 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1432 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1433 InsideTransaction => 1 );
1435 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1437 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1440 unless ( $args{'Silent'} ) {
1441 $self->_NewTransaction(
1442 Type => 'AddWatcher',
1443 NewValue => $principal->Id,
1444 Field => $args{'Type'}
1448 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1454 # {{{ sub DeleteWatcher
1456 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1459 Deletes a Ticket watcher. Takes two arguments:
1461 Type (one of Requestor,Cc,AdminCc)
1465 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1467 Email (the email address of an existing wathcer)
1476 my %args = ( Type => undef,
1477 PrincipalId => undef,
1481 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1482 return ( 0, $self->loc("No principal specified") );
1484 my $principal = RT::Principal->new( $self->CurrentUser );
1485 if ( $args{'PrincipalId'} ) {
1487 $principal->Load( $args{'PrincipalId'} );
1490 my $user = RT::User->new( $self->CurrentUser );
1491 $user->LoadByEmail( $args{'Email'} );
1492 $principal->Load( $user->Id );
1495 # If we can't find this watcher, we need to bail.
1496 unless ( $principal->Id ) {
1497 return ( 0, $self->loc("Could not find that principal") );
1500 my $group = RT::Group->new( $self->CurrentUser );
1501 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1502 unless ( $group->id ) {
1503 return ( 0, $self->loc("Group not found") );
1507 #If the watcher we're trying to add is for the current user
1508 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1510 # If it's an AdminCc and they don't have
1511 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1512 if ( $args{'Type'} eq 'AdminCc' ) {
1513 unless ( $self->CurrentUserHasRight('ModifyTicket')
1514 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1515 return ( 0, $self->loc('Permission Denied') );
1519 # If it's a Requestor or Cc and they don't have
1520 # 'Watch' or 'ModifyTicket', bail
1521 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1523 unless ( $self->CurrentUserHasRight('ModifyTicket')
1524 or $self->CurrentUserHasRight('Watch') ) {
1525 return ( 0, $self->loc('Permission Denied') );
1529 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1531 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1535 # If the watcher isn't the current user
1536 # and the current user doesn't have 'ModifyTicket' bail
1538 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1539 return ( 0, $self->loc("Permission Denied") );
1545 # see if this user is already a watcher.
1547 unless ( $group->HasMember($principal) ) {
1549 $self->loc( 'That principal is not a [_1] for this ticket',
1553 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1555 $RT::Logger->error( "Failed to delete "
1557 . " as a member of group "
1563 'Could not remove that principal as a [_1] for this ticket',
1567 unless ( $args{'Silent'} ) {
1568 $self->_NewTransaction( Type => 'DelWatcher',
1569 OldValue => $principal->Id,
1570 Field => $args{'Type'} );
1574 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1575 $principal->Object->Name,
1584 =head2 SquelchMailTo [EMAIL]
1586 Takes an optional email address to never email about updates to this ticket.
1589 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1593 my $t = RT::Ticket->new($RT::SystemUser);
1594 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1596 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1598 my @returned = $t->SquelchMailTo('nobody@example.com');
1600 is($#returned, 0, "The ticket has one squelched recipients");
1602 my @names = $t->Attributes->Names;
1603 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1604 @returned = $t->SquelchMailTo('nobody@example.com');
1607 is($#returned, 0, "The ticket has one squelched recipients");
1609 @names = $t->Attributes->Names;
1610 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1613 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1614 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1615 @returned = $t->SquelchMailTo();
1616 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1626 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1630 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1631 unless grep { $_->Content eq $attr }
1632 $self->Attributes->Named('SquelchMailTo');
1635 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1638 my @attributes = $self->Attributes->Named('SquelchMailTo');
1639 return (@attributes);
1643 =head2 UnsquelchMailTo ADDRESS
1645 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1647 Returns a tuple of (status, message)
1651 sub UnsquelchMailTo {
1654 my $address = shift;
1655 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1656 return ( 0, $self->loc("Permission Denied") );
1659 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1660 return ($val, $msg);
1664 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1666 =head2 RequestorAddresses
1668 B<Returns> String: All Ticket Requestor email addresses as a string.
1672 sub RequestorAddresses {
1675 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1679 return ( $self->Requestors->MemberEmailAddressesAsString );
1683 =head2 AdminCcAddresses
1685 returns String: All Ticket AdminCc email addresses as a string
1689 sub AdminCcAddresses {
1692 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1696 return ( $self->AdminCc->MemberEmailAddressesAsString )
1702 returns String: All Ticket Ccs as a string of email addresses
1709 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1713 return ( $self->Cc->MemberEmailAddressesAsString);
1719 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1721 # {{{ sub Requestors
1726 Returns this ticket's Requestors as an RT::Group object
1733 my $group = RT::Group->new($self->CurrentUser);
1734 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1735 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1748 Returns an RT::Group object which contains this ticket's Ccs.
1749 If the user doesn't have "ShowTicket" permission, returns an empty group
1756 my $group = RT::Group->new($self->CurrentUser);
1757 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1758 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1771 Returns an RT::Group object which contains this ticket's AdminCcs.
1772 If the user doesn't have "ShowTicket" permission, returns an empty group
1779 my $group = RT::Group->new($self->CurrentUser);
1780 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1781 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1791 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1794 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1796 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1798 Takes a param hash with the attributes Type and either PrincipalId or Email
1800 Type is one of Requestor, Cc, AdminCc and Owner
1802 PrincipalId is an RT::Principal id, and Email is an email address.
1804 Returns true if the specified principal (or the one corresponding to the
1805 specified address) is a member of the group Type for this ticket.
1807 XX TODO: This should be Memoized.
1814 my %args = ( Type => 'Requestor',
1815 PrincipalId => undef,
1820 # Load the relevant group.
1821 my $group = RT::Group->new($self->CurrentUser);
1822 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1824 # Find the relevant principal.
1825 my $principal = RT::Principal->new($self->CurrentUser);
1826 if (!$args{PrincipalId} && $args{Email}) {
1827 # Look up the specified user.
1828 my $user = RT::User->new($self->CurrentUser);
1829 $user->LoadByEmail($args{Email});
1831 $args{PrincipalId} = $user->PrincipalId;
1834 # A non-existent user can't be a group member.
1838 $principal->Load($args{'PrincipalId'});
1840 # Ask if it has the member in question
1841 return ($group->HasMember($principal));
1846 # {{{ sub IsRequestor
1848 =head2 IsRequestor PRINCIPAL_ID
1850 Takes an RT::Principal id
1851 Returns true if the principal is a requestor of the current ticket.
1860 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1868 =head2 IsCc PRINCIPAL_ID
1870 Takes an RT::Principal id.
1871 Returns true if the principal is a requestor of the current ticket.
1880 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1888 =head2 IsAdminCc PRINCIPAL_ID
1890 Takes an RT::Principal id.
1891 Returns true if the principal is a requestor of the current ticket.
1899 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1909 Takes an RT::User object. Returns true if that user is this ticket's owner.
1910 returns undef otherwise
1918 # no ACL check since this is used in acl decisions
1919 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1923 #Tickets won't yet have owners when they're being created.
1924 unless ( $self->OwnerObj->id ) {
1928 if ( $person->id == $self->OwnerObj->id ) {
1942 # {{{ Routines dealing with queues
1944 # {{{ sub ValidateQueue
1951 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1955 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1956 my $id = $QueueObj->Load($Value);
1972 my $NewQueue = shift;
1974 #Redundant. ACL gets checked in _Set;
1975 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1976 return ( 0, $self->loc("Permission Denied") );
1979 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1980 $NewQueueObj->Load($NewQueue);
1982 unless ( $NewQueueObj->Id() ) {
1983 return ( 0, $self->loc("That queue does not exist") );
1986 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1987 return ( 0, $self->loc('That is the same value') );
1990 $self->CurrentUser->HasRight(
1991 Right => 'CreateTicket',
1992 Object => $NewQueueObj
1996 return ( 0, $self->loc("You may not create requests in that queue.") );
2000 $self->OwnerObj->HasRight(
2001 Right => 'OwnTicket',
2002 Object => $NewQueueObj
2006 my $clone = RT::Ticket->new( $RT::SystemUser );
2007 $clone->Load( $self->Id );
2008 unless ( $clone->Id ) {
2009 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2011 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2012 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2015 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2024 Takes nothing. returns this ticket's queue object
2031 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2033 #We call __Value so that we can avoid the ACL decision and some deep recursion
2034 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2035 return ($queue_obj);
2042 # {{{ Date printing routines
2048 Returns an RT::Date object containing this ticket's due date
2055 my $time = new RT::Date( $self->CurrentUser );
2057 # -1 is RT::Date slang for never
2059 $time->Set( Format => 'sql', Value => $self->Due );
2062 $time->Set( Format => 'unix', Value => -1 );
2070 # {{{ sub DueAsString
2074 Returns this ticket's due date as a human readable string
2080 return $self->DueObj->AsString();
2085 # {{{ sub ResolvedObj
2089 Returns an RT::Date object of this ticket's 'resolved' time.
2096 my $time = new RT::Date( $self->CurrentUser );
2097 $time->Set( Format => 'sql', Value => $self->Resolved );
2103 # {{{ sub SetStarted
2107 Takes a date in ISO format or undef
2108 Returns a transaction id and a message
2109 The client calls "Start" to note that the project was started on the date in $date.
2110 A null date means "now"
2116 my $time = shift || 0;
2118 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2119 return ( 0, $self->loc("Permission Denied") );
2122 #We create a date object to catch date weirdness
2123 my $time_obj = new RT::Date( $self->CurrentUser() );
2125 $time_obj->Set( Format => 'ISO', Value => $time );
2128 $time_obj->SetToNow();
2131 #Now that we're starting, open this ticket
2132 #TODO do we really want to force this as policy? it should be a scrip
2134 #We need $TicketAsSystem, in case the current user doesn't have
2137 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2138 $TicketAsSystem->Load( $self->Id );
2139 if ( $TicketAsSystem->Status eq 'new' ) {
2140 $TicketAsSystem->Open();
2143 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2149 # {{{ sub StartedObj
2153 Returns an RT::Date object which contains this ticket's
2161 my $time = new RT::Date( $self->CurrentUser );
2162 $time->Set( Format => 'sql', Value => $self->Started );
2172 Returns an RT::Date object which contains this ticket's
2180 my $time = new RT::Date( $self->CurrentUser );
2181 $time->Set( Format => 'sql', Value => $self->Starts );
2191 Returns an RT::Date object which contains this ticket's
2199 my $time = new RT::Date( $self->CurrentUser );
2200 $time->Set( Format => 'sql', Value => $self->Told );
2206 # {{{ sub ToldAsString
2210 A convenience method that returns ToldObj->AsString
2212 TODO: This should be deprecated
2218 if ( $self->Told ) {
2219 return $self->ToldObj->AsString();
2228 # {{{ sub TimeWorkedAsString
2230 =head2 TimeWorkedAsString
2232 Returns the amount of time worked on this ticket as a Text String
2236 sub TimeWorkedAsString {
2238 return "0" unless $self->TimeWorked;
2240 #This is not really a date object, but if we diff a number of seconds
2241 #vs the epoch, we'll get a nice description of time worked.
2243 my $worked = new RT::Date( $self->CurrentUser );
2245 #return the #of minutes worked turned into seconds and written as
2246 # a simple text string
2248 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2255 # {{{ Routines dealing with correspondence/comments
2261 Comment on this ticket.
2262 Takes a hashref with the following attributes:
2263 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2266 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2268 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2269 They will, however, be prepared and you'll be able to access them through the TransactionObj
2271 Returns: Transaction id, Error Message, Transaction Object
2272 (note the different order from Create()!)
2279 my %args = ( CcMessageTo => undef,
2280 BccMessageTo => undef,
2287 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2288 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2289 return ( 0, $self->loc("Permission Denied"), undef );
2291 $args{'NoteType'} = 'Comment';
2293 if ($args{'DryRun'}) {
2294 $RT::Handle->BeginTransaction();
2295 $args{'CommitScrips'} = 0;
2298 my @results = $self->_RecordNote(%args);
2299 if ($args{'DryRun'}) {
2300 $RT::Handle->Rollback();
2307 # {{{ sub Correspond
2311 Correspond on this ticket.
2312 Takes a hashref with the following attributes:
2315 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2317 if there's no MIMEObj, Content is used to build a MIME::Entity object
2319 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2320 They will, however, be prepared and you'll be able to access them through the TransactionObj
2322 Returns: Transaction id, Error Message, Transaction Object
2323 (note the different order from Create()!)
2330 my %args = ( CcMessageTo => undef,
2331 BccMessageTo => undef,
2337 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2338 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2339 return ( 0, $self->loc("Permission Denied"), undef );
2342 $args{'NoteType'} = 'Correspond';
2343 if ($args{'DryRun'}) {
2344 $RT::Handle->BeginTransaction();
2345 $args{'CommitScrips'} = 0;
2348 my @results = $self->_RecordNote(%args);
2350 #Set the last told date to now if this isn't mail from the requestor.
2351 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2352 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2354 if ($args{'DryRun'}) {
2355 $RT::Handle->Rollback();
2364 # {{{ sub _RecordNote
2368 the meat of both comment and correspond.
2370 Performs no access control checks. hence, dangerous.
2377 my %args = ( CcMessageTo => undef,
2378 BccMessageTo => undef,
2385 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2386 return ( 0, $self->loc("No message attached"), undef );
2388 unless ( $args{'MIMEObj'} ) {
2389 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2390 ref $args{'Content'}
2392 : [ $args{'Content'} ]
2396 # convert text parts into utf-8
2397 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2399 # If we've been passed in CcMessageTo and BccMessageTo fields,
2400 # add them to the mime object for passing on to the transaction handler
2401 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2405 foreach my $type (qw/Cc Bcc/) {
2406 if ( defined $args{ $type . 'MessageTo' } ) {
2408 my $addresses = join ', ', (
2409 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2410 Mail::Address->parse( $args{ $type . 'MessageTo' } ) );
2411 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2415 # If this is from an external source, we need to come up with its
2416 # internal Message-ID now, so all emails sent because of this
2417 # message have a common Message-ID
2418 unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2419 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2421 $args{'MIMEObj'}->head->replace( 'RT-Message-ID',
2423 . $RT::VERSION . "-"
2425 . CORE::time() . "-"
2426 . int(rand(2000)) . '.'
2429 . "0" . "@" # Email sent
2434 #Record the correspondence (write the transaction)
2435 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2436 Type => $args{'NoteType'},
2437 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2438 TimeTaken => $args{'TimeTaken'},
2439 MIMEObj => $args{'MIMEObj'},
2440 CommitScrips => $args{'CommitScrips'},
2444 $RT::Logger->err("$self couldn't init a transaction $msg");
2445 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2448 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2460 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2463 my $type = shift || "";
2465 unless ( $self->{"$field$type"} ) {
2466 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2467 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2468 # Maybe this ticket is a merged ticket
2469 my $Tickets = new RT::Tickets( $self->CurrentUser );
2470 # at least to myself
2471 $self->{"$field$type"}->Limit( FIELD => $field,
2472 VALUE => $self->URI,
2473 ENTRYAGGREGATOR => 'OR' );
2474 $Tickets->Limit( FIELD => 'EffectiveId',
2475 VALUE => $self->EffectiveId );
2476 while (my $Ticket = $Tickets->Next) {
2477 $self->{"$field$type"}->Limit( FIELD => $field,
2478 VALUE => $Ticket->URI,
2479 ENTRYAGGREGATOR => 'OR' );
2481 $self->{"$field$type"}->Limit( FIELD => 'Type',
2486 return ( $self->{"$field$type"} );
2491 # {{{ sub DeleteLink
2495 Delete a link. takes a paramhash of Base, Target and Type.
2496 Either Base or Target must be null. The null value will
2497 be replaced with this ticket\'s id
2510 unless ( $args{'Target'} || $args{'Base'} ) {
2511 $RT::Logger->error("Base or Target must be specified\n");
2512 return ( 0, $self->loc('Either base or target must be specified') );
2517 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2518 if ( !$right && $RT::StrictLinkACL ) {
2519 return ( 0, $self->loc("Permission Denied") );
2522 # If the other URI is an RT::Ticket, we want to make sure the user
2523 # can modify it too...
2524 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2525 return (0, $msg) unless $status;
2526 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2529 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2530 ( $RT::StrictLinkACL && $right < 2 ) )
2532 return ( 0, $self->loc("Permission Denied") );
2535 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2538 $RT::Logger->debug("Couldn't find that link\n");
2542 my ($direction, $remote_link);
2544 if ( $args{'Base'} ) {
2545 $remote_link = $args{'Base'};
2546 $direction = 'Target';
2548 elsif ( $args{'Target'} ) {
2549 $remote_link = $args{'Target'};
2553 if ( $args{'Silent'} ) {
2554 return ( $val, $Msg );
2557 my $remote_uri = RT::URI->new( $self->CurrentUser );
2558 $remote_uri->FromURI( $remote_link );
2560 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2561 Type => 'DeleteLink',
2562 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2563 OldValue => $remote_uri->URI || $remote_link,
2567 if ( $remote_uri->IsLocal ) {
2569 my $OtherObj = $remote_uri->Object;
2570 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2571 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2572 : $LINKDIRMAP{$args{'Type'}}->{Target},
2573 OldValue => $self->URI,
2574 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2578 return ( $Trans, $Msg );
2588 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2594 my %args = ( Target => '',
2600 unless ( $args{'Target'} || $args{'Base'} ) {
2601 $RT::Logger->error("Base or Target must be specified\n");
2602 return ( 0, $self->loc('Either base or target must be specified') );
2606 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2607 if ( !$right && $RT::StrictLinkACL ) {
2608 return ( 0, $self->loc("Permission Denied") );
2611 # If the other URI is an RT::Ticket, we want to make sure the user
2612 # can modify it too...
2613 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2614 return (0, $msg) unless $status;
2615 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2618 if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2619 ( $RT::StrictLinkACL && $right < 2 ) )
2621 return ( 0, $self->loc("Permission Denied") );
2624 return $self->_AddLink(%args);
2627 sub __GetTicketFromURI {
2629 my %args = ( URI => '', @_ );
2631 # If the other URI is an RT::Ticket, we want to make sure the user
2632 # can modify it too...
2633 my $uri_obj = RT::URI->new( $self->CurrentUser );
2634 $uri_obj->FromURI( $args{'URI'} );
2636 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2637 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2638 $RT::Logger->warning( "$msg\n" );
2641 my $obj = $uri_obj->Resolver->Object;
2642 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2643 return (1, 'Found not a ticket', undef);
2645 return (1, 'Found ticket', $obj);
2650 Private non-acled variant of AddLink so that links can be added during create.
2656 my %args = ( Target => '',
2662 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2663 return ($val, $msg) if !$val || $exist;
2665 my ($direction, $remote_link);
2666 if ( $args{'Target'} ) {
2667 $remote_link = $args{'Target'};
2668 $direction = 'Base';
2669 } elsif ( $args{'Base'} ) {
2670 $remote_link = $args{'Base'};
2671 $direction = 'Target';
2674 # Don't write the transaction if we're doing this on create
2675 if ( $args{'Silent'} ) {
2676 return ( $val, $msg );
2679 my $remote_uri = RT::URI->new( $self->CurrentUser );
2680 $remote_uri->FromURI( $remote_link );
2682 #Write the transaction
2683 my ( $Trans, $Msg, $TransObj ) =
2684 $self->_NewTransaction(Type => 'AddLink',
2685 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2686 NewValue => $remote_uri->URI || $remote_link,
2689 if ( $remote_uri->IsLocal ) {
2691 my $OtherObj = $remote_uri->Object;
2692 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2693 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2694 : $LINKDIRMAP{$args{'Type'}}->{Target},
2695 NewValue => $self->URI,
2696 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2699 return ( $val, $Msg );
2711 MergeInto take the id of the ticket to merge this ticket into.
2716 my $t1 = RT::Ticket->new($RT::SystemUser);
2717 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2719 my $t2 = RT::Ticket->new($RT::SystemUser);
2720 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2722 my ($msg, $val) = $t1->MergeInto($t2->id);
2724 $t1 = RT::Ticket->new($RT::SystemUser);
2725 is ($t1->id, undef, "ok. we've got a blank ticket1");
2728 is ($t1->id, $t2->id);
2730 is ($t1->Requestors->MembersObj->Count, 2);
2739 my $ticket_id = shift;
2741 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2742 return ( 0, $self->loc("Permission Denied") );
2745 # Load up the new ticket.
2746 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2747 $MergeInto->Load($ticket_id);
2749 # make sure it exists.
2750 unless ( $MergeInto->Id ) {
2751 return ( 0, $self->loc("New ticket doesn't exist") );
2754 # Make sure the current user can modify the new ticket.
2755 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2756 return ( 0, $self->loc("Permission Denied") );
2759 $RT::Handle->BeginTransaction();
2761 # We use EffectiveId here even though it duplicates information from
2762 # the links table becasue of the massive performance hit we'd take
2763 # by trying to do a separate database query for merge info everytime
2766 #update this ticket's effective id to the new ticket's id.
2767 my ( $id_val, $id_msg ) = $self->__Set(
2768 Field => 'EffectiveId',
2769 Value => $MergeInto->Id()
2773 $RT::Handle->Rollback();
2774 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2778 if ( $self->__Value('Status') ne 'resolved' ) {
2780 my ( $status_val, $status_msg )
2781 = $self->__Set( Field => 'Status', Value => 'resolved' );
2783 unless ($status_val) {
2784 $RT::Handle->Rollback();
2787 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2791 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2795 # update all the links that point to that old ticket
2796 my $old_links_to = RT::Links->new($self->CurrentUser);
2797 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2800 while (my $link = $old_links_to->Next) {
2801 if (exists $old_seen{$link->Base."-".$link->Type}) {
2804 elsif ($link->Base eq $MergeInto->URI) {
2807 # First, make sure the link doesn't already exist. then move it over.
2808 my $tmp = RT::Link->new($RT::SystemUser);
2809 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2813 $link->SetTarget($MergeInto->URI);
2814 $link->SetLocalTarget($MergeInto->id);
2816 $old_seen{$link->Base."-".$link->Type} =1;
2821 my $old_links_from = RT::Links->new($self->CurrentUser);
2822 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2824 while (my $link = $old_links_from->Next) {
2825 if (exists $old_seen{$link->Type."-".$link->Target}) {
2828 if ($link->Target eq $MergeInto->URI) {
2831 # First, make sure the link doesn't already exist. then move it over.
2832 my $tmp = RT::Link->new($RT::SystemUser);
2833 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2837 $link->SetBase($MergeInto->URI);
2838 $link->SetLocalBase($MergeInto->id);
2839 $old_seen{$link->Type."-".$link->Target} =1;
2845 # Update time fields
2846 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2848 my $mutator = "Set$type";
2849 $MergeInto->$mutator(
2850 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2853 #add all of this ticket's watchers to that ticket.
2854 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2856 my $people = $self->$watcher_type->MembersObj;
2857 my $addwatcher_type = $watcher_type;
2858 $addwatcher_type =~ s/s$//;
2860 while ( my $watcher = $people->Next ) {
2862 my ($val, $msg) = $MergeInto->_AddWatcher(
2863 Type => $addwatcher_type,
2865 PrincipalId => $watcher->MemberId
2868 $RT::Logger->warning($msg);
2874 #find all of the tickets that were merged into this ticket.
2875 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2876 $old_mergees->Limit(
2877 FIELD => 'EffectiveId',
2882 # update their EffectiveId fields to the new ticket's id
2883 while ( my $ticket = $old_mergees->Next() ) {
2884 my ( $val, $msg ) = $ticket->__Set(
2885 Field => 'EffectiveId',
2886 Value => $MergeInto->Id()
2890 #make a new link: this ticket is merged into that other ticket.
2891 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2893 $MergeInto->_SetLastUpdated;
2895 $RT::Handle->Commit();
2896 return ( 1, $self->loc("Merge Successful") );
2903 # {{{ Routines dealing with ownership
2909 Takes nothing and returns an RT::User object of
2917 #If this gets ACLed, we lose on a rights check in User.pm and
2918 #get deep recursion. if we need ACLs here, we need
2919 #an equiv without ACLs
2921 my $owner = new RT::User( $self->CurrentUser );
2922 $owner->Load( $self->__Value('Owner') );
2924 #Return the owner object
2930 # {{{ sub OwnerAsString
2932 =head2 OwnerAsString
2934 Returns the owner's email address
2940 return ( $self->OwnerObj->EmailAddress );
2950 Takes two arguments:
2951 the Id or Name of the owner
2952 and (optionally) the type of the SetOwner Transaction. It defaults
2953 to 'Give'. 'Steal' is also a valid option.
2957 my $root = RT::User->new($RT::SystemUser);
2958 $root->Load('root');
2959 ok ($root->Id, "Loaded the root user");
2960 my $t = RT::Ticket->new($RT::SystemUser);
2962 $t->SetOwner('root');
2963 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2965 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2966 my $txns = RT::Transactions->new($RT::SystemUser);
2967 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2968 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2969 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2970 $txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
2972 my $steal = $txns->First;
2973 ok($steal->OldValue == $root->Id , "Stolen from root");
2974 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2982 my $NewOwner = shift;
2983 my $Type = shift || "Give";
2985 $RT::Handle->BeginTransaction();
2987 $self->_SetLastUpdated(); # lock the ticket
2988 $self->Load( $self->id ); # in case $self changed while waiting for lock
2990 my $OldOwnerObj = $self->OwnerObj;
2992 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2993 $NewOwnerObj->Load( $NewOwner );
2994 unless ( $NewOwnerObj->Id ) {
2995 $RT::Handle->Rollback();
2996 return ( 0, $self->loc("That user does not exist") );
3000 # must have ModifyTicket rights
3001 # or TakeTicket/StealTicket and $NewOwner is self
3002 # see if it's a take
3003 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3004 unless ( $self->CurrentUserHasRight('ModifyTicket')
3005 || $self->CurrentUserHasRight('TakeTicket') ) {
3006 $RT::Handle->Rollback();
3007 return ( 0, $self->loc("Permission Denied") );
3011 # see if it's a steal
3012 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
3013 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3015 unless ( $self->CurrentUserHasRight('ModifyTicket')
3016 || $self->CurrentUserHasRight('StealTicket') ) {
3017 $RT::Handle->Rollback();
3018 return ( 0, $self->loc("Permission Denied") );
3022 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3023 $RT::Handle->Rollback();
3024 return ( 0, $self->loc("Permission Denied") );
3028 # If we're not stealing and the ticket has an owner and it's not
3030 if ( $Type ne 'Steal' and $Type ne 'Force'
3031 and $OldOwnerObj->Id != $RT::Nobody->Id
3032 and $OldOwnerObj->Id != $self->CurrentUser->Id )
3034 $RT::Handle->Rollback();
3035 return ( 0, $self->loc("You can only take tickets that are unowned") )
3036 if $NewOwnerObj->id == $self->CurrentUser->id;
3039 $self->loc("You can only reassign tickets that you own or that are unowned" )
3043 #If we've specified a new owner and that user can't modify the ticket
3044 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3045 $RT::Handle->Rollback();
3046 return ( 0, $self->loc("That user may not own tickets in that queue") );
3049 # If the ticket has an owner and it's the new owner, we don't need
3051 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3052 $RT::Handle->Rollback();
3053 return ( 0, $self->loc("That user already owns that ticket") );
3056 # Delete the owner in the owner group, then add a new one
3057 # TODO: is this safe? it's not how we really want the API to work
3058 # for most things, but it's fast.
3059 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3061 $RT::Handle->Rollback();
3062 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3065 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3066 PrincipalId => $NewOwnerObj->PrincipalId,
3067 InsideTransaction => 1 );
3069 $RT::Handle->Rollback();
3070 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3073 # We call set twice with slightly different arguments, so
3074 # as to not have an SQL transaction span two RT transactions
3076 my ( $val, $msg ) = $self->_Set(
3078 RecordTransaction => 0,
3079 Value => $NewOwnerObj->Id,
3081 TransactionType => $Type,
3082 CheckACL => 0, # don't check acl
3086 $RT::Handle->Rollback;
3087 return ( 0, $self->loc("Could not change owner. ") . $msg );
3090 ($val, $msg) = $self->_NewTransaction(
3093 NewValue => $NewOwnerObj->Id,
3094 OldValue => $OldOwnerObj->Id,
3099 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3100 $OldOwnerObj->Name, $NewOwnerObj->Name );
3103 $RT::Handle->Rollback();
3107 $RT::Handle->Commit();
3109 return ( $val, $msg );
3118 A convenince method to set the ticket's owner to the current user
3124 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3133 Convenience method to set the owner to 'nobody' if the current user is the owner.
3139 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3148 A convenience method to change the owner of the current ticket to the
3149 current user. Even if it's owned by another user.
3156 if ( $self->IsOwner( $self->CurrentUser ) ) {
3157 return ( 0, $self->loc("You already own this ticket") );
3160 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3170 # {{{ Routines dealing with status
3172 # {{{ sub ValidateStatus
3174 =head2 ValidateStatus STATUS
3176 Takes a string. Returns true if that status is a valid status for this ticket.
3177 Returns false otherwise.
3181 sub ValidateStatus {
3185 #Make sure the status passed in is valid
3186 unless ( $self->QueueObj->IsValidStatus($status) ) {
3198 =head2 SetStatus STATUS
3200 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3202 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.
3206 my $tt = RT::Ticket->new($RT::SystemUser);
3207 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3210 is($tt->Status, 'new', "New ticket is created as new");
3212 ($id, $msg) = $tt->SetStatus('open');
3214 like($msg, qr/open/i, "Status message is correct");
3215 ($id, $msg) = $tt->SetStatus('resolved');
3217 like($msg, qr/resolved/i, "Status message is correct");
3218 ($id, $msg) = $tt->SetStatus('resolved');
3232 $args{Status} = shift;
3239 if ( $args{Status} eq 'deleted') {
3240 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3241 return ( 0, $self->loc('Permission Denied') );
3244 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3245 return ( 0, $self->loc('Permission Denied') );
3249 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3250 return (0, $self->loc('That ticket has unresolved dependencies'));
3253 my $now = RT::Date->new( $self->CurrentUser );
3256 #If we're changing the status from new, record that we've started
3257 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3259 #Set the Started time to "now"
3260 $self->_Set( Field => 'Started',
3262 RecordTransaction => 0 );
3265 #When we close a ticket, set the 'Resolved' attribute to now.
3266 # It's misnamed, but that's just historical.
3267 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3268 $self->_Set( Field => 'Resolved',
3270 RecordTransaction => 0 );
3273 #Actually update the status
3274 my ($val, $msg)= $self->_Set( Field => 'Status',
3275 Value => $args{Status},
3278 TransactionType => 'Status' );
3289 Takes no arguments. Marks this ticket for garbage collection
3295 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3296 return $self->Delete;
3301 return ( $self->SetStatus('deleted') );
3303 # TODO: garbage collection
3312 Sets this ticket's status to stalled
3318 return ( $self->SetStatus('stalled') );
3327 Sets this ticket's status to rejected
3333 return ( $self->SetStatus('rejected') );
3342 Sets this ticket\'s status to Open
3348 return ( $self->SetStatus('open') );
3357 Sets this ticket\'s status to Resolved
3363 return ( $self->SetStatus('resolved') );
3371 # {{{ Actions + Routines dealing with transactions
3373 # {{{ sub SetTold and _SetTold
3375 =head2 SetTold ISO [TIMETAKEN]
3377 Updates the told and records a transaction
3384 $told = shift if (@_);
3385 my $timetaken = shift || 0;
3387 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3388 return ( 0, $self->loc("Permission Denied") );
3391 my $datetold = new RT::Date( $self->CurrentUser );
3393 $datetold->Set( Format => 'iso',
3397 $datetold->SetToNow();
3400 return ( $self->_Set( Field => 'Told',
3401 Value => $datetold->ISO,
3402 TimeTaken => $timetaken,
3403 TransactionType => 'Told' ) );
3408 Updates the told without a transaction or acl check. Useful when we're sending replies.
3415 my $now = new RT::Date( $self->CurrentUser );
3418 #use __Set to get no ACLs ;)
3419 return ( $self->__Set( Field => 'Told',
3420 Value => $now->ISO ) );
3425 =head2 TransactionBatch
3427 Returns an array reference of all transactions created on this ticket during
3428 this ticket object's lifetime, or undef if there were none.
3430 Only works when the $RT::UseTransactionBatch config variable is set to true.
3434 sub TransactionBatch {
3436 return $self->{_TransactionBatch};
3442 # DESTROY methods need to localize $@, or it may unset it. This
3443 # causes $m->abort to not bubble all of the way up. See perlbug
3444 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3447 # The following line eliminates reentrancy.
3448 # It protects against the fact that perl doesn't deal gracefully
3449 # when an object's refcount is changed in its destructor.
3450 return if $self->{_Destroyed}++;
3452 my $batch = $self->TransactionBatch or return;
3453 return unless @$batch;
3456 RT::Scrips->new($RT::SystemUser)->Apply(
3457 Stage => 'TransactionBatch',
3459 TransactionObj => $batch->[0],
3460 Type => join(',', (map { $_->Type } @{$batch}) )
3466 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3468 # {{{ sub _OverlayAccessible
3470 sub _OverlayAccessible {
3472 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3473 Queue => { 'read' => 1, 'write' => 1 },
3474 Requestors => { 'read' => 1, 'write' => 1 },
3475 Owner => { 'read' => 1, 'write' => 1 },
3476 Subject => { 'read' => 1, 'write' => 1 },
3477 InitialPriority => { 'read' => 1, 'write' => 1 },
3478 FinalPriority => { 'read' => 1, 'write' => 1 },
3479 Priority => { 'read' => 1, 'write' => 1 },
3480 Status => { 'read' => 1, 'write' => 1 },
3481 TimeEstimated => { 'read' => 1, 'write' => 1 },
3482 TimeWorked => { 'read' => 1, 'write' => 1 },
3483 TimeLeft => { 'read' => 1, 'write' => 1 },
3484 Told => { 'read' => 1, 'write' => 1 },
3485 Resolved => { 'read' => 1 },
3486 Type => { 'read' => 1 },
3487 Starts => { 'read' => 1, 'write' => 1 },
3488 Started => { 'read' => 1, 'write' => 1 },
3489 Due => { 'read' => 1, 'write' => 1 },
3490 Creator => { 'read' => 1, 'auto' => 1 },
3491 Created => { 'read' => 1, 'auto' => 1 },
3492 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3493 LastUpdated => { 'read' => 1, 'auto' => 1 }
3505 my %args = ( Field => undef,
3508 RecordTransaction => 1,
3511 TransactionType => 'Set',
3514 if ($args{'CheckACL'}) {
3515 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3516 return ( 0, $self->loc("Permission Denied"));
3520 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3521 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3522 return(0, $self->loc("Internal Error"));
3525 #if the user is trying to modify the record
3527 #Take care of the old value we really don't want to get in an ACL loop.
3528 # so ask the super::_Value
3529 my $Old = $self->SUPER::_Value("$args{'Field'}");
3532 if ( $args{'UpdateTicket'} ) {
3535 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3536 Value => $args{'Value'} );
3538 #If we can't actually set the field to the value, don't record
3539 # a transaction. instead, get out of here.
3540 return ( 0, $msg ) unless $ret;
3543 if ( $args{'RecordTransaction'} == 1 ) {
3545 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3546 Type => $args{'TransactionType'},
3547 Field => $args{'Field'},
3548 NewValue => $args{'Value'},
3550 TimeTaken => $args{'TimeTaken'},
3552 return ( $Trans, scalar $TransObj->BriefDescription );
3555 return ( $ret, $msg );
3565 Takes the name of a table column.
3566 Returns its value as a string, if the user passes an ACL check
3575 #if the field is public, return it.
3576 if ( $self->_Accessible( $field, 'public' ) ) {
3578 #$RT::Logger->debug("Skipping ACL check for $field\n");
3579 return ( $self->SUPER::_Value($field) );
3583 #If the current user doesn't have ACLs, don't let em at it.
3585 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3588 return ( $self->SUPER::_Value($field) );
3594 # {{{ sub _UpdateTimeTaken
3596 =head2 _UpdateTimeTaken
3598 This routine will increment the timeworked counter. it should
3599 only be called from _NewTransaction
3603 sub _UpdateTimeTaken {
3605 my $Minutes = shift;
3608 $Total = $self->SUPER::_Value("TimeWorked");
3609 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3611 Field => "TimeWorked",
3622 # {{{ Routines dealing with ACCESS CONTROL
3624 # {{{ sub CurrentUserHasRight
3626 =head2 CurrentUserHasRight
3628 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3629 1 if the user has that right. It returns 0 if the user doesn't have that right.
3633 sub CurrentUserHasRight {
3639 Principal => $self->CurrentUser->UserObj(),
3652 Takes a paramhash with the attributes 'Right' and 'Principal'
3653 'Right' is a ticket-scoped textual right from RT::ACE
3654 'Principal' is an RT::User object
3656 Returns 1 if the principal has the right. Returns undef if not.
3668 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3671 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3676 $args{'Principal'}->HasRight(
3678 Right => $args{'Right'}
3689 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3690 It isn't acutally a searchbuilder collection itself.
3697 unless ($self->{'__reminders'}) {
3698 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3699 $self->{'__reminders'}->Ticket($self->id);
3701 return $self->{'__reminders'};
3707 # {{{ sub Transactions
3711 Returns an RT::Transactions object of all transactions on this ticket
3718 my $transactions = RT::Transactions->new( $self->CurrentUser );
3720 #If the user has no rights, return an empty object
3721 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3722 $transactions->LimitToTicket($self->id);
3724 # if the user may not see comments do not return them
3725 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3726 $transactions->Limit(
3731 $transactions->Limit(
3734 VALUE => "CommentEmailRecord",
3735 ENTRYAGGREGATOR => 'AND'
3741 return ($transactions);
3747 # {{{ TransactionCustomFields
3749 =head2 TransactionCustomFields
3751 Returns the custom fields that transactions on tickets will have.
3755 sub TransactionCustomFields {
3757 return $self->QueueObj->TicketTransactionCustomFields;
3762 # {{{ sub CustomFieldValues
3764 =head2 CustomFieldValues
3766 # Do name => id mapping (if needed) before falling back to
3767 # RT::Record's CustomFieldValues
3773 sub CustomFieldValues {
3777 return $self->SUPER::CustomFieldValues( $field )
3778 if !$field || $field =~ /^\d+$/;
3780 my $cf = RT::CustomField->new( $self->CurrentUser );
3781 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3782 unless ( $cf->id ) {
3783 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3786 # If we didn't find a valid cfid, give up.
3787 return RT::ObjectCustomFieldValues->new( $self->CurrentUser )
3790 return $self->SUPER::CustomFieldValues( $cf->id );
3795 # {{{ sub CustomFieldLookupType
3797 =head2 CustomFieldLookupType
3799 Returns the RT::Ticket lookup type, which can be passed to
3800 RT::CustomField->Create() via the 'LookupType' hash key.
3806 sub CustomFieldLookupType {
3807 "RT::Queue-RT::Ticket";
3814 Jesse Vincent, jesse@bestpractical.com