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 ( $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 ( $QueueObj->DefaultDueIn ) {
445 $Due->AddDays( $QueueObj->DefaultDueIn );
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,
1319 #If the watcher we're trying to add is for the current user
1320 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1321 # If it's an AdminCc and they don't have
1322 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1323 if ( $args{'Type'} eq 'AdminCc' ) {
1324 unless ( $self->CurrentUserHasRight('ModifyTicket')
1325 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1326 return ( 0, $self->loc('Permission Denied'))
1330 # If it's a Requestor or Cc and they don't have
1331 # 'Watch' or 'ModifyTicket', bail
1332 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1334 unless ( $self->CurrentUserHasRight('ModifyTicket')
1335 or $self->CurrentUserHasRight('Watch') ) {
1336 return ( 0, $self->loc('Permission Denied'))
1340 $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1341 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1345 # If the watcher isn't the current user
1346 # and the current user doesn't have 'ModifyTicket'
1349 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1350 return ( 0, $self->loc("Permission Denied") );
1356 return ( $self->_AddWatcher(%args) );
1359 #This contains the meat of AddWatcher. but can be called from a routine like
1360 # Create, which doesn't need the additional acl check
1366 PrincipalId => undef,
1372 my $principal = RT::Principal->new($self->CurrentUser);
1373 if ($args{'Email'}) {
1374 my $user = RT::User->new($RT::SystemUser);
1375 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1376 # If we can't load the user by email address, let's try to load by username
1378 ($pid,$msg) = $user->Load($args{'Email'})
1381 $args{'PrincipalId'} = $pid;
1384 if ($args{'PrincipalId'}) {
1385 $principal->Load($args{'PrincipalId'});
1389 # If we can't find this watcher, we need to bail.
1390 unless ($principal->Id) {
1391 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1392 return(0, $self->loc("Could not find or create that user"));
1396 my $group = RT::Group->new($self->CurrentUser);
1397 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1398 unless ($group->id) {
1399 return(0,$self->loc("Group not found"));
1402 if ( $group->HasMember( $principal)) {
1404 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1408 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1409 InsideTransaction => 1 );
1411 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1413 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1416 unless ( $args{'Silent'} ) {
1417 $self->_NewTransaction(
1418 Type => 'AddWatcher',
1419 NewValue => $principal->Id,
1420 Field => $args{'Type'}
1424 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1430 # {{{ sub DeleteWatcher
1432 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1435 Deletes a Ticket watcher. Takes two arguments:
1437 Type (one of Requestor,Cc,AdminCc)
1441 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1443 Email (the email address of an existing wathcer)
1452 my %args = ( Type => undef,
1453 PrincipalId => undef,
1457 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1458 return ( 0, $self->loc("No principal specified") );
1460 my $principal = RT::Principal->new( $self->CurrentUser );
1461 if ( $args{'PrincipalId'} ) {
1463 $principal->Load( $args{'PrincipalId'} );
1466 my $user = RT::User->new( $self->CurrentUser );
1467 $user->LoadByEmail( $args{'Email'} );
1468 $principal->Load( $user->Id );
1471 # If we can't find this watcher, we need to bail.
1472 unless ( $principal->Id ) {
1473 return ( 0, $self->loc("Could not find that principal") );
1476 my $group = RT::Group->new( $self->CurrentUser );
1477 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1478 unless ( $group->id ) {
1479 return ( 0, $self->loc("Group not found") );
1483 #If the watcher we're trying to add is for the current user
1484 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) {
1486 # If it's an AdminCc and they don't have
1487 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1488 if ( $args{'Type'} eq 'AdminCc' ) {
1489 unless ( $self->CurrentUserHasRight('ModifyTicket')
1490 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1491 return ( 0, $self->loc('Permission Denied') );
1495 # If it's a Requestor or Cc and they don't have
1496 # 'Watch' or 'ModifyTicket', bail
1497 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1499 unless ( $self->CurrentUserHasRight('ModifyTicket')
1500 or $self->CurrentUserHasRight('Watch') ) {
1501 return ( 0, $self->loc('Permission Denied') );
1505 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1507 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1511 # If the watcher isn't the current user
1512 # and the current user doesn't have 'ModifyTicket' bail
1514 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1515 return ( 0, $self->loc("Permission Denied") );
1521 # see if this user is already a watcher.
1523 unless ( $group->HasMember($principal) ) {
1525 $self->loc( 'That principal is not a [_1] for this ticket',
1529 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1531 $RT::Logger->error( "Failed to delete "
1533 . " as a member of group "
1539 'Could not remove that principal as a [_1] for this ticket',
1543 unless ( $args{'Silent'} ) {
1544 $self->_NewTransaction( Type => 'DelWatcher',
1545 OldValue => $principal->Id,
1546 Field => $args{'Type'} );
1550 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1551 $principal->Object->Name,
1560 =head2 SquelchMailTo [EMAIL]
1562 Takes an optional email address to never email about updates to this ticket.
1565 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1569 my $t = RT::Ticket->new($RT::SystemUser);
1570 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1572 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1574 my @returned = $t->SquelchMailTo('nobody@example.com');
1576 is($#returned, 0, "The ticket has one squelched recipients");
1578 my @names = $t->Attributes->Names;
1579 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1580 @returned = $t->SquelchMailTo('nobody@example.com');
1583 is($#returned, 0, "The ticket has one squelched recipients");
1585 @names = $t->Attributes->Names;
1586 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1589 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1590 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1591 @returned = $t->SquelchMailTo();
1592 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1602 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1606 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1607 unless grep { $_->Content eq $attr }
1608 $self->Attributes->Named('SquelchMailTo');
1611 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1614 my @attributes = $self->Attributes->Named('SquelchMailTo');
1615 return (@attributes);
1619 =head2 UnsquelchMailTo ADDRESS
1621 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1623 Returns a tuple of (status, message)
1627 sub UnsquelchMailTo {
1630 my $address = shift;
1631 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1632 return ( 0, $self->loc("Permission Denied") );
1635 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1636 return ($val, $msg);
1640 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1642 =head2 RequestorAddresses
1644 B<Returns> String: All Ticket Requestor email addresses as a string.
1648 sub RequestorAddresses {
1651 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1655 return ( $self->Requestors->MemberEmailAddressesAsString );
1659 =head2 AdminCcAddresses
1661 returns String: All Ticket AdminCc email addresses as a string
1665 sub AdminCcAddresses {
1668 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1672 return ( $self->AdminCc->MemberEmailAddressesAsString )
1678 returns String: All Ticket Ccs as a string of email addresses
1685 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1689 return ( $self->Cc->MemberEmailAddressesAsString);
1695 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1697 # {{{ sub Requestors
1702 Returns this ticket's Requestors as an RT::Group object
1709 my $group = RT::Group->new($self->CurrentUser);
1710 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1711 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1724 Returns an RT::Group object which contains this ticket's Ccs.
1725 If the user doesn't have "ShowTicket" permission, returns an empty group
1732 my $group = RT::Group->new($self->CurrentUser);
1733 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1734 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1747 Returns an RT::Group object which contains this ticket's AdminCcs.
1748 If the user doesn't have "ShowTicket" permission, returns an empty group
1755 my $group = RT::Group->new($self->CurrentUser);
1756 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1757 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1767 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1770 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1772 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1774 Takes a param hash with the attributes Type and either PrincipalId or Email
1776 Type is one of Requestor, Cc, AdminCc and Owner
1778 PrincipalId is an RT::Principal id, and Email is an email address.
1780 Returns true if the specified principal (or the one corresponding to the
1781 specified address) is a member of the group Type for this ticket.
1783 XX TODO: This should be Memoized.
1790 my %args = ( Type => 'Requestor',
1791 PrincipalId => undef,
1796 # Load the relevant group.
1797 my $group = RT::Group->new($self->CurrentUser);
1798 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1800 # Find the relevant principal.
1801 my $principal = RT::Principal->new($self->CurrentUser);
1802 if (!$args{PrincipalId} && $args{Email}) {
1803 # Look up the specified user.
1804 my $user = RT::User->new($self->CurrentUser);
1805 $user->LoadByEmail($args{Email});
1807 $args{PrincipalId} = $user->PrincipalId;
1810 # A non-existent user can't be a group member.
1814 $principal->Load($args{'PrincipalId'});
1816 # Ask if it has the member in question
1817 return ($group->HasMember($principal));
1822 # {{{ sub IsRequestor
1824 =head2 IsRequestor PRINCIPAL_ID
1826 Takes an RT::Principal id
1827 Returns true if the principal is a requestor of the current ticket.
1836 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1844 =head2 IsCc PRINCIPAL_ID
1846 Takes an RT::Principal id.
1847 Returns true if the principal is a requestor of the current ticket.
1856 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1864 =head2 IsAdminCc PRINCIPAL_ID
1866 Takes an RT::Principal id.
1867 Returns true if the principal is a requestor of the current ticket.
1875 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1885 Takes an RT::User object. Returns true if that user is this ticket's owner.
1886 returns undef otherwise
1894 # no ACL check since this is used in acl decisions
1895 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1899 #Tickets won't yet have owners when they're being created.
1900 unless ( $self->OwnerObj->id ) {
1904 if ( $person->id == $self->OwnerObj->id ) {
1918 # {{{ Routines dealing with queues
1920 # {{{ sub ValidateQueue
1927 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1931 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1932 my $id = $QueueObj->Load($Value);
1948 my $NewQueue = shift;
1950 #Redundant. ACL gets checked in _Set;
1951 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1952 return ( 0, $self->loc("Permission Denied") );
1955 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1956 $NewQueueObj->Load($NewQueue);
1958 unless ( $NewQueueObj->Id() ) {
1959 return ( 0, $self->loc("That queue does not exist") );
1962 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1963 return ( 0, $self->loc('That is the same value') );
1966 $self->CurrentUser->HasRight(
1967 Right => 'CreateTicket',
1968 Object => $NewQueueObj
1972 return ( 0, $self->loc("You may not create requests in that queue.") );
1976 $self->OwnerObj->HasRight(
1977 Right => 'OwnTicket',
1978 Object => $NewQueueObj
1985 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
1995 Takes nothing. returns this ticket's queue object
2002 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2004 #We call __Value so that we can avoid the ACL decision and some deep recursion
2005 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2006 return ($queue_obj);
2013 # {{{ Date printing routines
2019 Returns an RT::Date object containing this ticket's due date
2026 my $time = new RT::Date( $self->CurrentUser );
2028 # -1 is RT::Date slang for never
2030 $time->Set( Format => 'sql', Value => $self->Due );
2033 $time->Set( Format => 'unix', Value => -1 );
2041 # {{{ sub DueAsString
2045 Returns this ticket's due date as a human readable string
2051 return $self->DueObj->AsString();
2056 # {{{ sub ResolvedObj
2060 Returns an RT::Date object of this ticket's 'resolved' time.
2067 my $time = new RT::Date( $self->CurrentUser );
2068 $time->Set( Format => 'sql', Value => $self->Resolved );
2074 # {{{ sub SetStarted
2078 Takes a date in ISO format or undef
2079 Returns a transaction id and a message
2080 The client calls "Start" to note that the project was started on the date in $date.
2081 A null date means "now"
2087 my $time = shift || 0;
2089 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2090 return ( 0, self->loc("Permission Denied") );
2093 #We create a date object to catch date weirdness
2094 my $time_obj = new RT::Date( $self->CurrentUser() );
2096 $time_obj->Set( Format => 'ISO', Value => $time );
2099 $time_obj->SetToNow();
2102 #Now that we're starting, open this ticket
2103 #TODO do we really want to force this as policy? it should be a scrip
2105 #We need $TicketAsSystem, in case the current user doesn't have
2108 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2109 $TicketAsSystem->Load( $self->Id );
2110 if ( $TicketAsSystem->Status eq 'new' ) {
2111 $TicketAsSystem->Open();
2114 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2120 # {{{ sub StartedObj
2124 Returns an RT::Date object which contains this ticket's
2132 my $time = new RT::Date( $self->CurrentUser );
2133 $time->Set( Format => 'sql', Value => $self->Started );
2143 Returns an RT::Date object which contains this ticket's
2151 my $time = new RT::Date( $self->CurrentUser );
2152 $time->Set( Format => 'sql', Value => $self->Starts );
2162 Returns an RT::Date object which contains this ticket's
2170 my $time = new RT::Date( $self->CurrentUser );
2171 $time->Set( Format => 'sql', Value => $self->Told );
2177 # {{{ sub ToldAsString
2181 A convenience method that returns ToldObj->AsString
2183 TODO: This should be deprecated
2189 if ( $self->Told ) {
2190 return $self->ToldObj->AsString();
2199 # {{{ sub TimeWorkedAsString
2201 =head2 TimeWorkedAsString
2203 Returns the amount of time worked on this ticket as a Text String
2207 sub TimeWorkedAsString {
2209 return "0" unless $self->TimeWorked;
2211 #This is not really a date object, but if we diff a number of seconds
2212 #vs the epoch, we'll get a nice description of time worked.
2214 my $worked = new RT::Date( $self->CurrentUser );
2216 #return the #of minutes worked turned into seconds and written as
2217 # a simple text string
2219 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2226 # {{{ Routines dealing with correspondence/comments
2232 Comment on this ticket.
2233 Takes a hashref with the following attributes:
2234 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2237 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2239 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2240 They will, however, be prepared and you'll be able to access them through the TransactionObj
2242 Returns: Transaction id, Error Message, Transaction Object
2243 (note the different order from Create()!)
2250 my %args = ( CcMessageTo => undef,
2251 BccMessageTo => undef,
2258 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2259 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2260 return ( 0, $self->loc("Permission Denied"), undef );
2262 $args{'NoteType'} = 'Comment';
2264 if ($args{'DryRun'}) {
2265 $RT::Handle->BeginTransaction();
2266 $args{'CommitScrips'} = 0;
2269 my @results = $self->_RecordNote(%args);
2270 if ($args{'DryRun'}) {
2271 $RT::Handle->Rollback();
2278 # {{{ sub Correspond
2282 Correspond on this ticket.
2283 Takes a hashref with the following attributes:
2286 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2288 if there's no MIMEObj, Content is used to build a MIME::Entity object
2290 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2291 They will, however, be prepared and you'll be able to access them through the TransactionObj
2293 Returns: Transaction id, Error Message, Transaction Object
2294 (note the different order from Create()!)
2301 my %args = ( CcMessageTo => undef,
2302 BccMessageTo => undef,
2308 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2309 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2310 return ( 0, $self->loc("Permission Denied"), undef );
2313 $args{'NoteType'} = 'Correspond';
2314 if ($args{'DryRun'}) {
2315 $RT::Handle->BeginTransaction();
2316 $args{'CommitScrips'} = 0;
2319 my @results = $self->_RecordNote(%args);
2321 #Set the last told date to now if this isn't mail from the requestor.
2322 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2323 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2325 if ($args{'DryRun'}) {
2326 $RT::Handle->Rollback();
2335 # {{{ sub _RecordNote
2339 the meat of both comment and correspond.
2341 Performs no access control checks. hence, dangerous.
2348 my %args = ( CcMessageTo => undef,
2349 BccMessageTo => undef,
2356 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2357 return ( 0, $self->loc("No message attached"), undef );
2359 unless ( $args{'MIMEObj'} ) {
2360 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2361 ref $args{'Content'}
2363 : [ $args{'Content'} ]
2367 # convert text parts into utf-8
2368 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2370 # If we've been passed in CcMessageTo and BccMessageTo fields,
2371 # add them to the mime object for passing on to the transaction handler
2372 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2375 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2376 undef, $args{'CcMessageTo'}
2378 if defined $args{'CcMessageTo'};
2379 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2380 RT::User::CanonicalizeEmailAddress(
2381 undef, $args{'BccMessageTo'}
2383 if defined $args{'BccMessageTo'};
2385 # If this is from an external source, we need to come up with its
2386 # internal Message-ID now, so all emails sent because of this
2387 # message have a common Message-ID
2388 unless ($args{'MIMEObj'}->head->get('Message-ID')
2389 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) {
2390 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2392 . $RT::VERSION . "-"
2394 . CORE::time() . "-"
2395 . int(rand(2000)) . '.'
2398 . "0" . "@" # Email sent
2403 #Record the correspondence (write the transaction)
2404 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2405 Type => $args{'NoteType'},
2406 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2407 TimeTaken => $args{'TimeTaken'},
2408 MIMEObj => $args{'MIMEObj'},
2409 CommitScrips => $args{'CommitScrips'},
2413 $RT::Logger->err("$self couldn't init a transaction $msg");
2414 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2417 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2429 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2432 my $type = shift || "";
2434 unless ( $self->{"$field$type"} ) {
2435 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2436 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2437 # Maybe this ticket is a merged ticket
2438 my $Tickets = new RT::Tickets( $self->CurrentUser );
2439 # at least to myself
2440 $self->{"$field$type"}->Limit( FIELD => $field,
2441 VALUE => $self->URI,
2442 ENTRYAGGREGATOR => 'OR' );
2443 $Tickets->Limit( FIELD => 'EffectiveId',
2444 VALUE => $self->EffectiveId );
2445 while (my $Ticket = $Tickets->Next) {
2446 $self->{"$field$type"}->Limit( FIELD => $field,
2447 VALUE => $Ticket->URI,
2448 ENTRYAGGREGATOR => 'OR' );
2450 $self->{"$field$type"}->Limit( FIELD => 'Type',
2455 return ( $self->{"$field$type"} );
2460 # {{{ sub DeleteLink
2464 Delete a link. takes a paramhash of Base, Target and Type.
2465 Either Base or Target must be null. The null value will
2466 be replaced with this ticket\'s id
2480 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2481 $RT::Logger->debug("No permission to delete links\n");
2482 return ( 0, $self->loc('Permission Denied'))
2486 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2489 $RT::Logger->debug("Couldn't find that link\n");
2493 my ($direction, $remote_link);
2495 if ( $args{'Base'} ) {
2496 $remote_link = $args{'Base'};
2497 $direction = 'Target';
2499 elsif ( $args{'Target'} ) {
2500 $remote_link = $args{'Target'};
2504 if ( $args{'Silent'} ) {
2505 return ( $val, $Msg );
2508 my $remote_uri = RT::URI->new( $self->CurrentUser );
2509 $remote_uri->FromURI( $remote_link );
2511 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2512 Type => 'DeleteLink',
2513 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2514 OldValue => $remote_uri->URI || $remote_link,
2518 if ( $remote_uri->IsLocal ) {
2520 my $OtherObj = $remote_uri->Object;
2521 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2522 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2523 : $LINKDIRMAP{$args{'Type'}}->{Target},
2524 OldValue => $self->URI,
2525 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2529 return ( $Trans, $Msg );
2539 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2545 my %args = ( Target => '',
2552 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2553 return ( 0, $self->loc("Permission Denied") );
2557 $self->_AddLink(%args);
2562 Private non-acled variant of AddLink so that links can be added during create.
2568 my %args = ( Target => '',
2574 # {{{ If the other URI is an RT::Ticket, we want to make sure the user
2575 # can modify it too...
2576 my $other_ticket_uri = RT::URI->new($self->CurrentUser);
2578 if ( $args{'Target'} ) {
2579 $other_ticket_uri->FromURI( $args{'Target'} );
2582 elsif ( $args{'Base'} ) {
2583 $other_ticket_uri->FromURI( $args{'Base'} );
2586 unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) {
2587 my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'})
2588 : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
2589 $RT::Logger->warning( "$self $msg\n" );
2594 if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') {
2595 my $object = $other_ticket_uri->Resolver->Object;
2597 if ( UNIVERSAL::isa( $object, 'RT::Ticket' )
2599 && !$object->CurrentUserHasRight('ModifyTicket') )
2601 return ( 0, $self->loc("Permission Denied") );
2608 my ($val, $Msg) = $self->SUPER::_AddLink(%args);
2611 return ($val, $Msg);
2614 my ($direction, $remote_link);
2615 if ( $args{'Target'} ) {
2616 $remote_link = $args{'Target'};
2617 $direction = 'Base';
2618 } elsif ( $args{'Base'} ) {
2619 $remote_link = $args{'Base'};
2620 $direction = 'Target';
2623 # Don't write the transaction if we're doing this on create
2624 if ( $args{'Silent'} ) {
2625 return ( $val, $Msg );
2628 my $remote_uri = RT::URI->new( $self->CurrentUser );
2629 $remote_uri->FromURI( $remote_link );
2631 #Write the transaction
2632 my ( $Trans, $Msg, $TransObj ) =
2633 $self->_NewTransaction(Type => 'AddLink',
2634 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2635 NewValue => $remote_uri->URI || $remote_link,
2638 if ( $remote_uri->IsLocal ) {
2640 my $OtherObj = $remote_uri->Object;
2641 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2642 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2643 : $LINKDIRMAP{$args{'Type'}}->{Target},
2644 NewValue => $self->URI,
2645 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2648 return ( $val, $Msg );
2660 MergeInto take the id of the ticket to merge this ticket into.
2665 my $t1 = RT::Ticket->new($RT::SystemUser);
2666 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2668 my $t2 = RT::Ticket->new($RT::SystemUser);
2669 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2671 my ($msg, $val) = $t1->MergeInto($t2->id);
2673 $t1 = RT::Ticket->new($RT::SystemUser);
2674 is ($t1->id, undef, "ok. we've got a blank ticket1");
2677 is ($t1->id, $t2->id);
2679 is ($t1->Requestors->MembersObj->Count, 2);
2688 my $ticket_id = shift;
2690 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2691 return ( 0, $self->loc("Permission Denied") );
2694 # Load up the new ticket.
2695 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2696 $MergeInto->Load($ticket_id);
2698 # make sure it exists.
2699 unless ( $MergeInto->Id ) {
2700 return ( 0, $self->loc("New ticket doesn't exist") );
2703 # Make sure the current user can modify the new ticket.
2704 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2705 return ( 0, $self->loc("Permission Denied") );
2708 $RT::Handle->BeginTransaction();
2710 # We use EffectiveId here even though it duplicates information from
2711 # the links table becasue of the massive performance hit we'd take
2712 # by trying to do a separate database query for merge info everytime
2715 #update this ticket's effective id to the new ticket's id.
2716 my ( $id_val, $id_msg ) = $self->__Set(
2717 Field => 'EffectiveId',
2718 Value => $MergeInto->Id()
2722 $RT::Handle->Rollback();
2723 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2726 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2728 unless ($status_val) {
2729 $RT::Handle->Rollback();
2730 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2731 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2735 # update all the links that point to that old ticket
2736 my $old_links_to = RT::Links->new($self->CurrentUser);
2737 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2740 while (my $link = $old_links_to->Next) {
2741 if (exists $old_seen{$link->Base."-".$link->Type}) {
2744 elsif ($link->Base eq $MergeInto->URI) {
2747 # First, make sure the link doesn't already exist. then move it over.
2748 my $tmp = RT::Link->new($RT::SystemUser);
2749 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2753 $link->SetTarget($MergeInto->URI);
2754 $link->SetLocalTarget($MergeInto->id);
2756 $old_seen{$link->Base."-".$link->Type} =1;
2761 my $old_links_from = RT::Links->new($self->CurrentUser);
2762 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2764 while (my $link = $old_links_from->Next) {
2765 if (exists $old_seen{$link->Type."-".$link->Target}) {
2768 if ($link->Target eq $MergeInto->URI) {
2771 # First, make sure the link doesn't already exist. then move it over.
2772 my $tmp = RT::Link->new($RT::SystemUser);
2773 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2777 $link->SetBase($MergeInto->URI);
2778 $link->SetLocalBase($MergeInto->id);
2779 $old_seen{$link->Type."-".$link->Target} =1;
2785 # Update time fields
2786 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2788 my $mutator = "Set$type";
2789 $MergeInto->$mutator(
2790 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2793 #add all of this ticket's watchers to that ticket.
2794 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2796 my $people = $self->$watcher_type->MembersObj;
2797 my $addwatcher_type = $watcher_type;
2798 $addwatcher_type =~ s/s$//;
2800 while ( my $watcher = $people->Next ) {
2802 my ($val, $msg) = $MergeInto->_AddWatcher(
2803 Type => $addwatcher_type,
2805 PrincipalId => $watcher->MemberId
2808 $RT::Logger->warning($msg);
2814 #find all of the tickets that were merged into this ticket.
2815 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2816 $old_mergees->Limit(
2817 FIELD => 'EffectiveId',
2822 # update their EffectiveId fields to the new ticket's id
2823 while ( my $ticket = $old_mergees->Next() ) {
2824 my ( $val, $msg ) = $ticket->__Set(
2825 Field => 'EffectiveId',
2826 Value => $MergeInto->Id()
2830 #make a new link: this ticket is merged into that other ticket.
2831 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2833 $MergeInto->_SetLastUpdated;
2835 $RT::Handle->Commit();
2836 return ( 1, $self->loc("Merge Successful") );
2843 # {{{ Routines dealing with ownership
2849 Takes nothing and returns an RT::User object of
2857 #If this gets ACLed, we lose on a rights check in User.pm and
2858 #get deep recursion. if we need ACLs here, we need
2859 #an equiv without ACLs
2861 my $owner = new RT::User( $self->CurrentUser );
2862 $owner->Load( $self->__Value('Owner') );
2864 #Return the owner object
2870 # {{{ sub OwnerAsString
2872 =head2 OwnerAsString
2874 Returns the owner's email address
2880 return ( $self->OwnerObj->EmailAddress );
2890 Takes two arguments:
2891 the Id or Name of the owner
2892 and (optionally) the type of the SetOwner Transaction. It defaults
2893 to 'Give'. 'Steal' is also a valid option.
2897 my $root = RT::User->new($RT::SystemUser);
2898 $root->Load('root');
2899 ok ($root->Id, "Loaded the root user");
2900 my $t = RT::Ticket->new($RT::SystemUser);
2902 $t->SetOwner('root');
2903 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2905 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2906 my $txns = RT::Transactions->new($RT::SystemUser);
2907 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2908 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2909 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2910 my $steal = $txns->First;
2911 ok($steal->OldValue == $root->Id , "Stolen from root");
2912 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2920 my $NewOwner = shift;
2921 my $Type = shift || "Give";
2923 # must have ModifyTicket rights
2924 # or TakeTicket/StealTicket and $NewOwner is self
2925 # see if it's a take
2926 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
2927 unless ( $self->CurrentUserHasRight('ModifyTicket')
2928 || $self->CurrentUserHasRight('TakeTicket') ) {
2929 return ( 0, $self->loc("Permission Denied") );
2933 # see if it's a steal
2934 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
2935 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
2937 unless ( $self->CurrentUserHasRight('ModifyTicket')
2938 || $self->CurrentUserHasRight('StealTicket') ) {
2939 return ( 0, $self->loc("Permission Denied") );
2943 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2944 return ( 0, $self->loc("Permission Denied") );
2947 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2948 my $OldOwnerObj = $self->OwnerObj;
2950 $NewOwnerObj->Load($NewOwner);
2951 if ( !$NewOwnerObj->Id ) {
2952 return ( 0, $self->loc("That user does not exist") );
2955 #If thie ticket has an owner and it's not the current user
2957 if ( ( $Type ne 'Steal' )
2958 and ( $Type ne 'Force' )
2959 and #If we're not stealing
2960 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
2961 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
2962 ) { #and it's not us
2965 "You can only reassign tickets that you own or that are unowned" ) );
2968 #If we've specified a new owner and that user can't modify the ticket
2969 elsif ( ( $NewOwnerObj->Id )
2970 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
2973 return ( 0, $self->loc("That user may not own tickets in that queue") );
2976 #If the ticket has an owner and it's the new owner, we don't need
2978 elsif ( ( $self->OwnerObj )
2979 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
2980 return ( 0, $self->loc("That user already owns that ticket") );
2983 $RT::Handle->BeginTransaction();
2985 # Delete the owner in the owner group, then add a new one
2986 # TODO: is this safe? it's not how we really want the API to work
2987 # for most things, but it's fast.
2988 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
2990 $RT::Handle->Rollback();
2991 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
2994 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2995 PrincipalId => $NewOwnerObj->PrincipalId,
2996 InsideTransaction => 1 );
2998 $RT::Handle->Rollback();
2999 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3002 # We call set twice with slightly different arguments, so
3003 # as to not have an SQL transaction span two RT transactions
3005 my ( $val, $msg ) = $self->_Set(
3007 RecordTransaction => 0,
3008 Value => $NewOwnerObj->Id,
3010 TransactionType => $Type,
3011 CheckACL => 0, # don't check acl
3015 $RT::Handle->Rollback;
3016 return ( 0, $self->loc("Could not change owner. ") . $msg );
3019 $RT::Handle->Commit();
3021 my ( $trans, $msg, undef ) = $self->_NewTransaction(
3024 NewValue => $NewOwnerObj->Id,
3025 OldValue => $OldOwnerObj->Id,
3029 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3030 $OldOwnerObj->Name, $NewOwnerObj->Name );
3032 # TODO: make sure the trans committed properly
3034 return ( $trans, $msg );
3044 A convenince method to set the ticket's owner to the current user
3050 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3059 Convenience method to set the owner to 'nobody' if the current user is the owner.
3065 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3074 A convenience method to change the owner of the current ticket to the
3075 current user. Even if it's owned by another user.
3082 if ( $self->IsOwner( $self->CurrentUser ) ) {
3083 return ( 0, $self->loc("You already own this ticket") );
3086 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3096 # {{{ Routines dealing with status
3098 # {{{ sub ValidateStatus
3100 =head2 ValidateStatus STATUS
3102 Takes a string. Returns true if that status is a valid status for this ticket.
3103 Returns false otherwise.
3107 sub ValidateStatus {
3111 #Make sure the status passed in is valid
3112 unless ( $self->QueueObj->IsValidStatus($status) ) {
3124 =head2 SetStatus STATUS
3126 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3128 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.
3132 my $tt = RT::Ticket->new($RT::SystemUser);
3133 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3136 is($tt->Status, 'new', "New ticket is created as new");
3138 ($id, $msg) = $tt->SetStatus('open');
3140 like($msg, qr/open/i, "Status message is correct");
3141 ($id, $msg) = $tt->SetStatus('resolved');
3143 like($msg, qr/resolved/i, "Status message is correct");
3144 ($id, $msg) = $tt->SetStatus('resolved');
3158 $args{Status} = shift;
3165 if ( $args{Status} eq 'deleted') {
3166 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3167 return ( 0, $self->loc('Permission Denied') );
3170 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3171 return ( 0, $self->loc('Permission Denied') );
3175 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3176 return (0, $self->loc('That ticket has unresolved dependencies'));
3179 my $now = RT::Date->new( $self->CurrentUser );
3182 #If we're changing the status from new, record that we've started
3183 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3185 #Set the Started time to "now"
3186 $self->_Set( Field => 'Started',
3188 RecordTransaction => 0 );
3191 #When we close a ticket, set the 'Resolved' attribute to now.
3192 # It's misnamed, but that's just historical.
3193 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3194 $self->_Set( Field => 'Resolved',
3196 RecordTransaction => 0 );
3199 #Actually update the status
3200 my ($val, $msg)= $self->_Set( Field => 'Status',
3201 Value => $args{Status},
3204 TransactionType => 'Status' );
3215 Takes no arguments. Marks this ticket for garbage collection
3221 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3222 return $self->Delete;
3227 return ( $self->SetStatus('deleted') );
3229 # TODO: garbage collection
3238 Sets this ticket's status to stalled
3244 return ( $self->SetStatus('stalled') );
3253 Sets this ticket's status to rejected
3259 return ( $self->SetStatus('rejected') );
3268 Sets this ticket\'s status to Open
3274 return ( $self->SetStatus('open') );
3283 Sets this ticket\'s status to Resolved
3289 return ( $self->SetStatus('resolved') );
3297 # {{{ Actions + Routines dealing with transactions
3299 # {{{ sub SetTold and _SetTold
3301 =head2 SetTold ISO [TIMETAKEN]
3303 Updates the told and records a transaction
3310 $told = shift if (@_);
3311 my $timetaken = shift || 0;
3313 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3314 return ( 0, $self->loc("Permission Denied") );
3317 my $datetold = new RT::Date( $self->CurrentUser );
3319 $datetold->Set( Format => 'iso',
3323 $datetold->SetToNow();
3326 return ( $self->_Set( Field => 'Told',
3327 Value => $datetold->ISO,
3328 TimeTaken => $timetaken,
3329 TransactionType => 'Told' ) );
3334 Updates the told without a transaction or acl check. Useful when we're sending replies.
3341 my $now = new RT::Date( $self->CurrentUser );
3344 #use __Set to get no ACLs ;)
3345 return ( $self->__Set( Field => 'Told',
3346 Value => $now->ISO ) );
3351 =head2 TransactionBatch
3353 Returns an array reference of all transactions created on this ticket during
3354 this ticket object's lifetime, or undef if there were none.
3356 Only works when the $RT::UseTransactionBatch config variable is set to true.
3360 sub TransactionBatch {
3362 return $self->{_TransactionBatch};
3368 # DESTROY methods need to localize $@, or it may unset it. This
3369 # causes $m->abort to not bubble all of the way up. See perlbug
3370 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3373 # The following line eliminates reentrancy.
3374 # It protects against the fact that perl doesn't deal gracefully
3375 # when an object's refcount is changed in its destructor.
3376 return if $self->{_Destroyed}++;
3378 my $batch = $self->TransactionBatch or return;
3380 RT::Scrips->new($RT::SystemUser)->Apply(
3381 Stage => 'TransactionBatch',
3383 TransactionObj => $batch->[0],
3384 Type => join(',', (map { $_->Type } @{$batch}) )
3390 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3392 # {{{ sub _OverlayAccessible
3394 sub _OverlayAccessible {
3396 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3397 Queue => { 'read' => 1, 'write' => 1 },
3398 Requestors => { 'read' => 1, 'write' => 1 },
3399 Owner => { 'read' => 1, 'write' => 1 },
3400 Subject => { 'read' => 1, 'write' => 1 },
3401 InitialPriority => { 'read' => 1, 'write' => 1 },
3402 FinalPriority => { 'read' => 1, 'write' => 1 },
3403 Priority => { 'read' => 1, 'write' => 1 },
3404 Status => { 'read' => 1, 'write' => 1 },
3405 TimeEstimated => { 'read' => 1, 'write' => 1 },
3406 TimeWorked => { 'read' => 1, 'write' => 1 },
3407 TimeLeft => { 'read' => 1, 'write' => 1 },
3408 Told => { 'read' => 1, 'write' => 1 },
3409 Resolved => { 'read' => 1 },
3410 Type => { 'read' => 1 },
3411 Starts => { 'read' => 1, 'write' => 1 },
3412 Started => { 'read' => 1, 'write' => 1 },
3413 Due => { 'read' => 1, 'write' => 1 },
3414 Creator => { 'read' => 1, 'auto' => 1 },
3415 Created => { 'read' => 1, 'auto' => 1 },
3416 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3417 LastUpdated => { 'read' => 1, 'auto' => 1 }
3429 my %args = ( Field => undef,
3432 RecordTransaction => 1,
3435 TransactionType => 'Set',
3438 if ($args{'CheckACL'}) {
3439 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3440 return ( 0, $self->loc("Permission Denied"));
3444 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3445 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3446 return(0, $self->loc("Internal Error"));
3449 #if the user is trying to modify the record
3451 #Take care of the old value we really don't want to get in an ACL loop.
3452 # so ask the super::_Value
3453 my $Old = $self->SUPER::_Value("$args{'Field'}");
3456 if ( $args{'UpdateTicket'} ) {
3459 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3460 Value => $args{'Value'} );
3462 #If we can't actually set the field to the value, don't record
3463 # a transaction. instead, get out of here.
3464 return ( 0, $msg ) unless $ret;
3467 if ( $args{'RecordTransaction'} == 1 ) {
3469 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3470 Type => $args{'TransactionType'},
3471 Field => $args{'Field'},
3472 NewValue => $args{'Value'},
3474 TimeTaken => $args{'TimeTaken'},
3476 return ( $Trans, scalar $TransObj->BriefDescription );
3479 return ( $ret, $msg );
3489 Takes the name of a table column.
3490 Returns its value as a string, if the user passes an ACL check
3499 #if the field is public, return it.
3500 if ( $self->_Accessible( $field, 'public' ) ) {
3502 #$RT::Logger->debug("Skipping ACL check for $field\n");
3503 return ( $self->SUPER::_Value($field) );
3507 #If the current user doesn't have ACLs, don't let em at it.
3509 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3512 return ( $self->SUPER::_Value($field) );
3518 # {{{ sub _UpdateTimeTaken
3520 =head2 _UpdateTimeTaken
3522 This routine will increment the timeworked counter. it should
3523 only be called from _NewTransaction
3527 sub _UpdateTimeTaken {
3529 my $Minutes = shift;
3532 $Total = $self->SUPER::_Value("TimeWorked");
3533 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3535 Field => "TimeWorked",
3546 # {{{ Routines dealing with ACCESS CONTROL
3548 # {{{ sub CurrentUserHasRight
3550 =head2 CurrentUserHasRight
3552 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3553 1 if the user has that right. It returns 0 if the user doesn't have that right.
3557 sub CurrentUserHasRight {
3563 Principal => $self->CurrentUser->UserObj(),
3576 Takes a paramhash with the attributes 'Right' and 'Principal'
3577 'Right' is a ticket-scoped textual right from RT::ACE
3578 'Principal' is an RT::User object
3580 Returns 1 if the principal has the right. Returns undef if not.
3592 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3595 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3600 $args{'Principal'}->HasRight(
3602 Right => $args{'Right'}
3611 # {{{ sub Transactions
3615 Returns an RT::Transactions object of all transactions on this ticket
3622 my $transactions = RT::Transactions->new( $self->CurrentUser );
3624 #If the user has no rights, return an empty object
3625 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3626 $transactions->LimitToTicket($self->id);
3628 # if the user may not see comments do not return them
3629 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3630 $transactions->Limit(
3635 $transactions->Limit(
3638 VALUE => "CommentEmailRecord",
3639 ENTRYAGGREGATOR => 'AND'
3645 return ($transactions);
3651 # {{{ TransactionCustomFields
3653 =head2 TransactionCustomFields
3655 Returns the custom fields that transactions on tickets will ahve.
3659 sub TransactionCustomFields {
3661 return $self->QueueObj->TicketTransactionCustomFields;
3666 # {{{ sub CustomFieldValues
3668 =head2 CustomFieldValues
3670 # Do name => id mapping (if needed) before falling back to
3671 # RT::Record's CustomFieldValues
3677 sub CustomFieldValues {
3680 if ( $field and $field !~ /^\d+$/ ) {
3681 my $cf = RT::CustomField->new( $self->CurrentUser );
3682 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->QueueObj->Id );
3683 unless ( $cf->id ) {
3684 $cf->LoadByNameAndQueue( Name => $field, Queue => '0' );
3687 unless ( $field =~ /^\d+$/ ) {
3688 # If we didn't find a valid cfid, give up.
3689 return RT::CustomFieldValues->new($self->CurrentUser);
3692 return $self->SUPER::CustomFieldValues($field);
3697 # {{{ sub CustomFieldLookupType
3699 =head2 CustomFieldLookupType
3701 Returns the RT::Ticket lookup type, which can be passed to
3702 RT::CustomField->Create() via the 'LookupType' hash key.
3708 sub CustomFieldLookupType {
3709 "RT::Queue-RT::Ticket";
3716 Jesse Vincent, jesse@bestpractical.com