1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
51 my $ticket = new RT::Ticket($CurrentUser);
52 $ticket->Load($ticket_id);
56 This module lets you manipulate RT\'s ticket object.
64 ok(my $testqueue = RT::Queue->new($RT::SystemUser));
65 ok($testqueue->Create( Name => 'ticket tests'));
66 ok($testqueue->Id != 0);
67 use_ok(RT::CustomField);
68 ok(my $testcf = RT::CustomField->new($RT::SystemUser));
69 my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti',
70 Queue => $testqueue->id,
71 Type => 'SelectMultiple');
72 ok($ret,"Created the custom field - ".$cmsg);
73 ($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1',
75 Description => 'A testing value');
77 ok($ret, "Added a value - ".$cmsg);
79 ok($testcf->AddValue ( Name => 'Value2',
81 Description => 'Another testing value'));
82 ok($testcf->AddValue ( Name => 'Value3',
84 Description => 'Yet Another testing value'));
86 ok($testcf->Values->Count == 3);
90 my $u = RT::User->new($RT::SystemUser);
92 ok ($u->Id, "Found the root user");
93 ok(my $t = RT::Ticket->new($RT::SystemUser));
94 ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
99 ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
100 ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
102 ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
103 ok($t->CustomFieldValues($testcf->Id)->Count == 1);
104 ok($t->CustomFieldValues($testcf->Id)->First &&
105 $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
107 ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
109 ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
110 ok($t->CustomFieldValues($testcf->Id)->Count == 0);
112 ok(my $t2 = RT::Ticket->new($RT::SystemUser));
114 is($t2->Subject, 'Testing');
115 is($t2->QueueObj->Id, $testqueue->id);
116 ok($t2->OwnerObj->Id == $u->Id);
118 my $t3 = RT::Ticket->new($RT::SystemUser);
119 my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
120 Subject => 'Testing',
122 my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
124 ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
125 my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
127 ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
128 my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
130 ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
131 ok($t->CustomFieldValues($testcf->Id)->Count == 2,
132 "This ticket has 2 custom field values");
133 ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
134 "This ticket has 1 custom field value");
144 no warnings qw(redefine);
151 use RT::CustomFields;
153 use RT::Transactions;
154 use RT::URI::fsck_com_rt;
161 ok(require RT::Ticket, "Loading the RT::Ticket library");
170 # A helper table for links mapping to make it easier
171 # to build and parse links between tickets
173 use vars '%LINKTYPEMAP';
176 MemberOf => { Type => 'MemberOf',
178 Parents => { Type => 'MemberOf',
180 Members => { Type => 'MemberOf',
182 Children => { Type => 'MemberOf',
184 HasMember => { Type => 'MemberOf',
186 RefersTo => { Type => 'RefersTo',
188 ReferredToBy => { Type => 'RefersTo',
190 DependsOn => { Type => 'DependsOn',
192 DependedOnBy => { Type => 'DependsOn',
194 MergedInto => { Type => 'MergedInto',
202 # A helper table for links mapping to make it easier
203 # to build and parse links between tickets
205 use vars '%LINKDIRMAP';
208 MemberOf => { Base => 'MemberOf',
209 Target => 'HasMember', },
210 RefersTo => { Base => 'RefersTo',
211 Target => 'ReferredToBy', },
212 DependsOn => { Base => 'DependsOn',
213 Target => 'DependedOnBy', },
214 MergedInto => { Base => 'MergedInto',
215 Target => 'MergedInto', },
221 sub LINKTYPEMAP { return \%LINKTYPEMAP }
222 sub LINKDIRMAP { return \%LINKDIRMAP }
228 Takes a single argument. This can be a ticket id, ticket alias or
229 local ticket uri. If the ticket can't be loaded, returns undef.
230 Otherwise, returns the ticket id.
238 #TODO modify this routine to look at EffectiveId and do the recursive load
239 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
242 #If it's a local URI, turn it into a ticket id
243 if ( $RT::TicketBaseURI && $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
247 #If it's a remote URI, we're going to punt for now
248 elsif ( $id =~ '://' ) {
252 #If we have an integer URI, load the ticket
253 if ( $id =~ /^\d+$/ ) {
254 my ($ticketid,$msg) = $self->LoadById($id);
257 $RT::Logger->crit("$self tried to load a bogus ticket: $id\n");
262 #It's not a URI. It's not a numerical ticket ID. Punt!
264 $RT::Logger->warning("Tried to load a bogus ticket id: '$id'");
268 #If we're merged, resolve the merge.
269 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
270 $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId);
271 return ( $self->Load( $self->EffectiveId ) );
274 #Ok. we're loaded. lets get outa here.
275 return ( $self->Id );
285 Given a local ticket URI, loads the specified ticket.
293 if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
295 return ( $self->Load($id) );
308 Arguments: ARGS is a hash of named parameters. Valid parameters are:
311 Queue - Either a Queue object or a Queue Name
312 Requestor - A reference to a list of email addresses or RT user Names
313 Cc - A reference to a list of email addresses or Names
314 AdminCc - A reference to a list of email addresses or Names
315 Type -- The ticket\'s type. ignore this for now
316 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
317 Subject -- A string describing the subject of the ticket
318 Priority -- an integer from 0 to 99
319 InitialPriority -- an integer from 0 to 99
320 FinalPriority -- an integer from 0 to 99
321 Status -- any valid status (Defined in RT::Queue)
322 TimeEstimated -- an integer. estimated time for this task in minutes
323 TimeWorked -- an integer. time worked so far in minutes
324 TimeLeft -- an integer. time remaining in minutes
325 Starts -- an ISO date describing the ticket\'s start date and time in GMT
326 Due -- an ISO date describing the ticket\'s due date and time in GMT
327 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
328 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
331 Returns: TICKETID, Transaction Object, Error Message
336 my $t = RT::Ticket->new($RT::SystemUser);
338 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");
340 ok ( my $id = $t->Id, "Got ticket id");
341 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
342 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
343 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
354 EffectiveId => undef,
362 InitialPriority => undef,
363 FinalPriority => undef,
374 _RecordTransaction => 1,
378 my ( $ErrStr, $Owner, $resolved );
379 my (@non_fatal_errors);
381 my $QueueObj = RT::Queue->new($RT::SystemUser);
383 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
384 $QueueObj->Load( $args{'Queue'} );
386 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
387 $QueueObj->Load( $args{'Queue'}->Id );
390 $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object." );
393 #Can't create a ticket without a queue.
394 unless ( defined($QueueObj) && $QueueObj->Id ) {
395 $RT::Logger->debug("$self No queue given for ticket creation.");
396 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
399 #Now that we have a queue, Check the ACLS
401 $self->CurrentUser->HasRight(
402 Right => 'CreateTicket',
409 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
412 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
413 return ( 0, 0, $self->loc('Invalid value for status') );
416 #Since we have a queue, we can set queue defaults
419 # If there's no queue default initial priority and it's not set, set it to 0
420 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
421 unless ( $args{'InitialPriority'} );
425 # If there's no queue default final priority and it's not set, set it to 0
426 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
427 unless ( $args{'FinalPriority'} );
429 # Priority may have changed from InitialPriority, for the case
430 # where we're importing tickets (eg, from an older RT version.)
431 my $priority = $args{'Priority'} || $args{'InitialPriority'};
434 #TODO we should see what sort of due date we're getting, rather +
435 # than assuming it's in ISO format.
437 #Set the due date. if we didn't get fed one, use the queue default due in
438 my $Due = new RT::Date( $self->CurrentUser );
440 if ( $args{'Due'} ) {
441 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
443 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
445 $Due->AddDays( $due_in );
448 my $Starts = new RT::Date( $self->CurrentUser );
449 if ( defined $args{'Starts'} ) {
450 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
453 my $Started = new RT::Date( $self->CurrentUser );
454 if ( defined $args{'Started'} ) {
455 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
458 my $Resolved = new RT::Date( $self->CurrentUser );
459 if ( defined $args{'Resolved'} ) {
460 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
463 #If the status is an inactive status, set the resolved date
464 if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} )
466 $RT::Logger->debug( "Got a "
468 . "ticket with a resolved of "
469 . $args{'Resolved'} );
475 # {{{ Dealing with time fields
477 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
478 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
479 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
483 # {{{ Deal with setting the owner
485 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
486 $Owner = $args{'Owner'};
489 #If we've been handed something else, try to load the user.
490 elsif ( $args{'Owner'} ) {
491 $Owner = RT::User->new( $self->CurrentUser );
492 $Owner->Load( $args{'Owner'} );
494 push( @non_fatal_errors,
495 $self->loc("Owner could not be set.") . " "
496 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} )
498 unless ( $Owner->Id );
501 #If we have a proposed owner and they don't have the right
502 #to own a ticket, scream about it and make them not the owner
506 and ( $Owner->Id != $RT::Nobody->Id )
516 $RT::Logger->warning( "User "
520 . "as a ticket owner but has no rights to own "
524 push @non_fatal_errors,
525 $self->loc( "Owner '[_1]' does not have rights to own this ticket.",
532 #If we haven't been handed a valid owner, make it nobody.
533 unless ( defined($Owner) && $Owner->Id ) {
534 $Owner = new RT::User( $self->CurrentUser );
535 $Owner->Load( $RT::Nobody->Id );
540 # We attempt to load or create each of the people who might have a role for this ticket
541 # _outside_ the transaction, so we don't get into ticket creation races
542 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
543 next unless ( defined $args{$type} );
544 foreach my $watcher (
545 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
547 my $user = RT::User->new($RT::SystemUser);
548 $user->LoadOrCreateByEmail($watcher)
549 if ( $watcher && $watcher !~ /^\d+$/ );
553 $RT::Handle->BeginTransaction();
556 Queue => $QueueObj->Id,
558 Subject => $args{'Subject'},
559 InitialPriority => $args{'InitialPriority'},
560 FinalPriority => $args{'FinalPriority'},
561 Priority => $priority,
562 Status => $args{'Status'},
563 TimeWorked => $args{'TimeWorked'},
564 TimeEstimated => $args{'TimeEstimated'},
565 TimeLeft => $args{'TimeLeft'},
566 Type => $args{'Type'},
567 Starts => $Starts->ISO,
568 Started => $Started->ISO,
569 Resolved => $Resolved->ISO,
573 # Parameters passed in during an import that we probably don't want to touch, otherwise
574 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
575 $params{$attr} = $args{$attr} if ( $args{$attr} );
578 # Delete null integer parameters
580 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
581 delete $params{$attr}
582 unless ( exists $params{$attr} && $params{$attr} );
585 # Delete the time worked if we're counting it in the transaction
586 delete $params{TimeWorked} if $args{'_RecordTransaction'};
588 my ($id,$ticket_message) = $self->SUPER::Create( %params);
590 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
591 $RT::Handle->Rollback();
593 $self->loc("Ticket could not be created due to an internal error")
597 #Set the ticket's effective ID now that we've created it.
598 my ( $val, $msg ) = $self->__Set(
599 Field => 'EffectiveId',
600 Value => ( $args{'EffectiveId'} || $id )
604 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
605 $RT::Handle->Rollback();
607 $self->loc("Ticket could not be created due to an internal error")
611 my $create_groups_ret = $self->_CreateTicketGroups();
612 unless ($create_groups_ret) {
613 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
615 . ". aborting Ticket creation." );
616 $RT::Handle->Rollback();
618 $self->loc("Ticket could not be created due to an internal error")
622 # Set the owner in the Groups table
623 # We denormalize it into the Ticket table too because doing otherwise would
624 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
626 $self->OwnerGroup->_AddMember(
627 PrincipalId => $Owner->PrincipalId,
628 InsideTransaction => 1
631 # {{{ Deal with setting up watchers
633 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
634 next unless ( defined $args{$type} );
635 foreach my $watcher (
636 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
639 # If there is an empty entry in the list, let's get out of here.
640 next unless $watcher;
642 # we reason that all-digits number must be a principal id, not email
643 # this is the only way to can add
645 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
649 if ( $type eq 'AdminCc' ) {
651 # Note that we're using AddWatcher, rather than _AddWatcher, as we
652 # actually _want_ that ACL check. Otherwise, random ticket creators
653 # could make themselves adminccs and maybe get ticket rights. that would
655 ( $wval, $wmsg ) = $self->AddWatcher(
662 ( $wval, $wmsg ) = $self->_AddWatcher(
669 push @non_fatal_errors, $wmsg unless ($wval);
674 # {{{ Deal with setting up links
676 foreach my $type ( keys %LINKTYPEMAP ) {
677 next unless ( defined $args{$type} );
679 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
681 my ( $wval, $wmsg ) = $self->_AddLink(
682 Type => $LINKTYPEMAP{$type}->{'Type'},
683 $LINKTYPEMAP{$type}->{'Mode'} => $link,
687 push @non_fatal_errors, $wmsg unless ($wval);
693 # {{{ Add all the custom fields
695 foreach my $arg ( keys %args ) {
696 next unless ( $arg =~ /^CustomField-(\d+)$/i );
699 my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
701 next unless ( length($value) );
703 # Allow passing in uploaded LargeContent etc by hash reference
704 $self->_AddCustomFieldValue(
705 (UNIVERSAL::isa( $value => 'HASH' )
710 RecordTransaction => 0,
717 if ( $args{'_RecordTransaction'} ) {
719 # {{{ Add a transaction for the create
720 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
722 TimeTaken => $args{'TimeWorked'},
723 MIMEObj => $args{'MIMEObj'}
726 if ( $self->Id && $Trans ) {
728 $TransObj->UpdateCustomFields(ARGSRef => \%args);
730 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
731 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
732 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
735 $RT::Handle->Rollback();
737 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
738 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
739 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
742 $RT::Handle->Commit();
743 return ( $self->Id, $TransObj->Id, $ErrStr );
749 # Not going to record a transaction
750 $RT::Handle->Commit();
751 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
752 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
753 return ( $self->Id, 0, $ErrStr );
764 =head2 UpdateFrom822 $MESSAGE
766 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
767 Returns an um. ask me again when the code exists
772 my $simple_update = <<EOF;
774 AddRequestor: jesse\@example.com
777 my $ticket = RT::Ticket->new($RT::SystemUser);
778 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
779 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
780 $ticket->UpdateFrom822($simple_update);
781 is($ticket->Subject, 'target', "changed the subject");
782 my $jesse = RT::User->new($RT::SystemUser);
783 $jesse->LoadByEmail('jesse@example.com');
784 ok ($jesse->Id, "There's a user for jesse");
785 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
795 my %args = $self->_Parse822HeadersForAttributes($content);
799 Queue => $args{'queue'},
800 Subject => $args{'subject'},
801 Status => $args{'status'},
803 Starts => $args{'starts'},
804 Started => $args{'started'},
805 Resolved => $args{'resolved'},
806 Owner => $args{'owner'},
807 Requestor => $args{'requestor'},
809 AdminCc => $args{'admincc'},
810 TimeWorked => $args{'timeworked'},
811 TimeEstimated => $args{'timeestimated'},
812 TimeLeft => $args{'timeleft'},
813 InitialPriority => $args{'initialpriority'},
814 Priority => $args{'priority'},
815 FinalPriority => $args{'finalpriority'},
816 Type => $args{'type'},
817 DependsOn => $args{'dependson'},
818 DependedOnBy => $args{'dependedonby'},
819 RefersTo => $args{'refersto'},
820 ReferredToBy => $args{'referredtoby'},
821 Members => $args{'members'},
822 MemberOf => $args{'memberof'},
823 MIMEObj => $args{'mimeobj'}
826 foreach my $type qw(Requestor Cc Admincc) {
828 foreach my $action ( 'Add', 'Del', '' ) {
830 my $lctag = lc($action) . lc($type);
831 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
833 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
834 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
839 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
844 # Add custom field entries to %ticketargs.
845 # TODO: allow named custom fields
847 /^customfield-(\d+)$/
848 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
851 # for each ticket we've been told to update, iterate through the set of
852 # rfc822 headers and perform that update to the ticket.
855 # {{{ Set basic fields
869 # Resolve the queue from a name to a numeric id.
870 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
871 my $tempqueue = RT::Queue->new($RT::SystemUser);
872 $tempqueue->Load( $ticketargs{'Queue'} );
873 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
878 foreach my $attribute (@attribs) {
879 my $value = $ticketargs{$attribute};
881 if ( $value ne $self->$attribute() ) {
883 my $method = "Set$attribute";
884 my ( $code, $msg ) = $self->$method($value);
886 push @results, $self->loc($attribute) . ': ' . $msg;
891 # We special case owner changing, so we can use ForceOwnerChange
892 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
893 my $ChownType = "Give";
894 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
896 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
897 push ( @results, $msg );
901 # Deal with setting watchers
904 # Acceptable arguments:
911 foreach my $type qw(Requestor Cc AdminCc) {
913 # If we've been given a number of delresses to del, do it.
914 foreach my $address (@{$ticketargs{'Del'.$type}}) {
915 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
916 push (@results, $msg) ;
919 # If we've been given a number of addresses to add, do it.
920 foreach my $address (@{$ticketargs{'Add'.$type}}) {
921 $RT::Logger->debug("Adding $address as a $type");
922 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
923 push (@results, $msg) ;
934 # {{{ _Parse822HeadersForAttributes Content
936 =head2 _Parse822HeadersForAttributes Content
938 Takes an RFC822 style message and parses its attributes into a hash.
942 sub _Parse822HeadersForAttributes {
947 my @lines = ( split ( /\n/, $content ) );
948 while ( defined( my $line = shift @lines ) ) {
949 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
954 if ( defined( $args{$tag} ) )
955 { #if we're about to get a second value, make it an array
956 $args{$tag} = [ $args{$tag} ];
958 if ( ref( $args{$tag} ) )
959 { #If it's an array, we want to push the value
960 push @{ $args{$tag} }, $value;
962 else { #if there's nothing there, just set the value
963 $args{$tag} = $value;
965 } elsif ($line =~ /^$/) {
967 #TODO: this won't work, since "" isn't of the form "foo:value"
969 while ( defined( my $l = shift @lines ) ) {
970 push @{ $args{'content'} }, $l;
976 foreach my $date qw(due starts started resolved) {
977 my $dateobj = RT::Date->new($RT::SystemUser);
978 if ( $args{$date} =~ /^\d+$/ ) {
979 $dateobj->Set( Format => 'unix', Value => $args{$date} );
982 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
984 $args{$date} = $dateobj->ISO;
986 $args{'mimeobj'} = MIME::Entity->new();
987 $args{'mimeobj'}->build(
988 Type => ( $args{'contenttype'} || 'text/plain' ),
989 Data => ($args{'content'} || '')
999 =head2 Import PARAMHASH
1002 Doesn\'t create a transaction.
1003 Doesn\'t supply queue defaults, etc.
1011 my ( $ErrStr, $QueueObj, $Owner );
1015 EffectiveId => undef,
1019 Owner => $RT::Nobody->Id,
1020 Subject => '[no subject]',
1021 InitialPriority => undef,
1022 FinalPriority => undef,
1033 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1034 $QueueObj = RT::Queue->new($RT::SystemUser);
1035 $QueueObj->Load( $args{'Queue'} );
1037 #TODO error check this and return 0 if it\'s not loading properly +++
1039 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1040 $QueueObj = RT::Queue->new($RT::SystemUser);
1041 $QueueObj->Load( $args{'Queue'}->Id );
1045 "$self " . $args{'Queue'} . " not a recognised queue object." );
1048 #Can't create a ticket without a queue.
1049 unless ( defined($QueueObj) and $QueueObj->Id ) {
1050 $RT::Logger->debug("$self No queue given for ticket creation.");
1051 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1054 #Now that we have a queue, Check the ACLS
1056 $self->CurrentUser->HasRight(
1057 Right => 'CreateTicket',
1063 $self->loc("No permission to create tickets in the queue '[_1]'"
1064 , $QueueObj->Name));
1067 # {{{ Deal with setting the owner
1069 # Attempt to take user object, user name or user id.
1070 # Assign to nobody if lookup fails.
1071 if ( defined( $args{'Owner'} ) ) {
1072 if ( ref( $args{'Owner'} ) ) {
1073 $Owner = $args{'Owner'};
1076 $Owner = new RT::User( $self->CurrentUser );
1077 $Owner->Load( $args{'Owner'} );
1078 if ( !defined( $Owner->id ) ) {
1079 $Owner->Load( $RT::Nobody->id );
1084 #If we have a proposed owner and they don't have the right
1085 #to own a ticket, scream about it and make them not the owner
1088 and ( $Owner->Id != $RT::Nobody->Id )
1091 Object => $QueueObj,
1092 Right => 'OwnTicket'
1098 $RT::Logger->warning( "$self user "
1099 . $Owner->Name . "("
1102 . "as a ticket owner but has no rights to own "
1104 . $QueueObj->Name . "'\n" );
1109 #If we haven't been handed a valid owner, make it nobody.
1110 unless ( defined($Owner) ) {
1111 $Owner = new RT::User( $self->CurrentUser );
1112 $Owner->Load( $RT::Nobody->UserObj->Id );
1117 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1118 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1121 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1122 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1123 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1124 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1126 # If we're coming in with an id, set that now.
1127 my $EffectiveId = undef;
1128 if ( $args{'id'} ) {
1129 $EffectiveId = $args{'id'};
1133 my $id = $self->SUPER::Create(
1135 EffectiveId => $EffectiveId,
1136 Queue => $QueueObj->Id,
1137 Owner => $Owner->Id,
1138 Subject => $args{'Subject'}, # loc
1139 InitialPriority => $args{'InitialPriority'}, # loc
1140 FinalPriority => $args{'FinalPriority'}, # loc
1141 Priority => $args{'InitialPriority'}, # loc
1142 Status => $args{'Status'}, # loc
1143 TimeWorked => $args{'TimeWorked'}, # loc
1144 Type => $args{'Type'}, # loc
1145 Created => $args{'Created'}, # loc
1146 Told => $args{'Told'}, # loc
1147 LastUpdated => $args{'Updated'}, # loc
1148 Resolved => $args{'Resolved'}, # loc
1149 Due => $args{'Due'}, # loc
1152 # If the ticket didn't have an id
1153 # Set the ticket's effective ID now that we've created it.
1154 if ( $args{'id'} ) {
1155 $self->Load( $args{'id'} );
1159 $self->__Set( Field => 'EffectiveId', Value => $id );
1163 $self . "->Import couldn't set EffectiveId: $msg\n" );
1167 my $create_groups_ret = $self->_CreateTicketGroups();
1168 unless ($create_groups_ret) {
1170 "Couldn't create ticket groups for ticket " . $self->Id );
1173 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1176 foreach $watcher ( @{ $args{'Cc'} } ) {
1177 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1179 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1180 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1183 foreach $watcher ( @{ $args{'Requestor'} } ) {
1184 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1188 return ( $self->Id, $ErrStr );
1193 # {{{ Routines dealing with watchers.
1195 # {{{ _CreateTicketGroups
1197 =head2 _CreateTicketGroups
1199 Create the ticket groups and links for this ticket.
1200 This routine expects to be called from Ticket->Create _inside of a transaction_
1202 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1204 It will return true on success and undef on failure.
1208 my $ticket = RT::Ticket->new($RT::SystemUser);
1209 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1210 Owner => $RT::SystemUser->Id,
1212 Requestor => ['jesse@example.com'],
1215 ok ($id, "Ticket $id was created");
1216 ok(my $group = RT::Group->new($RT::SystemUser));
1217 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1218 ok ($group->Id, "Found the requestors object for this ticket");
1220 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1221 $jesse->LoadByEmail('jesse@example.com');
1222 ok($jesse->Id, "Found the jesse rt user");
1225 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1226 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1227 ok ($add_id, "Add succeeded: ($add_msg)");
1228 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1229 $bob->LoadByEmail('bob@fsck.com');
1230 ok($bob->Id, "Found the bob rt user");
1231 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1232 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1233 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1236 $group = RT::Group->new($RT::SystemUser);
1237 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1238 ok ($group->Id, "Found the cc object for this ticket");
1239 $group = RT::Group->new($RT::SystemUser);
1240 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1241 ok ($group->Id, "Found the AdminCc object for this ticket");
1242 $group = RT::Group->new($RT::SystemUser);
1243 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1244 ok ($group->Id, "Found the Owner object for this ticket");
1245 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1252 sub _CreateTicketGroups {
1255 my @types = qw(Requestor Owner Cc AdminCc);
1257 foreach my $type (@types) {
1258 my $type_obj = RT::Group->new($self->CurrentUser);
1259 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1260 Instance => $self->Id,
1263 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1264 $self->Id.": ".$msg);
1274 # {{{ sub OwnerGroup
1278 A constructor which returns an RT::Group object containing the owner of this ticket.
1284 my $owner_obj = RT::Group->new($self->CurrentUser);
1285 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1286 return ($owner_obj);
1292 # {{{ sub AddWatcher
1296 AddWatcher takes a parameter hash. The keys are as follows:
1298 Type One of Requestor, Cc, AdminCc
1300 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1302 Email The email address of the new watcher. If a user with this
1303 email address can't be found, a new nonprivileged user will be created.
1305 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.
1313 PrincipalId => undef,
1318 # XXX, FIXME, BUG: if only email is provided then we only check
1319 # for ModifyTicket right, but must try to get PrincipalId and
1320 # check Watch* rights too if user exist
1323 #If the watcher we're trying to add is for the current user
1324 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1325 # If it's an AdminCc and they don't have
1326 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1327 if ( $args{'Type'} eq 'AdminCc' ) {
1328 unless ( $self->CurrentUserHasRight('ModifyTicket')
1329 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1330 return ( 0, $self->loc('Permission Denied'))
1334 # If it's a Requestor or Cc and they don't have
1335 # 'Watch' or 'ModifyTicket', bail
1336 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1338 unless ( $self->CurrentUserHasRight('ModifyTicket')
1339 or $self->CurrentUserHasRight('Watch') ) {
1340 return ( 0, $self->loc('Permission Denied'))
1344 $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1345 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1349 # If the watcher isn't the current user
1350 # and the current user doesn't have 'ModifyTicket'
1353 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1354 return ( 0, $self->loc("Permission Denied") );
1360 return ( $self->_AddWatcher(%args) );
1363 #This contains the meat of AddWatcher. but can be called from a routine like
1364 # Create, which doesn't need the additional acl check
1370 PrincipalId => undef,
1376 my $principal = RT::Principal->new($self->CurrentUser);
1377 if ($args{'Email'}) {
1378 my $user = RT::User->new($RT::SystemUser);
1379 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1380 # If we can't load the user by email address, let's try to load by username
1382 ($pid,$msg) = $user->Load($args{'Email'})
1385 $args{'PrincipalId'} = $pid;
1388 if ($args{'PrincipalId'}) {
1389 $principal->Load($args{'PrincipalId'});
1393 # If we can't find this watcher, we need to bail.
1394 unless ($principal->Id) {
1395 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1396 return(0, $self->loc("Could not find or create that user"));
1400 my $group = RT::Group->new($self->CurrentUser);
1401 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1402 unless ($group->id) {
1403 return(0,$self->loc("Group not found"));
1406 if ( $group->HasMember( $principal)) {
1408 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1412 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1413 InsideTransaction => 1 );
1415 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1417 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1420 unless ( $args{'Silent'} ) {
1421 $self->_NewTransaction(
1422 Type => 'AddWatcher',
1423 NewValue => $principal->Id,
1424 Field => $args{'Type'}
1428 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1434 # {{{ sub DeleteWatcher
1436 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1439 Deletes a Ticket watcher. Takes two arguments:
1441 Type (one of Requestor,Cc,AdminCc)
1445 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1447 Email (the email address of an existing wathcer)
1456 my %args = ( Type => undef,
1457 PrincipalId => undef,
1461 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1462 return ( 0, $self->loc("No principal specified") );
1464 my $principal = RT::Principal->new( $self->CurrentUser );
1465 if ( $args{'PrincipalId'} ) {
1467 $principal->Load( $args{'PrincipalId'} );
1470 my $user = RT::User->new( $self->CurrentUser );
1471 $user->LoadByEmail( $args{'Email'} );
1472 $principal->Load( $user->Id );
1475 # If we can't find this watcher, we need to bail.
1476 unless ( $principal->Id ) {
1477 return ( 0, $self->loc("Could not find that principal") );
1480 my $group = RT::Group->new( $self->CurrentUser );
1481 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1482 unless ( $group->id ) {
1483 return ( 0, $self->loc("Group not found") );
1487 #If the watcher we're trying to add is for the current user
1488 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) {
1490 # If it's an AdminCc and they don't have
1491 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1492 if ( $args{'Type'} eq 'AdminCc' ) {
1493 unless ( $self->CurrentUserHasRight('ModifyTicket')
1494 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1495 return ( 0, $self->loc('Permission Denied') );
1499 # If it's a Requestor or Cc and they don't have
1500 # 'Watch' or 'ModifyTicket', bail
1501 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1503 unless ( $self->CurrentUserHasRight('ModifyTicket')
1504 or $self->CurrentUserHasRight('Watch') ) {
1505 return ( 0, $self->loc('Permission Denied') );
1509 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1511 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1515 # If the watcher isn't the current user
1516 # and the current user doesn't have 'ModifyTicket' bail
1518 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1519 return ( 0, $self->loc("Permission Denied") );
1525 # see if this user is already a watcher.
1527 unless ( $group->HasMember($principal) ) {
1529 $self->loc( 'That principal is not a [_1] for this ticket',
1533 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1535 $RT::Logger->error( "Failed to delete "
1537 . " as a member of group "
1543 'Could not remove that principal as a [_1] for this ticket',
1547 unless ( $args{'Silent'} ) {
1548 $self->_NewTransaction( Type => 'DelWatcher',
1549 OldValue => $principal->Id,
1550 Field => $args{'Type'} );
1554 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1555 $principal->Object->Name,
1564 =head2 SquelchMailTo [EMAIL]
1566 Takes an optional email address to never email about updates to this ticket.
1569 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1573 my $t = RT::Ticket->new($RT::SystemUser);
1574 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1576 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1578 my @returned = $t->SquelchMailTo('nobody@example.com');
1580 is($#returned, 0, "The ticket has one squelched recipients");
1582 my @names = $t->Attributes->Names;
1583 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1584 @returned = $t->SquelchMailTo('nobody@example.com');
1587 is($#returned, 0, "The ticket has one squelched recipients");
1589 @names = $t->Attributes->Names;
1590 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1593 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1594 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1595 @returned = $t->SquelchMailTo();
1596 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1606 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1610 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1611 unless grep { $_->Content eq $attr }
1612 $self->Attributes->Named('SquelchMailTo');
1615 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1618 my @attributes = $self->Attributes->Named('SquelchMailTo');
1619 return (@attributes);
1623 =head2 UnsquelchMailTo ADDRESS
1625 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1627 Returns a tuple of (status, message)
1631 sub UnsquelchMailTo {
1634 my $address = shift;
1635 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1636 return ( 0, $self->loc("Permission Denied") );
1639 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1640 return ($val, $msg);
1644 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1646 =head2 RequestorAddresses
1648 B<Returns> String: All Ticket Requestor email addresses as a string.
1652 sub RequestorAddresses {
1655 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1659 return ( $self->Requestors->MemberEmailAddressesAsString );
1663 =head2 AdminCcAddresses
1665 returns String: All Ticket AdminCc email addresses as a string
1669 sub AdminCcAddresses {
1672 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1676 return ( $self->AdminCc->MemberEmailAddressesAsString )
1682 returns String: All Ticket Ccs as a string of email addresses
1689 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1693 return ( $self->Cc->MemberEmailAddressesAsString);
1699 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1701 # {{{ sub Requestors
1706 Returns this ticket's Requestors as an RT::Group object
1713 my $group = RT::Group->new($self->CurrentUser);
1714 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1715 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1728 Returns an RT::Group object which contains this ticket's Ccs.
1729 If the user doesn't have "ShowTicket" permission, returns an empty group
1736 my $group = RT::Group->new($self->CurrentUser);
1737 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1738 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1751 Returns an RT::Group object which contains this ticket's AdminCcs.
1752 If the user doesn't have "ShowTicket" permission, returns an empty group
1759 my $group = RT::Group->new($self->CurrentUser);
1760 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1761 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1771 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1774 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1776 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1778 Takes a param hash with the attributes Type and either PrincipalId or Email
1780 Type is one of Requestor, Cc, AdminCc and Owner
1782 PrincipalId is an RT::Principal id, and Email is an email address.
1784 Returns true if the specified principal (or the one corresponding to the
1785 specified address) is a member of the group Type for this ticket.
1787 XX TODO: This should be Memoized.
1794 my %args = ( Type => 'Requestor',
1795 PrincipalId => undef,
1800 # Load the relevant group.
1801 my $group = RT::Group->new($self->CurrentUser);
1802 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1804 # Find the relevant principal.
1805 my $principal = RT::Principal->new($self->CurrentUser);
1806 if (!$args{PrincipalId} && $args{Email}) {
1807 # Look up the specified user.
1808 my $user = RT::User->new($self->CurrentUser);
1809 $user->LoadByEmail($args{Email});
1811 $args{PrincipalId} = $user->PrincipalId;
1814 # A non-existent user can't be a group member.
1818 $principal->Load($args{'PrincipalId'});
1820 # Ask if it has the member in question
1821 return ($group->HasMember($principal));
1826 # {{{ sub IsRequestor
1828 =head2 IsRequestor PRINCIPAL_ID
1830 Takes an RT::Principal id
1831 Returns true if the principal is a requestor of the current ticket.
1840 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1848 =head2 IsCc 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 => 'Cc', PrincipalId => $cc ) );
1868 =head2 IsAdminCc PRINCIPAL_ID
1870 Takes an RT::Principal id.
1871 Returns true if the principal is a requestor of the current ticket.
1879 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1889 Takes an RT::User object. Returns true if that user is this ticket's owner.
1890 returns undef otherwise
1898 # no ACL check since this is used in acl decisions
1899 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1903 #Tickets won't yet have owners when they're being created.
1904 unless ( $self->OwnerObj->id ) {
1908 if ( $person->id == $self->OwnerObj->id ) {
1922 # {{{ Routines dealing with queues
1924 # {{{ sub ValidateQueue
1931 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1935 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1936 my $id = $QueueObj->Load($Value);
1952 my $NewQueue = shift;
1954 #Redundant. ACL gets checked in _Set;
1955 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1956 return ( 0, $self->loc("Permission Denied") );
1959 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1960 $NewQueueObj->Load($NewQueue);
1962 unless ( $NewQueueObj->Id() ) {
1963 return ( 0, $self->loc("That queue does not exist") );
1966 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1967 return ( 0, $self->loc('That is the same value') );
1970 $self->CurrentUser->HasRight(
1971 Right => 'CreateTicket',
1972 Object => $NewQueueObj
1976 return ( 0, $self->loc("You may not create requests in that queue.") );
1980 $self->OwnerObj->HasRight(
1981 Right => 'OwnTicket',
1982 Object => $NewQueueObj
1986 my $clone = RT::Ticket->new( $RT::SystemUser );
1987 $clone->Load( $self->Id );
1988 unless ( $clone->Id ) {
1989 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1991 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1992 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1995 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2004 Takes nothing. returns this ticket's queue object
2011 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2013 #We call __Value so that we can avoid the ACL decision and some deep recursion
2014 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2015 return ($queue_obj);
2022 # {{{ Date printing routines
2028 Returns an RT::Date object containing this ticket's due date
2035 my $time = new RT::Date( $self->CurrentUser );
2037 # -1 is RT::Date slang for never
2039 $time->Set( Format => 'sql', Value => $self->Due );
2042 $time->Set( Format => 'unix', Value => -1 );
2050 # {{{ sub DueAsString
2054 Returns this ticket's due date as a human readable string
2060 return $self->DueObj->AsString();
2065 # {{{ sub ResolvedObj
2069 Returns an RT::Date object of this ticket's 'resolved' time.
2076 my $time = new RT::Date( $self->CurrentUser );
2077 $time->Set( Format => 'sql', Value => $self->Resolved );
2083 # {{{ sub SetStarted
2087 Takes a date in ISO format or undef
2088 Returns a transaction id and a message
2089 The client calls "Start" to note that the project was started on the date in $date.
2090 A null date means "now"
2096 my $time = shift || 0;
2098 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2099 return ( 0, self->loc("Permission Denied") );
2102 #We create a date object to catch date weirdness
2103 my $time_obj = new RT::Date( $self->CurrentUser() );
2105 $time_obj->Set( Format => 'ISO', Value => $time );
2108 $time_obj->SetToNow();
2111 #Now that we're starting, open this ticket
2112 #TODO do we really want to force this as policy? it should be a scrip
2114 #We need $TicketAsSystem, in case the current user doesn't have
2117 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2118 $TicketAsSystem->Load( $self->Id );
2119 if ( $TicketAsSystem->Status eq 'new' ) {
2120 $TicketAsSystem->Open();
2123 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2129 # {{{ sub StartedObj
2133 Returns an RT::Date object which contains this ticket's
2141 my $time = new RT::Date( $self->CurrentUser );
2142 $time->Set( Format => 'sql', Value => $self->Started );
2152 Returns an RT::Date object which contains this ticket's
2160 my $time = new RT::Date( $self->CurrentUser );
2161 $time->Set( Format => 'sql', Value => $self->Starts );
2171 Returns an RT::Date object which contains this ticket's
2179 my $time = new RT::Date( $self->CurrentUser );
2180 $time->Set( Format => 'sql', Value => $self->Told );
2186 # {{{ sub ToldAsString
2190 A convenience method that returns ToldObj->AsString
2192 TODO: This should be deprecated
2198 if ( $self->Told ) {
2199 return $self->ToldObj->AsString();
2208 # {{{ sub TimeWorkedAsString
2210 =head2 TimeWorkedAsString
2212 Returns the amount of time worked on this ticket as a Text String
2216 sub TimeWorkedAsString {
2218 return "0" unless $self->TimeWorked;
2220 #This is not really a date object, but if we diff a number of seconds
2221 #vs the epoch, we'll get a nice description of time worked.
2223 my $worked = new RT::Date( $self->CurrentUser );
2225 #return the #of minutes worked turned into seconds and written as
2226 # a simple text string
2228 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2235 # {{{ Routines dealing with correspondence/comments
2241 Comment on this ticket.
2242 Takes a hashref with the following attributes:
2243 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2246 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2248 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2249 They will, however, be prepared and you'll be able to access them through the TransactionObj
2251 Returns: Transaction id, Error Message, Transaction Object
2252 (note the different order from Create()!)
2259 my %args = ( CcMessageTo => undef,
2260 BccMessageTo => undef,
2267 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2268 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2269 return ( 0, $self->loc("Permission Denied"), undef );
2271 $args{'NoteType'} = 'Comment';
2273 if ($args{'DryRun'}) {
2274 $RT::Handle->BeginTransaction();
2275 $args{'CommitScrips'} = 0;
2278 my @results = $self->_RecordNote(%args);
2279 if ($args{'DryRun'}) {
2280 $RT::Handle->Rollback();
2287 # {{{ sub Correspond
2291 Correspond on this ticket.
2292 Takes a hashref with the following attributes:
2295 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2297 if there's no MIMEObj, Content is used to build a MIME::Entity object
2299 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2300 They will, however, be prepared and you'll be able to access them through the TransactionObj
2302 Returns: Transaction id, Error Message, Transaction Object
2303 (note the different order from Create()!)
2310 my %args = ( CcMessageTo => undef,
2311 BccMessageTo => undef,
2317 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2318 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2319 return ( 0, $self->loc("Permission Denied"), undef );
2322 $args{'NoteType'} = 'Correspond';
2323 if ($args{'DryRun'}) {
2324 $RT::Handle->BeginTransaction();
2325 $args{'CommitScrips'} = 0;
2328 my @results = $self->_RecordNote(%args);
2330 #Set the last told date to now if this isn't mail from the requestor.
2331 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2332 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2334 if ($args{'DryRun'}) {
2335 $RT::Handle->Rollback();
2344 # {{{ sub _RecordNote
2348 the meat of both comment and correspond.
2350 Performs no access control checks. hence, dangerous.
2357 my %args = ( CcMessageTo => undef,
2358 BccMessageTo => undef,
2365 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2366 return ( 0, $self->loc("No message attached"), undef );
2368 unless ( $args{'MIMEObj'} ) {
2369 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2370 ref $args{'Content'}
2372 : [ $args{'Content'} ]
2376 # convert text parts into utf-8
2377 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2379 # If we've been passed in CcMessageTo and BccMessageTo fields,
2380 # add them to the mime object for passing on to the transaction handler
2381 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2384 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2385 undef, $args{'CcMessageTo'}
2387 if defined $args{'CcMessageTo'};
2388 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2389 RT::User::CanonicalizeEmailAddress(
2390 undef, $args{'BccMessageTo'}
2392 if defined $args{'BccMessageTo'};
2394 # If this is from an external source, we need to come up with its
2395 # internal Message-ID now, so all emails sent because of this
2396 # message have a common Message-ID
2397 unless ($args{'MIMEObj'}->head->get('Message-ID')
2398 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) {
2399 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2401 . $RT::VERSION . "-"
2403 . CORE::time() . "-"
2404 . int(rand(2000)) . '.'
2407 . "0" . "@" # Email sent
2412 #Record the correspondence (write the transaction)
2413 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2414 Type => $args{'NoteType'},
2415 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2416 TimeTaken => $args{'TimeTaken'},
2417 MIMEObj => $args{'MIMEObj'},
2418 CommitScrips => $args{'CommitScrips'},
2422 $RT::Logger->err("$self couldn't init a transaction $msg");
2423 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2426 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2438 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2441 my $type = shift || "";
2443 unless ( $self->{"$field$type"} ) {
2444 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2445 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2446 # Maybe this ticket is a merged ticket
2447 my $Tickets = new RT::Tickets( $self->CurrentUser );
2448 # at least to myself
2449 $self->{"$field$type"}->Limit( FIELD => $field,
2450 VALUE => $self->URI,
2451 ENTRYAGGREGATOR => 'OR' );
2452 $Tickets->Limit( FIELD => 'EffectiveId',
2453 VALUE => $self->EffectiveId );
2454 while (my $Ticket = $Tickets->Next) {
2455 $self->{"$field$type"}->Limit( FIELD => $field,
2456 VALUE => $Ticket->URI,
2457 ENTRYAGGREGATOR => 'OR' );
2459 $self->{"$field$type"}->Limit( FIELD => 'Type',
2464 return ( $self->{"$field$type"} );
2469 # {{{ sub DeleteLink
2473 Delete a link. takes a paramhash of Base, Target and Type.
2474 Either Base or Target must be null. The null value will
2475 be replaced with this ticket\'s id
2489 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2490 $RT::Logger->debug("No permission to delete links\n");
2491 return ( 0, $self->loc('Permission Denied'))
2495 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2498 $RT::Logger->debug("Couldn't find that link\n");
2502 my ($direction, $remote_link);
2504 if ( $args{'Base'} ) {
2505 $remote_link = $args{'Base'};
2506 $direction = 'Target';
2508 elsif ( $args{'Target'} ) {
2509 $remote_link = $args{'Target'};
2513 if ( $args{'Silent'} ) {
2514 return ( $val, $Msg );
2517 my $remote_uri = RT::URI->new( $self->CurrentUser );
2518 $remote_uri->FromURI( $remote_link );
2520 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2521 Type => 'DeleteLink',
2522 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2523 OldValue => $remote_uri->URI || $remote_link,
2527 if ( $remote_uri->IsLocal ) {
2529 my $OtherObj = $remote_uri->Object;
2530 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2531 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2532 : $LINKDIRMAP{$args{'Type'}}->{Target},
2533 OldValue => $self->URI,
2534 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2538 return ( $Trans, $Msg );
2548 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2554 my %args = ( Target => '',
2561 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2562 return ( 0, $self->loc("Permission Denied") );
2566 $self->_AddLink(%args);
2571 Private non-acled variant of AddLink so that links can be added during create.
2577 my %args = ( Target => '',
2583 # {{{ If the other URI is an RT::Ticket, we want to make sure the user
2584 # can modify it too...
2585 my $other_ticket_uri = RT::URI->new($self->CurrentUser);
2587 if ( $args{'Target'} ) {
2588 $other_ticket_uri->FromURI( $args{'Target'} );
2591 elsif ( $args{'Base'} ) {
2592 $other_ticket_uri->FromURI( $args{'Base'} );
2595 unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) {
2596 my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'})
2597 : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
2598 $RT::Logger->warning( "$self $msg\n" );
2603 if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') {
2604 my $object = $other_ticket_uri->Resolver->Object;
2606 if ( UNIVERSAL::isa( $object, 'RT::Ticket' )
2608 && !$object->CurrentUserHasRight('ModifyTicket') )
2610 return ( 0, $self->loc("Permission Denied") );
2617 my ($val, $Msg) = $self->SUPER::_AddLink(%args);
2620 return ($val, $Msg);
2623 my ($direction, $remote_link);
2624 if ( $args{'Target'} ) {
2625 $remote_link = $args{'Target'};
2626 $direction = 'Base';
2627 } elsif ( $args{'Base'} ) {
2628 $remote_link = $args{'Base'};
2629 $direction = 'Target';
2632 # Don't write the transaction if we're doing this on create
2633 if ( $args{'Silent'} ) {
2634 return ( $val, $Msg );
2637 my $remote_uri = RT::URI->new( $self->CurrentUser );
2638 $remote_uri->FromURI( $remote_link );
2640 #Write the transaction
2641 my ( $Trans, $Msg, $TransObj ) =
2642 $self->_NewTransaction(Type => 'AddLink',
2643 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2644 NewValue => $remote_uri->URI || $remote_link,
2647 if ( $remote_uri->IsLocal ) {
2649 my $OtherObj = $remote_uri->Object;
2650 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2651 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2652 : $LINKDIRMAP{$args{'Type'}}->{Target},
2653 NewValue => $self->URI,
2654 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2657 return ( $val, $Msg );
2669 MergeInto take the id of the ticket to merge this ticket into.
2674 my $t1 = RT::Ticket->new($RT::SystemUser);
2675 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2677 my $t2 = RT::Ticket->new($RT::SystemUser);
2678 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2680 my ($msg, $val) = $t1->MergeInto($t2->id);
2682 $t1 = RT::Ticket->new($RT::SystemUser);
2683 is ($t1->id, undef, "ok. we've got a blank ticket1");
2686 is ($t1->id, $t2->id);
2688 is ($t1->Requestors->MembersObj->Count, 2);
2697 my $ticket_id = shift;
2699 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2700 return ( 0, $self->loc("Permission Denied") );
2703 # Load up the new ticket.
2704 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2705 $MergeInto->Load($ticket_id);
2707 # make sure it exists.
2708 unless ( $MergeInto->Id ) {
2709 return ( 0, $self->loc("New ticket doesn't exist") );
2712 # Make sure the current user can modify the new ticket.
2713 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2714 return ( 0, $self->loc("Permission Denied") );
2717 $RT::Handle->BeginTransaction();
2719 # We use EffectiveId here even though it duplicates information from
2720 # the links table becasue of the massive performance hit we'd take
2721 # by trying to do a separate database query for merge info everytime
2724 #update this ticket's effective id to the new ticket's id.
2725 my ( $id_val, $id_msg ) = $self->__Set(
2726 Field => 'EffectiveId',
2727 Value => $MergeInto->Id()
2731 $RT::Handle->Rollback();
2732 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2735 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2737 unless ($status_val) {
2738 $RT::Handle->Rollback();
2739 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2740 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2744 # update all the links that point to that old ticket
2745 my $old_links_to = RT::Links->new($self->CurrentUser);
2746 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2749 while (my $link = $old_links_to->Next) {
2750 if (exists $old_seen{$link->Base."-".$link->Type}) {
2753 elsif ($link->Base eq $MergeInto->URI) {
2756 # First, make sure the link doesn't already exist. then move it over.
2757 my $tmp = RT::Link->new($RT::SystemUser);
2758 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2762 $link->SetTarget($MergeInto->URI);
2763 $link->SetLocalTarget($MergeInto->id);
2765 $old_seen{$link->Base."-".$link->Type} =1;
2770 my $old_links_from = RT::Links->new($self->CurrentUser);
2771 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2773 while (my $link = $old_links_from->Next) {
2774 if (exists $old_seen{$link->Type."-".$link->Target}) {
2777 if ($link->Target eq $MergeInto->URI) {
2780 # First, make sure the link doesn't already exist. then move it over.
2781 my $tmp = RT::Link->new($RT::SystemUser);
2782 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2786 $link->SetBase($MergeInto->URI);
2787 $link->SetLocalBase($MergeInto->id);
2788 $old_seen{$link->Type."-".$link->Target} =1;
2794 # Update time fields
2795 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2797 my $mutator = "Set$type";
2798 $MergeInto->$mutator(
2799 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2802 #add all of this ticket's watchers to that ticket.
2803 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2805 my $people = $self->$watcher_type->MembersObj;
2806 my $addwatcher_type = $watcher_type;
2807 $addwatcher_type =~ s/s$//;
2809 while ( my $watcher = $people->Next ) {
2811 my ($val, $msg) = $MergeInto->_AddWatcher(
2812 Type => $addwatcher_type,
2814 PrincipalId => $watcher->MemberId
2817 $RT::Logger->warning($msg);
2823 #find all of the tickets that were merged into this ticket.
2824 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2825 $old_mergees->Limit(
2826 FIELD => 'EffectiveId',
2831 # update their EffectiveId fields to the new ticket's id
2832 while ( my $ticket = $old_mergees->Next() ) {
2833 my ( $val, $msg ) = $ticket->__Set(
2834 Field => 'EffectiveId',
2835 Value => $MergeInto->Id()
2839 #make a new link: this ticket is merged into that other ticket.
2840 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2842 $MergeInto->_SetLastUpdated;
2844 $RT::Handle->Commit();
2845 return ( 1, $self->loc("Merge Successful") );
2852 # {{{ Routines dealing with ownership
2858 Takes nothing and returns an RT::User object of
2866 #If this gets ACLed, we lose on a rights check in User.pm and
2867 #get deep recursion. if we need ACLs here, we need
2868 #an equiv without ACLs
2870 my $owner = new RT::User( $self->CurrentUser );
2871 $owner->Load( $self->__Value('Owner') );
2873 #Return the owner object
2879 # {{{ sub OwnerAsString
2881 =head2 OwnerAsString
2883 Returns the owner's email address
2889 return ( $self->OwnerObj->EmailAddress );
2899 Takes two arguments:
2900 the Id or Name of the owner
2901 and (optionally) the type of the SetOwner Transaction. It defaults
2902 to 'Give'. 'Steal' is also a valid option.
2906 my $root = RT::User->new($RT::SystemUser);
2907 $root->Load('root');
2908 ok ($root->Id, "Loaded the root user");
2909 my $t = RT::Ticket->new($RT::SystemUser);
2911 $t->SetOwner('root');
2912 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2914 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2915 my $txns = RT::Transactions->new($RT::SystemUser);
2916 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2917 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2918 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2919 my $steal = $txns->First;
2920 ok($steal->OldValue == $root->Id , "Stolen from root");
2921 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2929 my $NewOwner = shift;
2930 my $Type = shift || "Give";
2932 # must have ModifyTicket rights
2933 # or TakeTicket/StealTicket and $NewOwner is self
2934 # see if it's a take
2935 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
2936 unless ( $self->CurrentUserHasRight('ModifyTicket')
2937 || $self->CurrentUserHasRight('TakeTicket') ) {
2938 return ( 0, $self->loc("Permission Denied") );
2942 # see if it's a steal
2943 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
2944 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
2946 unless ( $self->CurrentUserHasRight('ModifyTicket')
2947 || $self->CurrentUserHasRight('StealTicket') ) {
2948 return ( 0, $self->loc("Permission Denied") );
2952 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2953 return ( 0, $self->loc("Permission Denied") );
2956 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2957 my $OldOwnerObj = $self->OwnerObj;
2959 $NewOwnerObj->Load($NewOwner);
2960 if ( !$NewOwnerObj->Id ) {
2961 return ( 0, $self->loc("That user does not exist") );
2964 #If thie ticket has an owner and it's not the current user
2966 if ( ( $Type ne 'Steal' )
2967 and ( $Type ne 'Force' )
2968 and #If we're not stealing
2969 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
2970 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
2971 ) { #and it's not us
2974 "You can only reassign tickets that you own or that are unowned" ) );
2977 #If we've specified a new owner and that user can't modify the ticket
2978 elsif ( ( $NewOwnerObj->Id )
2979 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
2982 return ( 0, $self->loc("That user may not own tickets in that queue") );
2985 #If the ticket has an owner and it's the new owner, we don't need
2987 elsif ( ( $self->OwnerObj )
2988 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
2989 return ( 0, $self->loc("That user already owns that ticket") );
2992 $RT::Handle->BeginTransaction();
2994 # Delete the owner in the owner group, then add a new one
2995 # TODO: is this safe? it's not how we really want the API to work
2996 # for most things, but it's fast.
2997 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
2999 $RT::Handle->Rollback();
3000 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3003 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3004 PrincipalId => $NewOwnerObj->PrincipalId,
3005 InsideTransaction => 1 );
3007 $RT::Handle->Rollback();
3008 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3011 # We call set twice with slightly different arguments, so
3012 # as to not have an SQL transaction span two RT transactions
3014 my ( $val, $msg ) = $self->_Set(
3016 RecordTransaction => 0,
3017 Value => $NewOwnerObj->Id,
3019 TransactionType => $Type,
3020 CheckACL => 0, # don't check acl
3024 $RT::Handle->Rollback;
3025 return ( 0, $self->loc("Could not change owner. ") . $msg );
3028 $RT::Handle->Commit();
3030 ($val, $msg) = $self->_NewTransaction(
3033 NewValue => $NewOwnerObj->Id,
3034 OldValue => $OldOwnerObj->Id,
3039 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3040 $OldOwnerObj->Name, $NewOwnerObj->Name );
3042 # TODO: make sure the trans committed properly
3044 return ( $val, $msg );
3053 A convenince method to set the ticket's owner to the current user
3059 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3068 Convenience method to set the owner to 'nobody' if the current user is the owner.
3074 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3083 A convenience method to change the owner of the current ticket to the
3084 current user. Even if it's owned by another user.
3091 if ( $self->IsOwner( $self->CurrentUser ) ) {
3092 return ( 0, $self->loc("You already own this ticket") );
3095 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3105 # {{{ Routines dealing with status
3107 # {{{ sub ValidateStatus
3109 =head2 ValidateStatus STATUS
3111 Takes a string. Returns true if that status is a valid status for this ticket.
3112 Returns false otherwise.
3116 sub ValidateStatus {
3120 #Make sure the status passed in is valid
3121 unless ( $self->QueueObj->IsValidStatus($status) ) {
3133 =head2 SetStatus STATUS
3135 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3137 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.
3141 my $tt = RT::Ticket->new($RT::SystemUser);
3142 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3145 is($tt->Status, 'new', "New ticket is created as new");
3147 ($id, $msg) = $tt->SetStatus('open');
3149 like($msg, qr/open/i, "Status message is correct");
3150 ($id, $msg) = $tt->SetStatus('resolved');
3152 like($msg, qr/resolved/i, "Status message is correct");
3153 ($id, $msg) = $tt->SetStatus('resolved');
3167 $args{Status} = shift;
3174 if ( $args{Status} eq 'deleted') {
3175 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3176 return ( 0, $self->loc('Permission Denied') );
3179 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3180 return ( 0, $self->loc('Permission Denied') );
3184 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3185 return (0, $self->loc('That ticket has unresolved dependencies'));
3188 my $now = RT::Date->new( $self->CurrentUser );
3191 #If we're changing the status from new, record that we've started
3192 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3194 #Set the Started time to "now"
3195 $self->_Set( Field => 'Started',
3197 RecordTransaction => 0 );
3200 #When we close a ticket, set the 'Resolved' attribute to now.
3201 # It's misnamed, but that's just historical.
3202 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3203 $self->_Set( Field => 'Resolved',
3205 RecordTransaction => 0 );
3208 #Actually update the status
3209 my ($val, $msg)= $self->_Set( Field => 'Status',
3210 Value => $args{Status},
3213 TransactionType => 'Status' );
3224 Takes no arguments. Marks this ticket for garbage collection
3230 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3231 return $self->Delete;
3236 return ( $self->SetStatus('deleted') );
3238 # TODO: garbage collection
3247 Sets this ticket's status to stalled
3253 return ( $self->SetStatus('stalled') );
3262 Sets this ticket's status to rejected
3268 return ( $self->SetStatus('rejected') );
3277 Sets this ticket\'s status to Open
3283 return ( $self->SetStatus('open') );
3292 Sets this ticket\'s status to Resolved
3298 return ( $self->SetStatus('resolved') );
3306 # {{{ Actions + Routines dealing with transactions
3308 # {{{ sub SetTold and _SetTold
3310 =head2 SetTold ISO [TIMETAKEN]
3312 Updates the told and records a transaction
3319 $told = shift if (@_);
3320 my $timetaken = shift || 0;
3322 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3323 return ( 0, $self->loc("Permission Denied") );
3326 my $datetold = new RT::Date( $self->CurrentUser );
3328 $datetold->Set( Format => 'iso',
3332 $datetold->SetToNow();
3335 return ( $self->_Set( Field => 'Told',
3336 Value => $datetold->ISO,
3337 TimeTaken => $timetaken,
3338 TransactionType => 'Told' ) );
3343 Updates the told without a transaction or acl check. Useful when we're sending replies.
3350 my $now = new RT::Date( $self->CurrentUser );
3353 #use __Set to get no ACLs ;)
3354 return ( $self->__Set( Field => 'Told',
3355 Value => $now->ISO ) );
3360 =head2 TransactionBatch
3362 Returns an array reference of all transactions created on this ticket during
3363 this ticket object's lifetime, or undef if there were none.
3365 Only works when the $RT::UseTransactionBatch config variable is set to true.
3369 sub TransactionBatch {
3371 return $self->{_TransactionBatch};
3377 # DESTROY methods need to localize $@, or it may unset it. This
3378 # causes $m->abort to not bubble all of the way up. See perlbug
3379 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3382 # The following line eliminates reentrancy.
3383 # It protects against the fact that perl doesn't deal gracefully
3384 # when an object's refcount is changed in its destructor.
3385 return if $self->{_Destroyed}++;
3387 my $batch = $self->TransactionBatch or return;
3389 RT::Scrips->new($RT::SystemUser)->Apply(
3390 Stage => 'TransactionBatch',
3392 TransactionObj => $batch->[0],
3393 Type => join(',', (map { $_->Type } @{$batch}) )
3399 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3401 # {{{ sub _OverlayAccessible
3403 sub _OverlayAccessible {
3405 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3406 Queue => { 'read' => 1, 'write' => 1 },
3407 Requestors => { 'read' => 1, 'write' => 1 },
3408 Owner => { 'read' => 1, 'write' => 1 },
3409 Subject => { 'read' => 1, 'write' => 1 },
3410 InitialPriority => { 'read' => 1, 'write' => 1 },
3411 FinalPriority => { 'read' => 1, 'write' => 1 },
3412 Priority => { 'read' => 1, 'write' => 1 },
3413 Status => { 'read' => 1, 'write' => 1 },
3414 TimeEstimated => { 'read' => 1, 'write' => 1 },
3415 TimeWorked => { 'read' => 1, 'write' => 1 },
3416 TimeLeft => { 'read' => 1, 'write' => 1 },
3417 Told => { 'read' => 1, 'write' => 1 },
3418 Resolved => { 'read' => 1 },
3419 Type => { 'read' => 1 },
3420 Starts => { 'read' => 1, 'write' => 1 },
3421 Started => { 'read' => 1, 'write' => 1 },
3422 Due => { 'read' => 1, 'write' => 1 },
3423 Creator => { 'read' => 1, 'auto' => 1 },
3424 Created => { 'read' => 1, 'auto' => 1 },
3425 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3426 LastUpdated => { 'read' => 1, 'auto' => 1 }
3438 my %args = ( Field => undef,
3441 RecordTransaction => 1,
3444 TransactionType => 'Set',
3447 if ($args{'CheckACL'}) {
3448 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3449 return ( 0, $self->loc("Permission Denied"));
3453 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3454 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3455 return(0, $self->loc("Internal Error"));
3458 #if the user is trying to modify the record
3460 #Take care of the old value we really don't want to get in an ACL loop.
3461 # so ask the super::_Value
3462 my $Old = $self->SUPER::_Value("$args{'Field'}");
3465 if ( $args{'UpdateTicket'} ) {
3468 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3469 Value => $args{'Value'} );
3471 #If we can't actually set the field to the value, don't record
3472 # a transaction. instead, get out of here.
3473 return ( 0, $msg ) unless $ret;
3476 if ( $args{'RecordTransaction'} == 1 ) {
3478 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3479 Type => $args{'TransactionType'},
3480 Field => $args{'Field'},
3481 NewValue => $args{'Value'},
3483 TimeTaken => $args{'TimeTaken'},
3485 return ( $Trans, scalar $TransObj->BriefDescription );
3488 return ( $ret, $msg );
3498 Takes the name of a table column.
3499 Returns its value as a string, if the user passes an ACL check
3508 #if the field is public, return it.
3509 if ( $self->_Accessible( $field, 'public' ) ) {
3511 #$RT::Logger->debug("Skipping ACL check for $field\n");
3512 return ( $self->SUPER::_Value($field) );
3516 #If the current user doesn't have ACLs, don't let em at it.
3518 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3521 return ( $self->SUPER::_Value($field) );
3527 # {{{ sub _UpdateTimeTaken
3529 =head2 _UpdateTimeTaken
3531 This routine will increment the timeworked counter. it should
3532 only be called from _NewTransaction
3536 sub _UpdateTimeTaken {
3538 my $Minutes = shift;
3541 $Total = $self->SUPER::_Value("TimeWorked");
3542 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3544 Field => "TimeWorked",
3555 # {{{ Routines dealing with ACCESS CONTROL
3557 # {{{ sub CurrentUserHasRight
3559 =head2 CurrentUserHasRight
3561 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3562 1 if the user has that right. It returns 0 if the user doesn't have that right.
3566 sub CurrentUserHasRight {
3572 Principal => $self->CurrentUser->UserObj(),
3585 Takes a paramhash with the attributes 'Right' and 'Principal'
3586 'Right' is a ticket-scoped textual right from RT::ACE
3587 'Principal' is an RT::User object
3589 Returns 1 if the principal has the right. Returns undef if not.
3601 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3604 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3609 $args{'Principal'}->HasRight(
3611 Right => $args{'Right'}
3620 # {{{ sub Transactions
3624 Returns an RT::Transactions object of all transactions on this ticket
3631 my $transactions = RT::Transactions->new( $self->CurrentUser );
3633 #If the user has no rights, return an empty object
3634 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3635 $transactions->LimitToTicket($self->id);
3637 # if the user may not see comments do not return them
3638 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3639 $transactions->Limit(
3644 $transactions->Limit(
3647 VALUE => "CommentEmailRecord",
3648 ENTRYAGGREGATOR => 'AND'
3654 return ($transactions);
3660 # {{{ TransactionCustomFields
3662 =head2 TransactionCustomFields
3664 Returns the custom fields that transactions on tickets will ahve.
3668 sub TransactionCustomFields {
3670 return $self->QueueObj->TicketTransactionCustomFields;
3675 # {{{ sub CustomFieldValues
3677 =head2 CustomFieldValues
3679 # Do name => id mapping (if needed) before falling back to
3680 # RT::Record's CustomFieldValues
3686 sub CustomFieldValues {
3689 if ( $field and $field !~ /^\d+$/ ) {
3690 my $cf = RT::CustomField->new( $self->CurrentUser );
3691 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3692 unless ( $cf->id ) {
3693 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3695 unless ( $cf->id ) {
3696 # If we didn't find a valid cfid, give up.
3697 return RT::CustomFieldValues->new($self->CurrentUser);
3701 return $self->SUPER::CustomFieldValues($field);
3706 # {{{ sub CustomFieldLookupType
3708 =head2 CustomFieldLookupType
3710 Returns the RT::Ticket lookup type, which can be passed to
3711 RT::CustomField->Create() via the 'LookupType' hash key.
3717 sub CustomFieldLookupType {
3718 "RT::Queue-RT::Ticket";
3725 Jesse Vincent, jesse@bestpractical.com