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 # {{{ Deal with auto-customer association
695 #unless we already have (a) customer(s)...
696 unless ( $self->Customers->Count ) {
698 #first find any requestors with emails but *without* customer targets
699 my @NoCust_Requestors =
700 grep { $_->EmailAddress && ! $_->Customers->Count }
701 @{ $self->Requestors->UserMembersObj->ItemsArrayRef };
703 for my $Requestor (@NoCust_Requestors) {
705 #perhaps the stuff in here should be in a User method??
707 &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
709 foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
711 ## false laziness w/RT/Interface/Web_Vendor.pm
712 my @link = ( 'Type' => 'MemberOf',
713 'Target' => "freeside://freeside/cust_main/$custnum",
716 my( $val, $msg ) = $Requestor->AddLink(@link);
717 #XXX should do something with $msg# push @non_fatal_errors, $msg;
723 #find any requestors with customer targets
725 my %cust_target = ();
728 grep { $_->Customers->Count }
729 @{ $self->Requestors->UserMembersObj->ItemsArrayRef };
731 foreach my $Requestor ( @Requestors ) {
732 foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
733 $cust_target{ $cust_link->Target } = 1;
737 #and then auto-associate this ticket with those customers
739 foreach my $cust_target ( keys %cust_target ) {
741 my @link = ( 'Type' => 'MemberOf',
742 #'Target' => "freeside://freeside/cust_main/$custnum",
743 'Target' => $cust_target,
746 my( $val, $msg ) = $self->AddLink(@link);
747 push @non_fatal_errors, $msg;
755 # {{{ Add all the custom fields
757 foreach my $arg ( keys %args ) {
758 next unless ( $arg =~ /^CustomField-(\d+)$/i );
761 my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
763 next unless ( length($value) );
765 # Allow passing in uploaded LargeContent etc by hash reference
766 $self->_AddCustomFieldValue(
767 (UNIVERSAL::isa( $value => 'HASH' )
772 RecordTransaction => 0,
779 if ( $args{'_RecordTransaction'} ) {
781 # {{{ Add a transaction for the create
782 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
784 TimeTaken => $args{'TimeWorked'},
785 MIMEObj => $args{'MIMEObj'}
788 if ( $self->Id && $Trans ) {
790 $TransObj->UpdateCustomFields(ARGSRef => \%args);
792 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
793 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
794 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
797 $RT::Handle->Rollback();
799 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
800 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
801 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
804 $RT::Handle->Commit();
805 return ( $self->Id, $TransObj->Id, $ErrStr );
811 # Not going to record a transaction
812 $RT::Handle->Commit();
813 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
814 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
815 return ( $self->Id, 0, $ErrStr );
826 =head2 UpdateFrom822 $MESSAGE
828 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
829 Returns an um. ask me again when the code exists
834 my $simple_update = <<EOF;
836 AddRequestor: jesse\@example.com
839 my $ticket = RT::Ticket->new($RT::SystemUser);
840 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
841 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
842 $ticket->UpdateFrom822($simple_update);
843 is($ticket->Subject, 'target', "changed the subject");
844 my $jesse = RT::User->new($RT::SystemUser);
845 $jesse->LoadByEmail('jesse@example.com');
846 ok ($jesse->Id, "There's a user for jesse");
847 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
857 my %args = $self->_Parse822HeadersForAttributes($content);
861 Queue => $args{'queue'},
862 Subject => $args{'subject'},
863 Status => $args{'status'},
865 Starts => $args{'starts'},
866 Started => $args{'started'},
867 Resolved => $args{'resolved'},
868 Owner => $args{'owner'},
869 Requestor => $args{'requestor'},
871 AdminCc => $args{'admincc'},
872 TimeWorked => $args{'timeworked'},
873 TimeEstimated => $args{'timeestimated'},
874 TimeLeft => $args{'timeleft'},
875 InitialPriority => $args{'initialpriority'},
876 Priority => $args{'priority'},
877 FinalPriority => $args{'finalpriority'},
878 Type => $args{'type'},
879 DependsOn => $args{'dependson'},
880 DependedOnBy => $args{'dependedonby'},
881 RefersTo => $args{'refersto'},
882 ReferredToBy => $args{'referredtoby'},
883 Members => $args{'members'},
884 MemberOf => $args{'memberof'},
885 MIMEObj => $args{'mimeobj'}
888 foreach my $type qw(Requestor Cc Admincc) {
890 foreach my $action ( 'Add', 'Del', '' ) {
892 my $lctag = lc($action) . lc($type);
893 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
895 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
896 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
901 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
906 # Add custom field entries to %ticketargs.
907 # TODO: allow named custom fields
909 /^customfield-(\d+)$/
910 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
913 # for each ticket we've been told to update, iterate through the set of
914 # rfc822 headers and perform that update to the ticket.
917 # {{{ Set basic fields
931 # Resolve the queue from a name to a numeric id.
932 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
933 my $tempqueue = RT::Queue->new($RT::SystemUser);
934 $tempqueue->Load( $ticketargs{'Queue'} );
935 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
940 foreach my $attribute (@attribs) {
941 my $value = $ticketargs{$attribute};
943 if ( $value ne $self->$attribute() ) {
945 my $method = "Set$attribute";
946 my ( $code, $msg ) = $self->$method($value);
948 push @results, $self->loc($attribute) . ': ' . $msg;
953 # We special case owner changing, so we can use ForceOwnerChange
954 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
955 my $ChownType = "Give";
956 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
958 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
959 push ( @results, $msg );
963 # Deal with setting watchers
966 # Acceptable arguments:
973 foreach my $type qw(Requestor Cc AdminCc) {
975 # If we've been given a number of delresses to del, do it.
976 foreach my $address (@{$ticketargs{'Del'.$type}}) {
977 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
978 push (@results, $msg) ;
981 # If we've been given a number of addresses to add, do it.
982 foreach my $address (@{$ticketargs{'Add'.$type}}) {
983 $RT::Logger->debug("Adding $address as a $type");
984 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
985 push (@results, $msg) ;
996 # {{{ _Parse822HeadersForAttributes Content
998 =head2 _Parse822HeadersForAttributes Content
1000 Takes an RFC822 style message and parses its attributes into a hash.
1004 sub _Parse822HeadersForAttributes {
1006 my $content = shift;
1009 my @lines = ( split ( /\n/, $content ) );
1010 while ( defined( my $line = shift @lines ) ) {
1011 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1016 if ( defined( $args{$tag} ) )
1017 { #if we're about to get a second value, make it an array
1018 $args{$tag} = [ $args{$tag} ];
1020 if ( ref( $args{$tag} ) )
1021 { #If it's an array, we want to push the value
1022 push @{ $args{$tag} }, $value;
1024 else { #if there's nothing there, just set the value
1025 $args{$tag} = $value;
1027 } elsif ($line =~ /^$/) {
1029 #TODO: this won't work, since "" isn't of the form "foo:value"
1031 while ( defined( my $l = shift @lines ) ) {
1032 push @{ $args{'content'} }, $l;
1038 foreach my $date qw(due starts started resolved) {
1039 my $dateobj = RT::Date->new($RT::SystemUser);
1040 if ( $args{$date} =~ /^\d+$/ ) {
1041 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1044 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1046 $args{$date} = $dateobj->ISO;
1048 $args{'mimeobj'} = MIME::Entity->new();
1049 $args{'mimeobj'}->build(
1050 Type => ( $args{'contenttype'} || 'text/plain' ),
1051 Data => ($args{'content'} || '')
1061 =head2 Import PARAMHASH
1064 Doesn\'t create a transaction.
1065 Doesn\'t supply queue defaults, etc.
1073 my ( $ErrStr, $QueueObj, $Owner );
1077 EffectiveId => undef,
1081 Owner => $RT::Nobody->Id,
1082 Subject => '[no subject]',
1083 InitialPriority => undef,
1084 FinalPriority => undef,
1095 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1096 $QueueObj = RT::Queue->new($RT::SystemUser);
1097 $QueueObj->Load( $args{'Queue'} );
1099 #TODO error check this and return 0 if it\'s not loading properly +++
1101 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1102 $QueueObj = RT::Queue->new($RT::SystemUser);
1103 $QueueObj->Load( $args{'Queue'}->Id );
1107 "$self " . $args{'Queue'} . " not a recognised queue object." );
1110 #Can't create a ticket without a queue.
1111 unless ( defined($QueueObj) and $QueueObj->Id ) {
1112 $RT::Logger->debug("$self No queue given for ticket creation.");
1113 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1116 #Now that we have a queue, Check the ACLS
1118 $self->CurrentUser->HasRight(
1119 Right => 'CreateTicket',
1125 $self->loc("No permission to create tickets in the queue '[_1]'"
1126 , $QueueObj->Name));
1129 # {{{ Deal with setting the owner
1131 # Attempt to take user object, user name or user id.
1132 # Assign to nobody if lookup fails.
1133 if ( defined( $args{'Owner'} ) ) {
1134 if ( ref( $args{'Owner'} ) ) {
1135 $Owner = $args{'Owner'};
1138 $Owner = new RT::User( $self->CurrentUser );
1139 $Owner->Load( $args{'Owner'} );
1140 if ( !defined( $Owner->id ) ) {
1141 $Owner->Load( $RT::Nobody->id );
1146 #If we have a proposed owner and they don't have the right
1147 #to own a ticket, scream about it and make them not the owner
1150 and ( $Owner->Id != $RT::Nobody->Id )
1153 Object => $QueueObj,
1154 Right => 'OwnTicket'
1160 $RT::Logger->warning( "$self user "
1161 . $Owner->Name . "("
1164 . "as a ticket owner but has no rights to own "
1166 . $QueueObj->Name . "'\n" );
1171 #If we haven't been handed a valid owner, make it nobody.
1172 unless ( defined($Owner) ) {
1173 $Owner = new RT::User( $self->CurrentUser );
1174 $Owner->Load( $RT::Nobody->UserObj->Id );
1179 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1180 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1183 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1184 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1185 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1186 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1188 # If we're coming in with an id, set that now.
1189 my $EffectiveId = undef;
1190 if ( $args{'id'} ) {
1191 $EffectiveId = $args{'id'};
1195 my $id = $self->SUPER::Create(
1197 EffectiveId => $EffectiveId,
1198 Queue => $QueueObj->Id,
1199 Owner => $Owner->Id,
1200 Subject => $args{'Subject'}, # loc
1201 InitialPriority => $args{'InitialPriority'}, # loc
1202 FinalPriority => $args{'FinalPriority'}, # loc
1203 Priority => $args{'InitialPriority'}, # loc
1204 Status => $args{'Status'}, # loc
1205 TimeWorked => $args{'TimeWorked'}, # loc
1206 Type => $args{'Type'}, # loc
1207 Created => $args{'Created'}, # loc
1208 Told => $args{'Told'}, # loc
1209 LastUpdated => $args{'Updated'}, # loc
1210 Resolved => $args{'Resolved'}, # loc
1211 Due => $args{'Due'}, # loc
1214 # If the ticket didn't have an id
1215 # Set the ticket's effective ID now that we've created it.
1216 if ( $args{'id'} ) {
1217 $self->Load( $args{'id'} );
1221 $self->__Set( Field => 'EffectiveId', Value => $id );
1225 $self . "->Import couldn't set EffectiveId: $msg\n" );
1229 my $create_groups_ret = $self->_CreateTicketGroups();
1230 unless ($create_groups_ret) {
1232 "Couldn't create ticket groups for ticket " . $self->Id );
1235 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1238 foreach $watcher ( @{ $args{'Cc'} } ) {
1239 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1241 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1242 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1245 foreach $watcher ( @{ $args{'Requestor'} } ) {
1246 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1250 return ( $self->Id, $ErrStr );
1255 # {{{ Routines dealing with watchers.
1257 # {{{ _CreateTicketGroups
1259 =head2 _CreateTicketGroups
1261 Create the ticket groups and links for this ticket.
1262 This routine expects to be called from Ticket->Create _inside of a transaction_
1264 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1266 It will return true on success and undef on failure.
1270 my $ticket = RT::Ticket->new($RT::SystemUser);
1271 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1272 Owner => $RT::SystemUser->Id,
1274 Requestor => ['jesse@example.com'],
1277 ok ($id, "Ticket $id was created");
1278 ok(my $group = RT::Group->new($RT::SystemUser));
1279 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1280 ok ($group->Id, "Found the requestors object for this ticket");
1282 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1283 $jesse->LoadByEmail('jesse@example.com');
1284 ok($jesse->Id, "Found the jesse rt user");
1287 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1288 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1289 ok ($add_id, "Add succeeded: ($add_msg)");
1290 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1291 $bob->LoadByEmail('bob@fsck.com');
1292 ok($bob->Id, "Found the bob rt user");
1293 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1294 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1295 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1298 $group = RT::Group->new($RT::SystemUser);
1299 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1300 ok ($group->Id, "Found the cc object for this ticket");
1301 $group = RT::Group->new($RT::SystemUser);
1302 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1303 ok ($group->Id, "Found the AdminCc object for this ticket");
1304 $group = RT::Group->new($RT::SystemUser);
1305 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1306 ok ($group->Id, "Found the Owner object for this ticket");
1307 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1314 sub _CreateTicketGroups {
1317 my @types = qw(Requestor Owner Cc AdminCc);
1319 foreach my $type (@types) {
1320 my $type_obj = RT::Group->new($self->CurrentUser);
1321 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1322 Instance => $self->Id,
1325 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1326 $self->Id.": ".$msg);
1336 # {{{ sub OwnerGroup
1340 A constructor which returns an RT::Group object containing the owner of this ticket.
1346 my $owner_obj = RT::Group->new($self->CurrentUser);
1347 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1348 return ($owner_obj);
1354 # {{{ sub AddWatcher
1358 AddWatcher takes a parameter hash. The keys are as follows:
1360 Type One of Requestor, Cc, AdminCc
1362 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1364 Email The email address of the new watcher. If a user with this
1365 email address can't be found, a new nonprivileged user will be created.
1367 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.
1375 PrincipalId => undef,
1380 # XXX, FIXME, BUG: if only email is provided then we only check
1381 # for ModifyTicket right, but must try to get PrincipalId and
1382 # check Watch* rights too if user exist
1385 #If the watcher we're trying to add is for the current user
1386 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1387 # If it's an AdminCc and they don't have
1388 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1389 if ( $args{'Type'} eq 'AdminCc' ) {
1390 unless ( $self->CurrentUserHasRight('ModifyTicket')
1391 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1392 return ( 0, $self->loc('Permission Denied'))
1396 # If it's a Requestor or Cc and they don't have
1397 # 'Watch' or 'ModifyTicket', bail
1398 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1400 unless ( $self->CurrentUserHasRight('ModifyTicket')
1401 or $self->CurrentUserHasRight('Watch') ) {
1402 return ( 0, $self->loc('Permission Denied'))
1406 $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1407 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1411 # If the watcher isn't the current user
1412 # and the current user doesn't have 'ModifyTicket'
1415 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1416 return ( 0, $self->loc("Permission Denied") );
1422 return ( $self->_AddWatcher(%args) );
1425 #This contains the meat of AddWatcher. but can be called from a routine like
1426 # Create, which doesn't need the additional acl check
1432 PrincipalId => undef,
1438 my $principal = RT::Principal->new($self->CurrentUser);
1439 if ($args{'Email'}) {
1440 my $user = RT::User->new($RT::SystemUser);
1441 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1442 # If we can't load the user by email address, let's try to load by username
1444 ($pid,$msg) = $user->Load($args{'Email'})
1447 $args{'PrincipalId'} = $pid;
1450 if ($args{'PrincipalId'}) {
1451 $principal->Load($args{'PrincipalId'});
1455 # If we can't find this watcher, we need to bail.
1456 unless ($principal->Id) {
1457 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1458 return(0, $self->loc("Could not find or create that user"));
1462 my $group = RT::Group->new($self->CurrentUser);
1463 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1464 unless ($group->id) {
1465 return(0,$self->loc("Group not found"));
1468 if ( $group->HasMember( $principal)) {
1470 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1474 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1475 InsideTransaction => 1 );
1477 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1479 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1482 unless ( $args{'Silent'} ) {
1483 $self->_NewTransaction(
1484 Type => 'AddWatcher',
1485 NewValue => $principal->Id,
1486 Field => $args{'Type'}
1490 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1496 # {{{ sub DeleteWatcher
1498 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1501 Deletes a Ticket watcher. Takes two arguments:
1503 Type (one of Requestor,Cc,AdminCc)
1507 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1509 Email (the email address of an existing wathcer)
1518 my %args = ( Type => undef,
1519 PrincipalId => undef,
1523 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1524 return ( 0, $self->loc("No principal specified") );
1526 my $principal = RT::Principal->new( $self->CurrentUser );
1527 if ( $args{'PrincipalId'} ) {
1529 $principal->Load( $args{'PrincipalId'} );
1532 my $user = RT::User->new( $self->CurrentUser );
1533 $user->LoadByEmail( $args{'Email'} );
1534 $principal->Load( $user->Id );
1537 # If we can't find this watcher, we need to bail.
1538 unless ( $principal->Id ) {
1539 return ( 0, $self->loc("Could not find that principal") );
1542 my $group = RT::Group->new( $self->CurrentUser );
1543 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1544 unless ( $group->id ) {
1545 return ( 0, $self->loc("Group not found") );
1549 #If the watcher we're trying to add is for the current user
1550 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) {
1552 # If it's an AdminCc and they don't have
1553 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1554 if ( $args{'Type'} eq 'AdminCc' ) {
1555 unless ( $self->CurrentUserHasRight('ModifyTicket')
1556 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1557 return ( 0, $self->loc('Permission Denied') );
1561 # If it's a Requestor or Cc and they don't have
1562 # 'Watch' or 'ModifyTicket', bail
1563 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1565 unless ( $self->CurrentUserHasRight('ModifyTicket')
1566 or $self->CurrentUserHasRight('Watch') ) {
1567 return ( 0, $self->loc('Permission Denied') );
1571 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1573 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1577 # If the watcher isn't the current user
1578 # and the current user doesn't have 'ModifyTicket' bail
1580 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1581 return ( 0, $self->loc("Permission Denied") );
1587 # see if this user is already a watcher.
1589 unless ( $group->HasMember($principal) ) {
1591 $self->loc( 'That principal is not a [_1] for this ticket',
1595 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1597 $RT::Logger->error( "Failed to delete "
1599 . " as a member of group "
1605 'Could not remove that principal as a [_1] for this ticket',
1609 unless ( $args{'Silent'} ) {
1610 $self->_NewTransaction( Type => 'DelWatcher',
1611 OldValue => $principal->Id,
1612 Field => $args{'Type'} );
1616 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1617 $principal->Object->Name,
1626 =head2 SquelchMailTo [EMAIL]
1628 Takes an optional email address to never email about updates to this ticket.
1631 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1635 my $t = RT::Ticket->new($RT::SystemUser);
1636 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1638 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1640 my @returned = $t->SquelchMailTo('nobody@example.com');
1642 is($#returned, 0, "The ticket has one squelched recipients");
1644 my @names = $t->Attributes->Names;
1645 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1646 @returned = $t->SquelchMailTo('nobody@example.com');
1649 is($#returned, 0, "The ticket has one squelched recipients");
1651 @names = $t->Attributes->Names;
1652 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1655 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1656 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1657 @returned = $t->SquelchMailTo();
1658 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1668 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1672 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1673 unless grep { $_->Content eq $attr }
1674 $self->Attributes->Named('SquelchMailTo');
1677 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1680 my @attributes = $self->Attributes->Named('SquelchMailTo');
1681 return (@attributes);
1685 =head2 UnsquelchMailTo ADDRESS
1687 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1689 Returns a tuple of (status, message)
1693 sub UnsquelchMailTo {
1696 my $address = shift;
1697 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1698 return ( 0, $self->loc("Permission Denied") );
1701 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1702 return ($val, $msg);
1706 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1708 =head2 RequestorAddresses
1710 B<Returns> String: All Ticket Requestor email addresses as a string.
1714 sub RequestorAddresses {
1717 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1721 return ( $self->Requestors->MemberEmailAddressesAsString );
1725 =head2 AdminCcAddresses
1727 returns String: All Ticket AdminCc email addresses as a string
1731 sub AdminCcAddresses {
1734 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1738 return ( $self->AdminCc->MemberEmailAddressesAsString )
1744 returns String: All Ticket Ccs as a string of email addresses
1751 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1755 return ( $self->Cc->MemberEmailAddressesAsString);
1761 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1763 # {{{ sub Requestors
1768 Returns this ticket's Requestors as an RT::Group object
1775 my $group = RT::Group->new($self->CurrentUser);
1776 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1777 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1790 Returns an RT::Group object which contains this ticket's Ccs.
1791 If the user doesn't have "ShowTicket" permission, returns an empty group
1798 my $group = RT::Group->new($self->CurrentUser);
1799 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1800 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1813 Returns an RT::Group object which contains this ticket's AdminCcs.
1814 If the user doesn't have "ShowTicket" permission, returns an empty group
1821 my $group = RT::Group->new($self->CurrentUser);
1822 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1823 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1833 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1836 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1838 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1840 Takes a param hash with the attributes Type and either PrincipalId or Email
1842 Type is one of Requestor, Cc, AdminCc and Owner
1844 PrincipalId is an RT::Principal id, and Email is an email address.
1846 Returns true if the specified principal (or the one corresponding to the
1847 specified address) is a member of the group Type for this ticket.
1849 XX TODO: This should be Memoized.
1856 my %args = ( Type => 'Requestor',
1857 PrincipalId => undef,
1862 # Load the relevant group.
1863 my $group = RT::Group->new($self->CurrentUser);
1864 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1866 # Find the relevant principal.
1867 my $principal = RT::Principal->new($self->CurrentUser);
1868 if (!$args{PrincipalId} && $args{Email}) {
1869 # Look up the specified user.
1870 my $user = RT::User->new($self->CurrentUser);
1871 $user->LoadByEmail($args{Email});
1873 $args{PrincipalId} = $user->PrincipalId;
1876 # A non-existent user can't be a group member.
1880 $principal->Load($args{'PrincipalId'});
1882 # Ask if it has the member in question
1883 return ($group->HasMember($principal));
1888 # {{{ sub IsRequestor
1890 =head2 IsRequestor PRINCIPAL_ID
1892 Takes an RT::Principal id
1893 Returns true if the principal is a requestor of the current ticket.
1902 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1910 =head2 IsCc PRINCIPAL_ID
1912 Takes an RT::Principal id.
1913 Returns true if the principal is a requestor of the current ticket.
1922 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1930 =head2 IsAdminCc PRINCIPAL_ID
1932 Takes an RT::Principal id.
1933 Returns true if the principal is a requestor of the current ticket.
1941 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1951 Takes an RT::User object. Returns true if that user is this ticket's owner.
1952 returns undef otherwise
1960 # no ACL check since this is used in acl decisions
1961 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1965 #Tickets won't yet have owners when they're being created.
1966 unless ( $self->OwnerObj->id ) {
1970 if ( $person->id == $self->OwnerObj->id ) {
1984 # {{{ Routines dealing with queues
1986 # {{{ sub ValidateQueue
1993 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1997 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1998 my $id = $QueueObj->Load($Value);
2014 my $NewQueue = shift;
2016 #Redundant. ACL gets checked in _Set;
2017 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2018 return ( 0, $self->loc("Permission Denied") );
2021 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2022 $NewQueueObj->Load($NewQueue);
2024 unless ( $NewQueueObj->Id() ) {
2025 return ( 0, $self->loc("That queue does not exist") );
2028 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2029 return ( 0, $self->loc('That is the same value') );
2032 $self->CurrentUser->HasRight(
2033 Right => 'CreateTicket',
2034 Object => $NewQueueObj
2038 return ( 0, $self->loc("You may not create requests in that queue.") );
2042 $self->OwnerObj->HasRight(
2043 Right => 'OwnTicket',
2044 Object => $NewQueueObj
2048 my $clone = RT::Ticket->new( $RT::SystemUser );
2049 $clone->Load( $self->Id );
2050 unless ( $clone->Id ) {
2051 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2053 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2054 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2057 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2066 Takes nothing. returns this ticket's queue object
2073 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2075 #We call __Value so that we can avoid the ACL decision and some deep recursion
2076 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2077 return ($queue_obj);
2084 # {{{ Date printing routines
2090 Returns an RT::Date object containing this ticket's due date
2097 my $time = new RT::Date( $self->CurrentUser );
2099 # -1 is RT::Date slang for never
2101 $time->Set( Format => 'sql', Value => $self->Due );
2104 $time->Set( Format => 'unix', Value => -1 );
2112 # {{{ sub DueAsString
2116 Returns this ticket's due date as a human readable string
2122 return $self->DueObj->AsString();
2127 # {{{ sub ResolvedObj
2131 Returns an RT::Date object of this ticket's 'resolved' time.
2138 my $time = new RT::Date( $self->CurrentUser );
2139 $time->Set( Format => 'sql', Value => $self->Resolved );
2145 # {{{ sub SetStarted
2149 Takes a date in ISO format or undef
2150 Returns a transaction id and a message
2151 The client calls "Start" to note that the project was started on the date in $date.
2152 A null date means "now"
2158 my $time = shift || 0;
2160 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2161 return ( 0, self->loc("Permission Denied") );
2164 #We create a date object to catch date weirdness
2165 my $time_obj = new RT::Date( $self->CurrentUser() );
2167 $time_obj->Set( Format => 'ISO', Value => $time );
2170 $time_obj->SetToNow();
2173 #Now that we're starting, open this ticket
2174 #TODO do we really want to force this as policy? it should be a scrip
2176 #We need $TicketAsSystem, in case the current user doesn't have
2179 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2180 $TicketAsSystem->Load( $self->Id );
2181 if ( $TicketAsSystem->Status eq 'new' ) {
2182 $TicketAsSystem->Open();
2185 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2191 # {{{ sub StartedObj
2195 Returns an RT::Date object which contains this ticket's
2203 my $time = new RT::Date( $self->CurrentUser );
2204 $time->Set( Format => 'sql', Value => $self->Started );
2214 Returns an RT::Date object which contains this ticket's
2222 my $time = new RT::Date( $self->CurrentUser );
2223 $time->Set( Format => 'sql', Value => $self->Starts );
2233 Returns an RT::Date object which contains this ticket's
2241 my $time = new RT::Date( $self->CurrentUser );
2242 $time->Set( Format => 'sql', Value => $self->Told );
2248 # {{{ sub ToldAsString
2252 A convenience method that returns ToldObj->AsString
2254 TODO: This should be deprecated
2260 if ( $self->Told ) {
2261 return $self->ToldObj->AsString();
2270 # {{{ sub TimeWorkedAsString
2272 =head2 TimeWorkedAsString
2274 Returns the amount of time worked on this ticket as a Text String
2278 sub TimeWorkedAsString {
2280 return "0" unless $self->TimeWorked;
2282 #This is not really a date object, but if we diff a number of seconds
2283 #vs the epoch, we'll get a nice description of time worked.
2285 my $worked = new RT::Date( $self->CurrentUser );
2287 #return the #of minutes worked turned into seconds and written as
2288 # a simple text string
2290 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2297 # {{{ Routines dealing with correspondence/comments
2303 Comment on this ticket.
2304 Takes a hashref with the following attributes:
2305 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2308 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2310 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2311 They will, however, be prepared and you'll be able to access them through the TransactionObj
2313 Returns: Transaction id, Error Message, Transaction Object
2314 (note the different order from Create()!)
2321 my %args = ( CcMessageTo => undef,
2322 BccMessageTo => undef,
2329 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2330 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2331 return ( 0, $self->loc("Permission Denied"), undef );
2333 $args{'NoteType'} = 'Comment';
2335 if ($args{'DryRun'}) {
2336 $RT::Handle->BeginTransaction();
2337 $args{'CommitScrips'} = 0;
2340 my @results = $self->_RecordNote(%args);
2341 if ($args{'DryRun'}) {
2342 $RT::Handle->Rollback();
2349 # {{{ sub Correspond
2353 Correspond on this ticket.
2354 Takes a hashref with the following attributes:
2357 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2359 if there's no MIMEObj, Content is used to build a MIME::Entity object
2361 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2362 They will, however, be prepared and you'll be able to access them through the TransactionObj
2364 Returns: Transaction id, Error Message, Transaction Object
2365 (note the different order from Create()!)
2372 my %args = ( CcMessageTo => undef,
2373 BccMessageTo => undef,
2379 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2380 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2381 return ( 0, $self->loc("Permission Denied"), undef );
2384 $args{'NoteType'} = 'Correspond';
2385 if ($args{'DryRun'}) {
2386 $RT::Handle->BeginTransaction();
2387 $args{'CommitScrips'} = 0;
2390 my @results = $self->_RecordNote(%args);
2392 #Set the last told date to now if this isn't mail from the requestor.
2393 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2394 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2396 if ($args{'DryRun'}) {
2397 $RT::Handle->Rollback();
2406 # {{{ sub _RecordNote
2410 the meat of both comment and correspond.
2412 Performs no access control checks. hence, dangerous.
2419 my %args = ( CcMessageTo => undef,
2420 BccMessageTo => undef,
2427 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2428 return ( 0, $self->loc("No message attached"), undef );
2430 unless ( $args{'MIMEObj'} ) {
2431 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2432 ref $args{'Content'}
2434 : [ $args{'Content'} ]
2438 # convert text parts into utf-8
2439 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2441 # If we've been passed in CcMessageTo and BccMessageTo fields,
2442 # add them to the mime object for passing on to the transaction handler
2443 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2446 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2447 undef, $args{'CcMessageTo'}
2449 if defined $args{'CcMessageTo'};
2450 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2451 RT::User::CanonicalizeEmailAddress(
2452 undef, $args{'BccMessageTo'}
2454 if defined $args{'BccMessageTo'};
2456 # If this is from an external source, we need to come up with its
2457 # internal Message-ID now, so all emails sent because of this
2458 # message have a common Message-ID
2459 unless ($args{'MIMEObj'}->head->get('Message-ID')
2460 =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) {
2461 $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2463 . $RT::VERSION . "-"
2465 . CORE::time() . "-"
2466 . int(rand(2000)) . '.'
2469 . "0" . "@" # Email sent
2474 #Record the correspondence (write the transaction)
2475 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2476 Type => $args{'NoteType'},
2477 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2478 TimeTaken => $args{'TimeTaken'},
2479 MIMEObj => $args{'MIMEObj'},
2480 CommitScrips => $args{'CommitScrips'},
2484 $RT::Logger->err("$self couldn't init a transaction $msg");
2485 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2488 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2500 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2503 my $type = shift || "";
2505 unless ( $self->{"$field$type"} ) {
2506 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2507 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2508 # Maybe this ticket is a merged ticket
2509 my $Tickets = new RT::Tickets( $self->CurrentUser );
2510 # at least to myself
2511 $self->{"$field$type"}->Limit( FIELD => $field,
2512 VALUE => $self->URI,
2513 ENTRYAGGREGATOR => 'OR' );
2514 $Tickets->Limit( FIELD => 'EffectiveId',
2515 VALUE => $self->EffectiveId );
2516 while (my $Ticket = $Tickets->Next) {
2517 $self->{"$field$type"}->Limit( FIELD => $field,
2518 VALUE => $Ticket->URI,
2519 ENTRYAGGREGATOR => 'OR' );
2521 $self->{"$field$type"}->Limit( FIELD => 'Type',
2526 return ( $self->{"$field$type"} );
2531 # {{{ sub DeleteLink
2535 Delete a link. takes a paramhash of Base, Target and Type.
2536 Either Base or Target must be null. The null value will
2537 be replaced with this ticket\'s id
2551 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2552 $RT::Logger->debug("No permission to delete links\n");
2553 return ( 0, $self->loc('Permission Denied'))
2557 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2560 $RT::Logger->debug("Couldn't find that link\n");
2564 my ($direction, $remote_link);
2566 if ( $args{'Base'} ) {
2567 $remote_link = $args{'Base'};
2568 $direction = 'Target';
2570 elsif ( $args{'Target'} ) {
2571 $remote_link = $args{'Target'};
2575 if ( $args{'Silent'} ) {
2576 return ( $val, $Msg );
2579 my $remote_uri = RT::URI->new( $self->CurrentUser );
2580 $remote_uri->FromURI( $remote_link );
2582 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2583 Type => 'DeleteLink',
2584 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2585 OldValue => $remote_uri->URI || $remote_link,
2589 if ( $remote_uri->IsLocal ) {
2591 my $OtherObj = $remote_uri->Object;
2592 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
2593 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2594 : $LINKDIRMAP{$args{'Type'}}->{Target},
2595 OldValue => $self->URI,
2596 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2600 return ( $Trans, $Msg );
2610 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2616 my %args = ( Target => '',
2623 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2624 return ( 0, $self->loc("Permission Denied") );
2628 $self->_AddLink(%args);
2633 Private non-acled variant of AddLink so that links can be added during create.
2639 my %args = ( Target => '',
2645 # {{{ If the other URI is an RT::Ticket, we want to make sure the user
2646 # can modify it too...
2647 my $other_ticket_uri = RT::URI->new($self->CurrentUser);
2649 if ( $args{'Target'} ) {
2650 $other_ticket_uri->FromURI( $args{'Target'} );
2653 elsif ( $args{'Base'} ) {
2654 $other_ticket_uri->FromURI( $args{'Base'} );
2657 unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) {
2658 my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'})
2659 : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
2660 $RT::Logger->warning( "$self $msg\n" );
2665 if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') {
2666 my $object = $other_ticket_uri->Resolver->Object;
2668 if ( UNIVERSAL::isa( $object, 'RT::Ticket' )
2670 && !$object->CurrentUserHasRight('ModifyTicket') )
2672 return ( 0, $self->loc("Permission Denied") );
2679 my ($val, $Msg) = $self->SUPER::_AddLink(%args);
2682 return ($val, $Msg);
2685 my ($direction, $remote_link);
2686 if ( $args{'Target'} ) {
2687 $remote_link = $args{'Target'};
2688 $direction = 'Base';
2689 } elsif ( $args{'Base'} ) {
2690 $remote_link = $args{'Base'};
2691 $direction = 'Target';
2694 # Don't write the transaction if we're doing this on create
2695 if ( $args{'Silent'} ) {
2696 return ( $val, $Msg );
2699 my $remote_uri = RT::URI->new( $self->CurrentUser );
2700 $remote_uri->FromURI( $remote_link );
2702 #Write the transaction
2703 my ( $Trans, $Msg, $TransObj ) =
2704 $self->_NewTransaction(Type => 'AddLink',
2705 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2706 NewValue => $remote_uri->URI || $remote_link,
2709 if ( $remote_uri->IsLocal ) {
2711 my $OtherObj = $remote_uri->Object;
2712 my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
2713 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2714 : $LINKDIRMAP{$args{'Type'}}->{Target},
2715 NewValue => $self->URI,
2716 ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2719 return ( $val, $Msg );
2731 MergeInto take the id of the ticket to merge this ticket into.
2736 my $t1 = RT::Ticket->new($RT::SystemUser);
2737 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2739 my $t2 = RT::Ticket->new($RT::SystemUser);
2740 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2742 my ($msg, $val) = $t1->MergeInto($t2->id);
2744 $t1 = RT::Ticket->new($RT::SystemUser);
2745 is ($t1->id, undef, "ok. we've got a blank ticket1");
2748 is ($t1->id, $t2->id);
2750 is ($t1->Requestors->MembersObj->Count, 2);
2759 my $ticket_id = shift;
2761 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2762 return ( 0, $self->loc("Permission Denied") );
2765 # Load up the new ticket.
2766 my $MergeInto = RT::Ticket->new($RT::SystemUser);
2767 $MergeInto->Load($ticket_id);
2769 # make sure it exists.
2770 unless ( $MergeInto->Id ) {
2771 return ( 0, $self->loc("New ticket doesn't exist") );
2774 # Make sure the current user can modify the new ticket.
2775 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2776 return ( 0, $self->loc("Permission Denied") );
2779 $RT::Handle->BeginTransaction();
2781 # We use EffectiveId here even though it duplicates information from
2782 # the links table becasue of the massive performance hit we'd take
2783 # by trying to do a separate database query for merge info everytime
2786 #update this ticket's effective id to the new ticket's id.
2787 my ( $id_val, $id_msg ) = $self->__Set(
2788 Field => 'EffectiveId',
2789 Value => $MergeInto->Id()
2793 $RT::Handle->Rollback();
2794 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2797 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2799 unless ($status_val) {
2800 $RT::Handle->Rollback();
2801 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2802 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2806 # update all the links that point to that old ticket
2807 my $old_links_to = RT::Links->new($self->CurrentUser);
2808 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2811 while (my $link = $old_links_to->Next) {
2812 if (exists $old_seen{$link->Base."-".$link->Type}) {
2815 elsif ($link->Base eq $MergeInto->URI) {
2818 # First, make sure the link doesn't already exist. then move it over.
2819 my $tmp = RT::Link->new($RT::SystemUser);
2820 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2824 $link->SetTarget($MergeInto->URI);
2825 $link->SetLocalTarget($MergeInto->id);
2827 $old_seen{$link->Base."-".$link->Type} =1;
2832 my $old_links_from = RT::Links->new($self->CurrentUser);
2833 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2835 while (my $link = $old_links_from->Next) {
2836 if (exists $old_seen{$link->Type."-".$link->Target}) {
2839 if ($link->Target eq $MergeInto->URI) {
2842 # First, make sure the link doesn't already exist. then move it over.
2843 my $tmp = RT::Link->new($RT::SystemUser);
2844 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2848 $link->SetBase($MergeInto->URI);
2849 $link->SetLocalBase($MergeInto->id);
2850 $old_seen{$link->Type."-".$link->Target} =1;
2856 # Update time fields
2857 foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2859 my $mutator = "Set$type";
2860 $MergeInto->$mutator(
2861 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2864 #add all of this ticket's watchers to that ticket.
2865 foreach my $watcher_type qw(Requestors Cc AdminCc) {
2867 my $people = $self->$watcher_type->MembersObj;
2868 my $addwatcher_type = $watcher_type;
2869 $addwatcher_type =~ s/s$//;
2871 while ( my $watcher = $people->Next ) {
2873 my ($val, $msg) = $MergeInto->_AddWatcher(
2874 Type => $addwatcher_type,
2876 PrincipalId => $watcher->MemberId
2879 $RT::Logger->warning($msg);
2885 #find all of the tickets that were merged into this ticket.
2886 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2887 $old_mergees->Limit(
2888 FIELD => 'EffectiveId',
2893 # update their EffectiveId fields to the new ticket's id
2894 while ( my $ticket = $old_mergees->Next() ) {
2895 my ( $val, $msg ) = $ticket->__Set(
2896 Field => 'EffectiveId',
2897 Value => $MergeInto->Id()
2901 #make a new link: this ticket is merged into that other ticket.
2902 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2904 $MergeInto->_SetLastUpdated;
2906 $RT::Handle->Commit();
2907 return ( 1, $self->loc("Merge Successful") );
2914 # {{{ Routines dealing with ownership
2920 Takes nothing and returns an RT::User object of
2928 #If this gets ACLed, we lose on a rights check in User.pm and
2929 #get deep recursion. if we need ACLs here, we need
2930 #an equiv without ACLs
2932 my $owner = new RT::User( $self->CurrentUser );
2933 $owner->Load( $self->__Value('Owner') );
2935 #Return the owner object
2941 # {{{ sub OwnerAsString
2943 =head2 OwnerAsString
2945 Returns the owner's email address
2951 return ( $self->OwnerObj->EmailAddress );
2961 Takes two arguments:
2962 the Id or Name of the owner
2963 and (optionally) the type of the SetOwner Transaction. It defaults
2964 to 'Give'. 'Steal' is also a valid option.
2968 my $root = RT::User->new($RT::SystemUser);
2969 $root->Load('root');
2970 ok ($root->Id, "Loaded the root user");
2971 my $t = RT::Ticket->new($RT::SystemUser);
2973 $t->SetOwner('root');
2974 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
2976 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
2977 my $txns = RT::Transactions->new($RT::SystemUser);
2978 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2979 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
2980 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
2981 my $steal = $txns->First;
2982 ok($steal->OldValue == $root->Id , "Stolen from root");
2983 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2991 my $NewOwner = shift;
2992 my $Type = shift || "Give";
2994 # must have ModifyTicket rights
2995 # or TakeTicket/StealTicket and $NewOwner is self
2996 # see if it's a take
2997 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
2998 unless ( $self->CurrentUserHasRight('ModifyTicket')
2999 || $self->CurrentUserHasRight('TakeTicket') ) {
3000 return ( 0, $self->loc("Permission Denied") );
3004 # see if it's a steal
3005 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
3006 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
3008 unless ( $self->CurrentUserHasRight('ModifyTicket')
3009 || $self->CurrentUserHasRight('StealTicket') ) {
3010 return ( 0, $self->loc("Permission Denied") );
3014 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3015 return ( 0, $self->loc("Permission Denied") );
3018 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3019 my $OldOwnerObj = $self->OwnerObj;
3021 $NewOwnerObj->Load($NewOwner);
3022 if ( !$NewOwnerObj->Id ) {
3023 return ( 0, $self->loc("That user does not exist") );
3026 #If thie ticket has an owner and it's not the current user
3028 if ( ( $Type ne 'Steal' )
3029 and ( $Type ne 'Force' )
3030 and #If we're not stealing
3031 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
3032 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
3033 ) { #and it's not us
3036 "You can only reassign tickets that you own or that are unowned" ) );
3039 #If we've specified a new owner and that user can't modify the ticket
3040 elsif ( ( $NewOwnerObj->Id )
3041 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
3044 return ( 0, $self->loc("That user may not own tickets in that queue") );
3047 #If the ticket has an owner and it's the new owner, we don't need
3049 elsif ( ( $self->OwnerObj )
3050 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
3051 return ( 0, $self->loc("That user already owns that ticket") );
3054 $RT::Handle->BeginTransaction();
3056 # Delete the owner in the owner group, then add a new one
3057 # TODO: is this safe? it's not how we really want the API to work
3058 # for most things, but it's fast.
3059 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3061 $RT::Handle->Rollback();
3062 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3065 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3066 PrincipalId => $NewOwnerObj->PrincipalId,
3067 InsideTransaction => 1 );
3069 $RT::Handle->Rollback();
3070 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3073 # We call set twice with slightly different arguments, so
3074 # as to not have an SQL transaction span two RT transactions
3076 my ( $val, $msg ) = $self->_Set(
3078 RecordTransaction => 0,
3079 Value => $NewOwnerObj->Id,
3081 TransactionType => $Type,
3082 CheckACL => 0, # don't check acl
3086 $RT::Handle->Rollback;
3087 return ( 0, $self->loc("Could not change owner. ") . $msg );
3090 $RT::Handle->Commit();
3092 ($val, $msg) = $self->_NewTransaction(
3095 NewValue => $NewOwnerObj->Id,
3096 OldValue => $OldOwnerObj->Id,
3101 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3102 $OldOwnerObj->Name, $NewOwnerObj->Name );
3104 # TODO: make sure the trans committed properly
3106 return ( $val, $msg );
3115 A convenince method to set the ticket's owner to the current user
3121 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3130 Convenience method to set the owner to 'nobody' if the current user is the owner.
3136 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3145 A convenience method to change the owner of the current ticket to the
3146 current user. Even if it's owned by another user.
3153 if ( $self->IsOwner( $self->CurrentUser ) ) {
3154 return ( 0, $self->loc("You already own this ticket") );
3157 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3167 # {{{ Routines dealing with status
3169 # {{{ sub ValidateStatus
3171 =head2 ValidateStatus STATUS
3173 Takes a string. Returns true if that status is a valid status for this ticket.
3174 Returns false otherwise.
3178 sub ValidateStatus {
3182 #Make sure the status passed in is valid
3183 unless ( $self->QueueObj->IsValidStatus($status) ) {
3195 =head2 SetStatus STATUS
3197 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3199 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.
3203 my $tt = RT::Ticket->new($RT::SystemUser);
3204 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3207 is($tt->Status, 'new', "New ticket is created as new");
3209 ($id, $msg) = $tt->SetStatus('open');
3211 like($msg, qr/open/i, "Status message is correct");
3212 ($id, $msg) = $tt->SetStatus('resolved');
3214 like($msg, qr/resolved/i, "Status message is correct");
3215 ($id, $msg) = $tt->SetStatus('resolved');
3229 $args{Status} = shift;
3236 if ( $args{Status} eq 'deleted') {
3237 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3238 return ( 0, $self->loc('Permission Denied') );
3241 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3242 return ( 0, $self->loc('Permission Denied') );
3246 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3247 return (0, $self->loc('That ticket has unresolved dependencies'));
3250 my $now = RT::Date->new( $self->CurrentUser );
3253 #If we're changing the status from new, record that we've started
3254 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3256 #Set the Started time to "now"
3257 $self->_Set( Field => 'Started',
3259 RecordTransaction => 0 );
3262 #When we close a ticket, set the 'Resolved' attribute to now.
3263 # It's misnamed, but that's just historical.
3264 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3265 $self->_Set( Field => 'Resolved',
3267 RecordTransaction => 0 );
3270 #Actually update the status
3271 my ($val, $msg)= $self->_Set( Field => 'Status',
3272 Value => $args{Status},
3275 TransactionType => 'Status' );
3286 Takes no arguments. Marks this ticket for garbage collection
3292 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3293 return $self->Delete;
3298 return ( $self->SetStatus('deleted') );
3300 # TODO: garbage collection
3309 Sets this ticket's status to stalled
3315 return ( $self->SetStatus('stalled') );
3324 Sets this ticket's status to rejected
3330 return ( $self->SetStatus('rejected') );
3339 Sets this ticket\'s status to Open
3345 return ( $self->SetStatus('open') );
3354 Sets this ticket\'s status to Resolved
3360 return ( $self->SetStatus('resolved') );
3368 # {{{ Actions + Routines dealing with transactions
3370 # {{{ sub SetTold and _SetTold
3372 =head2 SetTold ISO [TIMETAKEN]
3374 Updates the told and records a transaction
3381 $told = shift if (@_);
3382 my $timetaken = shift || 0;
3384 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3385 return ( 0, $self->loc("Permission Denied") );
3388 my $datetold = new RT::Date( $self->CurrentUser );
3390 $datetold->Set( Format => 'iso',
3394 $datetold->SetToNow();
3397 return ( $self->_Set( Field => 'Told',
3398 Value => $datetold->ISO,
3399 TimeTaken => $timetaken,
3400 TransactionType => 'Told' ) );
3405 Updates the told without a transaction or acl check. Useful when we're sending replies.
3412 my $now = new RT::Date( $self->CurrentUser );
3415 #use __Set to get no ACLs ;)
3416 return ( $self->__Set( Field => 'Told',
3417 Value => $now->ISO ) );
3422 =head2 TransactionBatch
3424 Returns an array reference of all transactions created on this ticket during
3425 this ticket object's lifetime, or undef if there were none.
3427 Only works when the $RT::UseTransactionBatch config variable is set to true.
3431 sub TransactionBatch {
3433 return $self->{_TransactionBatch};
3439 # DESTROY methods need to localize $@, or it may unset it. This
3440 # causes $m->abort to not bubble all of the way up. See perlbug
3441 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3444 # The following line eliminates reentrancy.
3445 # It protects against the fact that perl doesn't deal gracefully
3446 # when an object's refcount is changed in its destructor.
3447 return if $self->{_Destroyed}++;
3449 my $batch = $self->TransactionBatch or return;
3451 RT::Scrips->new($RT::SystemUser)->Apply(
3452 Stage => 'TransactionBatch',
3454 TransactionObj => $batch->[0],
3455 Type => join(',', (map { $_->Type } @{$batch}) )
3461 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3463 # {{{ sub _OverlayAccessible
3465 sub _OverlayAccessible {
3467 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3468 Queue => { 'read' => 1, 'write' => 1 },
3469 Requestors => { 'read' => 1, 'write' => 1 },
3470 Owner => { 'read' => 1, 'write' => 1 },
3471 Subject => { 'read' => 1, 'write' => 1 },
3472 InitialPriority => { 'read' => 1, 'write' => 1 },
3473 FinalPriority => { 'read' => 1, 'write' => 1 },
3474 Priority => { 'read' => 1, 'write' => 1 },
3475 Status => { 'read' => 1, 'write' => 1 },
3476 TimeEstimated => { 'read' => 1, 'write' => 1 },
3477 TimeWorked => { 'read' => 1, 'write' => 1 },
3478 TimeLeft => { 'read' => 1, 'write' => 1 },
3479 Told => { 'read' => 1, 'write' => 1 },
3480 Resolved => { 'read' => 1 },
3481 Type => { 'read' => 1 },
3482 Starts => { 'read' => 1, 'write' => 1 },
3483 Started => { 'read' => 1, 'write' => 1 },
3484 Due => { 'read' => 1, 'write' => 1 },
3485 Creator => { 'read' => 1, 'auto' => 1 },
3486 Created => { 'read' => 1, 'auto' => 1 },
3487 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3488 LastUpdated => { 'read' => 1, 'auto' => 1 }
3500 my %args = ( Field => undef,
3503 RecordTransaction => 1,
3506 TransactionType => 'Set',
3509 if ($args{'CheckACL'}) {
3510 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3511 return ( 0, $self->loc("Permission Denied"));
3515 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3516 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3517 return(0, $self->loc("Internal Error"));
3520 #if the user is trying to modify the record
3522 #Take care of the old value we really don't want to get in an ACL loop.
3523 # so ask the super::_Value
3524 my $Old = $self->SUPER::_Value("$args{'Field'}");
3527 if ( $args{'UpdateTicket'} ) {
3530 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3531 Value => $args{'Value'} );
3533 #If we can't actually set the field to the value, don't record
3534 # a transaction. instead, get out of here.
3535 return ( 0, $msg ) unless $ret;
3538 if ( $args{'RecordTransaction'} == 1 ) {
3540 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3541 Type => $args{'TransactionType'},
3542 Field => $args{'Field'},
3543 NewValue => $args{'Value'},
3545 TimeTaken => $args{'TimeTaken'},
3547 return ( $Trans, scalar $TransObj->BriefDescription );
3550 return ( $ret, $msg );
3560 Takes the name of a table column.
3561 Returns its value as a string, if the user passes an ACL check
3570 #if the field is public, return it.
3571 if ( $self->_Accessible( $field, 'public' ) ) {
3573 #$RT::Logger->debug("Skipping ACL check for $field\n");
3574 return ( $self->SUPER::_Value($field) );
3578 #If the current user doesn't have ACLs, don't let em at it.
3580 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3583 return ( $self->SUPER::_Value($field) );
3589 # {{{ sub _UpdateTimeTaken
3591 =head2 _UpdateTimeTaken
3593 This routine will increment the timeworked counter. it should
3594 only be called from _NewTransaction
3598 sub _UpdateTimeTaken {
3600 my $Minutes = shift;
3603 $Total = $self->SUPER::_Value("TimeWorked");
3604 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3606 Field => "TimeWorked",
3617 # {{{ Routines dealing with ACCESS CONTROL
3619 # {{{ sub CurrentUserHasRight
3621 =head2 CurrentUserHasRight
3623 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3624 1 if the user has that right. It returns 0 if the user doesn't have that right.
3628 sub CurrentUserHasRight {
3634 Principal => $self->CurrentUser->UserObj(),
3647 Takes a paramhash with the attributes 'Right' and 'Principal'
3648 'Right' is a ticket-scoped textual right from RT::ACE
3649 'Principal' is an RT::User object
3651 Returns 1 if the principal has the right. Returns undef if not.
3663 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3666 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3671 $args{'Principal'}->HasRight(
3673 Right => $args{'Right'}
3682 # {{{ sub Transactions
3686 Returns an RT::Transactions object of all transactions on this ticket
3693 my $transactions = RT::Transactions->new( $self->CurrentUser );
3695 #If the user has no rights, return an empty object
3696 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3697 $transactions->LimitToTicket($self->id);
3699 # if the user may not see comments do not return them
3700 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3701 $transactions->Limit(
3706 $transactions->Limit(
3709 VALUE => "CommentEmailRecord",
3710 ENTRYAGGREGATOR => 'AND'
3716 return ($transactions);
3722 # {{{ TransactionCustomFields
3724 =head2 TransactionCustomFields
3726 Returns the custom fields that transactions on tickets will ahve.
3730 sub TransactionCustomFields {
3732 return $self->QueueObj->TicketTransactionCustomFields;
3737 # {{{ sub CustomFieldValues
3739 =head2 CustomFieldValues
3741 # Do name => id mapping (if needed) before falling back to
3742 # RT::Record's CustomFieldValues
3748 sub CustomFieldValues {
3751 if ( $field and $field !~ /^\d+$/ ) {
3752 my $cf = RT::CustomField->new( $self->CurrentUser );
3753 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3754 unless ( $cf->id ) {
3755 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3757 unless ( $cf->id ) {
3758 # If we didn't find a valid cfid, give up.
3759 return RT::CustomFieldValues->new($self->CurrentUser);
3763 return $self->SUPER::CustomFieldValues($field);
3768 # {{{ sub CustomFieldLookupType
3770 =head2 CustomFieldLookupType
3772 Returns the RT::Ticket lookup type, which can be passed to
3773 RT::CustomField->Create() via the 'LookupType' hash key.
3779 sub CustomFieldLookupType {
3780 "RT::Queue-RT::Ticket";
3787 Jesse Vincent, jesse@bestpractical.com