3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
29 my $ticket = new RT::Ticket($CurrentUser);
30 $ticket->Load($ticket_id);
34 This module lets you manipulate RT\'s ticket object.
42 ok(my $testqueue = RT::Queue->new($RT::SystemUser));
43 ok($testqueue->Create( Name => 'ticket tests'));
44 ok($testqueue->Id != 0);
45 use_ok(RT::CustomField);
46 ok(my $testcf = RT::CustomField->new($RT::SystemUser));
47 ok($testcf->Create( Name => 'selectmulti',
48 Queue => $testqueue->id,
49 Type => 'SelectMultiple'));
50 ok($testcf->AddValue ( Name => 'Value1',
52 Description => 'A testing value'));
53 ok($testcf->AddValue ( Name => 'Value2',
55 Description => 'Another testing value'));
56 ok($testcf->AddValue ( Name => 'Value3',
58 Description => 'Yet Another testing value'));
60 ok($testcf->Values->Count == 3);
64 my $u = RT::User->new($RT::SystemUser);
66 ok ($u->Id, "Found the root user");
67 ok(my $t = RT::Ticket->new($RT::SystemUser));
68 ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
73 ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
74 ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
76 ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
77 ok($t->CustomFieldValues($testcf->Id)->Count == 1);
78 ok($t->CustomFieldValues($testcf->Id)->First &&
79 $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
81 ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
83 ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
84 ok($t->CustomFieldValues($testcf->Id)->Count == 0);
86 ok(my $t2 = RT::Ticket->new($RT::SystemUser));
88 ok($t2->Subject eq 'Testing');
89 ok($t2->QueueObj->Id eq $testqueue->id);
90 ok($t2->OwnerObj->Id == $u->Id);
92 my $t3 = RT::Ticket->new($RT::SystemUser);
93 my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
96 my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
98 ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
99 my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
101 ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
102 my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
104 ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
105 ok($t->CustomFieldValues($testcf->Id)->Count == 2,
106 "This ticket has 2 custom field values");
107 ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
108 "This ticket has 1 custom field value");
115 no warnings qw(redefine);
122 use RT::CustomFields;
123 use RT::TicketCustomFieldValues;
125 use RT::URI::fsck_com_rt;
131 ok(require RT::Ticket, "Loading the RT::Ticket library");
140 # A helper table for relationships mapping to make it easier
141 # to build and parse links between tickets
143 use vars '%LINKTYPEMAP';
146 MemberOf => { Type => 'MemberOf',
148 Members => { Type => 'MemberOf',
150 HasMember => { Type => 'MemberOf',
152 RefersTo => { Type => 'RefersTo',
154 ReferredToBy => { Type => 'RefersTo',
156 DependsOn => { Type => 'DependsOn',
158 DependedOnBy => { Type => 'DependsOn',
166 # A helper table for relationships mapping to make it easier
167 # to build and parse links between tickets
169 use vars '%LINKDIRMAP';
172 MemberOf => { Base => 'MemberOf',
173 Target => 'HasMember', },
174 RefersTo => { Base => 'RefersTo',
175 Target => 'ReferredToBy', },
176 DependsOn => { Base => 'DependsOn',
177 Target => 'DependedOnBy', },
187 Takes a single argument. This can be a ticket id, ticket alias or
188 local ticket uri. If the ticket can't be loaded, returns undef.
189 Otherwise, returns the ticket id.
197 #TODO modify this routine to look at EffectiveId and do the recursive load
198 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
200 #If it's a local URI, turn it into a ticket id
201 if ( $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
205 #If it's a remote URI, we're going to punt for now
206 elsif ( $id =~ '://' ) {
210 #If we have an integer URI, load the ticket
211 if ( $id =~ /^\d+$/ ) {
212 my $ticketid = $self->LoadById($id);
215 $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
220 #It's not a URI. It's not a numerical ticket ID. Punt!
225 #If we're merged, resolve the merge.
226 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
227 return ( $self->Load( $self->EffectiveId ) );
230 #Ok. we're loaded. lets get outa here.
231 return ( $self->Id );
241 Given a local ticket URI, loads the specified ticket.
249 if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
251 return ( $self->Load($id) );
264 Arguments: ARGS is a hash of named parameters. Valid parameters are:
267 Queue - Either a Queue object or a Queue Name
268 Requestor - A reference to a list of RT::User objects, email addresses or RT user Names
269 Cc - A reference to a list of RT::User objects, email addresses or Names
270 AdminCc - A reference to a list of RT::User objects, email addresses or Names
271 Type -- The ticket\'s type. ignore this for now
272 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
273 Subject -- A string describing the subject of the ticket
274 InitialPriority -- an integer from 0 to 99
275 FinalPriority -- an integer from 0 to 99
276 Status -- any valid status (Defined in RT::Queue)
277 TimeEstimated -- an integer. estimated time for this task in minutes
278 TimeWorked -- an integer. time worked so far in minutes
279 TimeLeft -- an integer. time remaining in minutes
280 Starts -- an ISO date describing the ticket\'s start date and time in GMT
281 Due -- an ISO date describing the ticket\'s due date and time in GMT
282 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
283 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
286 Returns: TICKETID, Transaction Object, Error Message
291 my $t = RT::Ticket->new($RT::SystemUser);
293 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");
295 ok ( my $id = $t->Id, "Got ticket id");
296 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
297 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
298 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
307 my %args = ( id => undef,
308 EffectiveId => undef,
316 InitialPriority => undef,
317 FinalPriority => undef,
328 _RecordTransaction => 1,
334 my ( $ErrStr, $Owner, $resolved );
335 my (@non_fatal_errors);
337 my $QueueObj = RT::Queue->new($RT::SystemUser);
340 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
341 $QueueObj->Load( $args{'Queue'} );
343 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
344 $QueueObj->Load( $args{'Queue'}->Id );
347 $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object.");
351 #Can't create a ticket without a queue.
352 unless ( defined($QueueObj) && $QueueObj->Id ) {
353 $RT::Logger->debug("$self No queue given for ticket creation.");
354 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
357 #Now that we have a queue, Check the ACLS
358 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket',
359 Object => $QueueObj )
362 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name ) );
365 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
366 return ( 0, 0, $self->loc('Invalid value for status') );
370 #Since we have a queue, we can set queue defaults
373 # If there's no queue default initial priority and it's not set, set it to 0
374 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
375 unless ( defined $args{'InitialPriority'} );
379 # If there's no queue default final priority and it's not set, set it to 0
380 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
381 unless ( defined $args{'FinalPriority'} );
383 # Priority may have changed from InitialPriority, for the case
384 # where we're importing tickets (eg, from an older RT version.)
385 my $priority = $args{'Priority'} || $args{'InitialPriority'};
389 #TODO we should see what sort of due date we're getting, rather +
390 # than assuming it's in ISO format.
392 #Set the due date. if we didn't get fed one, use the queue default due in
393 my $Due = new RT::Date( $self->CurrentUser );
395 if ( $args{'Due'} ) {
396 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
398 elsif ( $QueueObj->DefaultDueIn ) {
400 $Due->AddDays( $QueueObj->DefaultDueIn );
403 my $Starts = new RT::Date( $self->CurrentUser );
404 if ( defined $args{'Starts'} ) {
405 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
408 my $Started = new RT::Date( $self->CurrentUser );
409 if ( defined $args{'Started'} ) {
410 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
413 my $Resolved = new RT::Date( $self->CurrentUser );
414 if ( defined $args{'Resolved'} ) {
415 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
419 #If the status is an inactive status, set the resolved date
420 if ($QueueObj->IsInactiveStatus($args{'Status'}) && !$args{'Resolved'}) {
421 $RT::Logger->debug("Got a ".$args{'Status'} . "ticket with a resolved of ".$args{'Resolved'});
427 # {{{ Dealing with time fields
429 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
430 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
431 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
435 # {{{ Deal with setting the owner
437 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
438 $Owner = $args{'Owner'};
441 #If we've been handed something else, try to load the user.
442 elsif ( defined $args{'Owner'} ) {
443 $Owner = RT::User->new( $self->CurrentUser );
444 $Owner->Load( $args{'Owner'} );
448 #If we have a proposed owner and they don't have the right
449 #to own a ticket, scream about it and make them not the owner
450 if ( ( defined($Owner) )
452 and ( $Owner->Id != $RT::Nobody->Id )
453 and ( !$Owner->HasRight( Object => $QueueObj,
454 Right => 'OwnTicket' ) )
457 $RT::Logger->warning( "User "
461 . "as a ticket owner but has no rights to own "
462 . "tickets in ".$QueueObj->Name );
464 push @non_fatal_errors, $self->loc("Invalid owner. Defaulting to 'nobody'.");
469 #If we haven't been handed a valid owner, make it nobody.
470 unless ( defined($Owner) && $Owner->Id ) {
471 $Owner = new RT::User( $self->CurrentUser );
472 $Owner->Load( $RT::Nobody->Id );
477 # We attempt to load or create each of the people who might have a role for this ticket
478 # _outside_ the transaction, so we don't get into ticket creation races
479 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
480 next unless (defined $args{$type});
481 foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
482 my $user = RT::User->new($RT::SystemUser);
483 $user->LoadOrCreateByEmail($watcher) if ($watcher && $watcher !~ /^\d+$/);
488 $RT::Handle->BeginTransaction();
490 my %params =( Queue => $QueueObj->Id,
492 Subject => $args{'Subject'},
493 InitialPriority => $args{'InitialPriority'},
494 FinalPriority => $args{'FinalPriority'},
495 Priority => $priority,
496 Status => $args{'Status'},
497 TimeWorked => $args{'TimeWorked'},
498 TimeEstimated => $args{'TimeEstimated'},
499 TimeLeft => $args{'TimeLeft'},
500 Type => $args{'Type'},
501 Starts => $Starts->ISO,
502 Started => $Started->ISO,
503 Resolved => $Resolved->ISO,
506 # Parameters passed in during an import that we probably don't want to touch, otherwise
507 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
508 $params{$attr} = $args{$attr} if ($args{$attr});
511 # Delete null integer parameters
512 foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
513 delete $params{$attr} unless (exists $params{$attr} && $params{$attr});
517 my $id = $self->SUPER::Create( %params);
519 $RT::Logger->crit( "Couldn't create a ticket");
520 $RT::Handle->Rollback();
521 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
524 #Set the ticket's effective ID now that we've created it.
525 my ( $val, $msg ) = $self->__Set( Field => 'EffectiveId', Value => ($args{'EffectiveId'} || $id ) );
528 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
529 $RT::Handle->Rollback();
530 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
533 my $create_groups_ret = $self->_CreateTicketGroups();
534 unless ($create_groups_ret) {
535 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
537 . ". aborting Ticket creation." );
538 $RT::Handle->Rollback();
540 $self->loc( "Ticket could not be created due to an internal error") );
543 # Set the owner in the Groups table
544 # We denormalize it into the Ticket table too because doing otherwise would
545 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
547 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId , InsideTransaction => 1);
549 # {{{ Deal with setting up watchers
552 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
553 next unless (defined $args{$type});
554 foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
556 # If there is an empty entry in the list, let's get out of here.
557 next unless $watcher;
559 # we reason that all-digits number must be a principal id, not email
560 # this is the only way to can add
562 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
566 if ( $type eq 'AdminCc' ) {
568 # Note that we're using AddWatcher, rather than _AddWatcher, as we
569 # actually _want_ that ACL check. Otherwise, random ticket creators
570 # could make themselves adminccs and maybe get ticket rights. that would
572 ( $wval, $wmsg ) = $self->AddWatcher( Type => $type,
577 ( $wval, $wmsg ) = $self->_AddWatcher( Type => $type,
582 push @non_fatal_errors, $wmsg unless ($wval);
587 # {{{ Deal with setting up links
590 foreach my $type ( keys %LINKTYPEMAP ) {
591 next unless (defined $args{$type});
593 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
595 my ( $wval, $wmsg ) = $self->AddLink(
596 Type => $LINKTYPEMAP{$type}->{'Type'},
597 $LINKTYPEMAP{$type}->{'Mode'} => $link,
601 push @non_fatal_errors, $wmsg unless ($wval);
607 # {{{ Add all the custom fields
609 foreach my $arg ( keys %args ) {
610 next unless ( $arg =~ /^CustomField-(\d+)$/i );
613 my $value ( ref( $args{$arg} ) ? @{ $args{$arg} } : ( $args{$arg} ) ) {
614 next unless (length($value));
615 $self->_AddCustomFieldValue( Field => $cfid,
617 RecordTransaction => 0
623 if ( $args{'_RecordTransaction'} ) {
624 # {{{ Add a transaction for the create
625 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
628 MIMEObj => $args{'MIMEObj'}
632 if ( $self->Id && $Trans ) {
633 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
634 $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
636 $RT::Logger->info("Ticket ".$self->Id. " created in queue '".$QueueObj->Name."' by ".$self->CurrentUser->Name);
639 $RT::Handle->Rollback();
641 # TODO where does this get errstr from?
642 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
643 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
646 $RT::Handle->Commit();
647 return ( $self->Id, $TransObj->Id, $ErrStr );
652 # Not going to record a transaction
653 $RT::Handle->Commit();
654 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
655 $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
656 return ( $self->Id, $0, $ErrStr );
664 # {{{ sub CreateFromEmailMessage
667 =head2 CreateFromEmailMessage { Message, Queue, ExtractActorFromHeaders }
669 This code replaces what was once a large part of the email gateway.
670 It takes an email message as a parameter, parses out the sender, subject
671 and a MIME object. It then creates a ticket based on those attributes
675 sub CreateFromEmailMessage {
677 my %args = ( Message => undef,
679 ExtractActorFromSender => undef,
699 CreateTickets uses the template as a template for an ordered set of tickets
700 to create. The basic format is as follows:
703 ===Create-Ticket: identifier
711 =head2 Acceptable fields
713 A complete list of acceptable fields for this beastie:
716 * Queue => Name or id# of a queue
717 Subject => A text string
718 Status => A valid status. defaults to 'new'
720 Due => Dates can be specified in seconds since the epoch
721 to be handled literally or in a semi-free textual
722 format which RT will attempt to parse.
726 Owner => Username or id of an RT user who can and should own
728 + Requestor => Email address
729 + Cc => Email address
730 + AdminCc => Email address
743 Content => content. Can extend to multiple lines. Everything
744 within a template after a Content: header is treated
745 as content until we hit a line containing only
747 ContentType => the content-type of the Content field
748 CustomField-<id#> => custom field value
750 Fields marked with an * are required.
752 Fields marked with a + man have multiple values, simply
753 by repeating the fieldname on a new line with an additional value.
756 When parsed, field names are converted to lowercase and have -s stripped.
757 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
758 be treated as the same thing.
776 my %args = $self->_Parse822HeadersForAttributes($content);
778 # Now we have a %args to work with.
779 # Make sure we have at least the minimum set of
780 # reasonable data and do our thang
781 my $ticket = RT::Ticket->new($RT::SystemUser);
784 Queue => $args{'queue'},
785 Subject => $args{'subject'},
786 Status => $args{'status'},
788 Starts => $args{'starts'},
789 Started => $args{'started'},
790 Resolved => $args{'resolved'},
791 Owner => $args{'owner'},
792 Requestor => $args{'requestor'},
794 AdminCc => $args{'admincc'},
795 TimeWorked => $args{'timeworked'},
796 TimeEstimated => $args{'timeestimated'},
797 TimeLeft => $args{'timeleft'},
798 InitialPriority => $args{'initialpriority'},
799 FinalPriority => $args{'finalpriority'},
800 Type => $args{'type'},
801 DependsOn => $args{'dependson'},
802 DependedOnBy => $args{'dependedonby'},
803 RefersTo => $args{'refersto'},
804 ReferredToBy => $args{'referredtoby'},
805 Members => $args{'members'},
806 MemberOf => $args{'memberof'},
807 MIMEObj => $args{'mimeobj'}
810 # Add custom field entries to %ticketargs.
811 # TODO: allow named custom fields
813 /^customfield-(\d+)$/
814 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
817 my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs);
819 $RT::Logger->error( "Couldn't create a related ticket for "
820 . $self->TicketObj->Id . " "
831 =head2 UpdateFrom822 $MESSAGE
833 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
834 Returns an um. ask me again when the code exists
839 my $simple_update = <<EOF;
841 AddRequestor: jesse\@example.com
844 my $ticket = RT::Ticket->new($RT::SystemUser);
845 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
846 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
847 $ticket->UpdateFrom822($simple_update);
848 is($ticket->Subject, 'target', "changed the subject");
849 my $jesse = RT::User->new($RT::SystemUser);
850 $jesse->LoadByEmail('jesse@example.com');
851 ok ($jesse->Id, "There's a user for jesse");
852 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
862 my %args = $self->_Parse822HeadersForAttributes($content);
866 Queue => $args{'queue'},
867 Subject => $args{'subject'},
868 Status => $args{'status'},
870 Starts => $args{'starts'},
871 Started => $args{'started'},
872 Resolved => $args{'resolved'},
873 Owner => $args{'owner'},
874 Requestor => $args{'requestor'},
876 AdminCc => $args{'admincc'},
877 TimeWorked => $args{'timeworked'},
878 TimeEstimated => $args{'timeestimated'},
879 TimeLeft => $args{'timeleft'},
880 InitialPriority => $args{'initialpriority'},
881 Priority => $args{'priority'},
882 FinalPriority => $args{'finalpriority'},
883 Type => $args{'type'},
884 DependsOn => $args{'dependson'},
885 DependedOnBy => $args{'dependedonby'},
886 RefersTo => $args{'refersto'},
887 ReferredToBy => $args{'referredtoby'},
888 Members => $args{'members'},
889 MemberOf => $args{'memberof'},
890 MIMEObj => $args{'mimeobj'}
893 foreach my $type qw(Requestor Cc Admincc) {
895 foreach my $action ( 'Add', 'Del', '' ) {
897 my $lctag = lc($action) . lc($type);
898 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
900 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
901 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
906 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
911 # Add custom field entries to %ticketargs.
912 # TODO: allow named custom fields
914 /^customfield-(\d+)$/
915 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
918 # for each ticket we've been told to update, iterate through the set of
919 # rfc822 headers and perform that update to the ticket.
922 # {{{ Set basic fields
936 # Resolve the queue from a name to a numeric id.
937 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
938 my $tempqueue = RT::Queue->new($RT::SystemUser);
939 $tempqueue->Load( $ticketargs{'Queue'} );
940 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
943 # die "updaterecordobject is a webui thingy";
946 foreach my $attribute (@attribs) {
947 my $value = $ticketargs{$attribute};
949 if ( $value ne $self->$attribute() ) {
951 my $method = "Set$attribute";
952 my ( $code, $msg ) = $self->$method($value);
954 push @results, $self->loc($attribute) . ': ' . $msg;
959 # We special case owner changing, so we can use ForceOwnerChange
960 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
961 my $ChownType = "Give";
962 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
964 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
965 push ( @results, $msg );
969 # Deal with setting watchers
972 # Acceptable arguments:
979 foreach my $type qw(Requestor Cc AdminCc) {
981 # If we've been given a number of delresses to del, do it.
982 foreach my $address (@{$ticketargs{'Del'.$type}}) {
983 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
984 push (@results, $msg) ;
987 # If we've been given a number of addresses to add, do it.
988 foreach my $address (@{$ticketargs{'Add'.$type}}) {
989 $RT::Logger->debug("Adding $address as a $type");
990 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
991 push (@results, $msg) ;
1002 # {{{ _Parse822HeadersForAttributes Content
1004 =head2 _Parse822HeadersForAttributes Content
1006 Takes an RFC822 style message and parses its attributes into a hash.
1010 sub _Parse822HeadersForAttributes {
1012 my $content = shift;
1015 my @lines = ( split ( /\n/, $content ) );
1016 while ( defined( my $line = shift @lines ) ) {
1017 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1022 if ( defined( $args{$tag} ) )
1023 { #if we're about to get a second value, make it an array
1024 $args{$tag} = [ $args{$tag} ];
1026 if ( ref( $args{$tag} ) )
1027 { #If it's an array, we want to push the value
1028 push @{ $args{$tag} }, $value;
1030 else { #if there's nothing there, just set the value
1031 $args{$tag} = $value;
1033 } elsif ($line =~ /^$/) {
1035 #TODO: this won't work, since "" isn't of the form "foo:value"
1037 while ( defined( my $l = shift @lines ) ) {
1038 push @{ $args{'content'} }, $l;
1044 foreach my $date qw(due starts started resolved) {
1045 my $dateobj = RT::Date->new($RT::SystemUser);
1046 if ( $args{$date} =~ /^\d+$/ ) {
1047 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1050 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1052 $args{$date} = $dateobj->ISO;
1054 $args{'mimeobj'} = MIME::Entity->new();
1055 $args{'mimeobj'}->build(
1056 Type => ( $args{'contenttype'} || 'text/plain' ),
1057 Data => ($args{'content'} || '')
1067 =head2 Import PARAMHASH
1070 Doesn\'t create a transaction.
1071 Doesn\'t supply queue defaults, etc.
1079 my ( $ErrStr, $QueueObj, $Owner );
1083 EffectiveId => undef,
1087 Owner => $RT::Nobody->Id,
1088 Subject => '[no subject]',
1089 InitialPriority => undef,
1090 FinalPriority => undef,
1101 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1102 $QueueObj = RT::Queue->new($RT::SystemUser);
1103 $QueueObj->Load( $args{'Queue'} );
1105 #TODO error check this and return 0 if it\'s not loading properly +++
1107 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1108 $QueueObj = RT::Queue->new($RT::SystemUser);
1109 $QueueObj->Load( $args{'Queue'}->Id );
1113 "$self " . $args{'Queue'} . " not a recognised queue object." );
1116 #Can't create a ticket without a queue.
1117 unless ( defined($QueueObj) and $QueueObj->Id ) {
1118 $RT::Logger->debug("$self No queue given for ticket creation.");
1119 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1122 #Now that we have a queue, Check the ACLS
1124 $self->CurrentUser->HasRight(
1125 Right => 'CreateTicket',
1131 $self->loc("No permission to create tickets in the queue '[_1]'"
1132 , $QueueObj->Name));
1135 # {{{ Deal with setting the owner
1137 # Attempt to take user object, user name or user id.
1138 # Assign to nobody if lookup fails.
1139 if ( defined( $args{'Owner'} ) ) {
1140 if ( ref( $args{'Owner'} ) ) {
1141 $Owner = $args{'Owner'};
1144 $Owner = new RT::User( $self->CurrentUser );
1145 $Owner->Load( $args{'Owner'} );
1146 if ( !defined( $Owner->id ) ) {
1147 $Owner->Load( $RT::Nobody->id );
1152 #If we have a proposed owner and they don't have the right
1153 #to own a ticket, scream about it and make them not the owner
1156 and ( $Owner->Id != $RT::Nobody->Id )
1159 Object => $QueueObj,
1160 Right => 'OwnTicket'
1166 $RT::Logger->warning( "$self user "
1167 . $Owner->Name . "("
1170 . "as a ticket owner but has no rights to own "
1172 . $QueueObj->Name . "'\n" );
1177 #If we haven't been handed a valid owner, make it nobody.
1178 unless ( defined($Owner) ) {
1179 $Owner = new RT::User( $self->CurrentUser );
1180 $Owner->Load( $RT::Nobody->UserObj->Id );
1185 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1186 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1189 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1190 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1191 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1192 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1194 # If we're coming in with an id, set that now.
1195 my $EffectiveId = undef;
1196 if ( $args{'id'} ) {
1197 $EffectiveId = $args{'id'};
1201 my $id = $self->SUPER::Create(
1203 EffectiveId => $EffectiveId,
1204 Queue => $QueueObj->Id,
1205 Owner => $Owner->Id,
1206 Subject => $args{'Subject'}, # loc
1207 InitialPriority => $args{'InitialPriority'}, # loc
1208 FinalPriority => $args{'FinalPriority'}, # loc
1209 Priority => $args{'InitialPriority'}, # loc
1210 Status => $args{'Status'}, # loc
1211 TimeWorked => $args{'TimeWorked'}, # loc
1212 Type => $args{'Type'}, # loc
1213 Created => $args{'Created'}, # loc
1214 Told => $args{'Told'}, # loc
1215 LastUpdated => $args{'Updated'}, # loc
1216 Resolved => $args{'Resolved'}, # loc
1217 Due => $args{'Due'}, # loc
1220 # If the ticket didn't have an id
1221 # Set the ticket's effective ID now that we've created it.
1222 if ( $args{'id'} ) {
1223 $self->Load( $args{'id'} );
1227 $self->__Set( Field => 'EffectiveId', Value => $id );
1231 $self . "->Import couldn't set EffectiveId: $msg\n" );
1236 foreach $watcher ( @{ $args{'Cc'} } ) {
1237 $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 );
1239 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1240 $self->_AddWatcher( Type => 'AdminCc', Person => $watcher,
1243 foreach $watcher ( @{ $args{'Requestor'} } ) {
1244 $self->_AddWatcher( Type => 'Requestor', Person => $watcher,
1248 return ( $self->Id, $ErrStr );
1254 # {{{ Routines dealing with watchers.
1256 # {{{ _CreateTicketGroups
1258 =head2 _CreateTicketGroups
1260 Create the ticket groups and relationships for this ticket.
1261 This routine expects to be called from Ticket->Create _inside of a transaction_
1263 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1265 It will return true on success and undef on failure.
1269 my $ticket = RT::Ticket->new($RT::SystemUser);
1270 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1271 Owner => $RT::SystemUser->Id,
1273 Requestor => ['jesse@example.com'],
1276 ok ($id, "Ticket $id was created");
1277 ok(my $group = RT::Group->new($RT::SystemUser));
1278 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1279 ok ($group->Id, "Found the requestors object for this ticket");
1281 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1282 $jesse->LoadByEmail('jesse@example.com');
1283 ok($jesse->Id, "Found the jesse rt user");
1286 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1287 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1288 ok ($add_id, "Add succeeded: ($add_msg)");
1289 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1290 $bob->LoadByEmail('bob@fsck.com');
1291 ok($bob->Id, "Found the bob rt user");
1292 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1293 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1294 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1297 $group = RT::Group->new($RT::SystemUser);
1298 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1299 ok ($group->Id, "Found the cc object for this ticket");
1300 $group = RT::Group->new($RT::SystemUser);
1301 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1302 ok ($group->Id, "Found the AdminCc object for this ticket");
1303 $group = RT::Group->new($RT::SystemUser);
1304 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1305 ok ($group->Id, "Found the Owner object for this ticket");
1306 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1313 sub _CreateTicketGroups {
1316 my @types = qw(Requestor Owner Cc AdminCc);
1318 foreach my $type (@types) {
1319 my $type_obj = RT::Group->new($self->CurrentUser);
1320 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1321 Instance => $self->Id,
1324 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1325 $self->Id.": ".$msg);
1335 # {{{ sub OwnerGroup
1339 A constructor which returns an RT::Group object containing the owner of this ticket.
1345 my $owner_obj = RT::Group->new($self->CurrentUser);
1346 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1347 return ($owner_obj);
1353 # {{{ sub AddWatcher
1357 AddWatcher takes a parameter hash. The keys are as follows:
1359 Type One of Requestor, Cc, AdminCc
1361 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1363 Email The email address of the new watcher. If a user with this
1364 email address can't be found, a new nonprivileged user will be created.
1366 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.
1374 PrincipalId => undef,
1380 #If the watcher we're trying to add is for the current user
1381 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1382 # If it's an AdminCc and they don't have
1383 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1384 if ( $args{'Type'} eq 'AdminCc' ) {
1385 unless ( $self->CurrentUserHasRight('ModifyTicket')
1386 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1387 return ( 0, $self->loc('Permission Denied'))
1391 # If it's a Requestor or Cc and they don't have
1392 # 'Watch' or 'ModifyTicket', bail
1393 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1395 unless ( $self->CurrentUserHasRight('ModifyTicket')
1396 or $self->CurrentUserHasRight('Watch') ) {
1397 return ( 0, $self->loc('Permission Denied'))
1401 $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type");
1402 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1406 # If the watcher isn't the current user
1407 # and the current user doesn't have 'ModifyTicket'
1410 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1411 return ( 0, $self->loc("Permission Denied") );
1417 return ( $self->_AddWatcher(%args) );
1420 #This contains the meat of AddWatcher. but can be called from a routine like
1421 # Create, which doesn't need the additional acl check
1427 PrincipalId => undef,
1433 my $principal = RT::Principal->new($self->CurrentUser);
1434 if ($args{'Email'}) {
1435 my $user = RT::User->new($RT::SystemUser);
1436 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1438 $args{'PrincipalId'} = $pid;
1441 if ($args{'PrincipalId'}) {
1442 $principal->Load($args{'PrincipalId'});
1446 # If we can't find this watcher, we need to bail.
1447 unless ($principal->Id) {
1448 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1449 return(0, $self->loc("Could not find or create that user"));
1453 my $group = RT::Group->new($self->CurrentUser);
1454 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1455 unless ($group->id) {
1456 return(0,$self->loc("Group not found"));
1459 if ( $group->HasMember( $principal)) {
1461 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1465 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1466 InsideTransaction => 1 );
1468 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1470 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1473 unless ( $args{'Silent'} ) {
1474 $self->_NewTransaction(
1475 Type => 'AddWatcher',
1476 NewValue => $principal->Id,
1477 Field => $args{'Type'}
1481 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1487 # {{{ sub DeleteWatcher
1489 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1492 Deletes a Ticket watcher. Takes two arguments:
1494 Type (one of Requestor,Cc,AdminCc)
1498 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1500 Email (the email address of an existing wathcer)
1509 my %args = ( Type => undef,
1510 PrincipalId => undef,
1514 unless ($args{'PrincipalId'} || $args{'Email'} ) {
1515 return(0, $self->loc("No principal specified"));
1517 my $principal = RT::Principal->new($self->CurrentUser);
1518 if ($args{'PrincipalId'} ) {
1520 $principal->Load($args{'PrincipalId'});
1522 my $user = RT::User->new($self->CurrentUser);
1523 $user->LoadByEmail($args{'Email'});
1524 $principal->Load($user->Id);
1526 # If we can't find this watcher, we need to bail.
1527 unless ($principal->Id) {
1528 return(0, $self->loc("Could not find that principal"));
1531 my $group = RT::Group->new($self->CurrentUser);
1532 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1533 unless ($group->id) {
1534 return(0,$self->loc("Group not found"));
1538 #If the watcher we're trying to add is for the current user
1539 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1540 # If it's an AdminCc and they don't have
1541 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1542 if ( $args{'Type'} eq 'AdminCc' ) {
1543 unless ( $self->CurrentUserHasRight('ModifyTicket')
1544 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1545 return ( 0, $self->loc('Permission Denied'))
1549 # If it's a Requestor or Cc and they don't have
1550 # 'Watch' or 'ModifyTicket', bail
1551 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1552 unless ( $self->CurrentUserHasRight('ModifyTicket')
1553 or $self->CurrentUserHasRight('Watch') ) {
1554 return ( 0, $self->loc('Permission Denied'))
1558 $RT::Logger->warn( "$self -> DeleteWatcher got passed a bogus type");
1559 return ( 0, $self->loc('Error in parameters to Ticket->DelWatcher') );
1563 # If the watcher isn't the current user
1564 # and the current user doesn't have 'ModifyTicket' bail
1566 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1567 return ( 0, $self->loc("Permission Denied") );
1574 # see if this user is already a watcher.
1576 unless ( $group->HasMember($principal)) {
1578 $self->loc('That principal is not a [_1] for this ticket', $args{'Type'}) );
1581 my ($m_id, $m_msg) = $group->_DeleteMember($principal->Id);
1583 $RT::Logger->error("Failed to delete ".$principal->Id.
1584 " as a member of group ".$group->Id."\n".$m_msg);
1586 return ( 0, $self->loc('Could not remove that principal as a [_1] for this ticket', $args{'Type'}) );
1589 unless ( $args{'Silent'} ) {
1590 $self->_NewTransaction(
1591 Type => 'DelWatcher',
1592 OldValue => $principal->Id,
1593 Field => $args{'Type'}
1597 return ( 1, $self->loc("[_1] is no longer a [_2] for this ticket.", $principal->Object->Name, $args{'Type'} ));
1606 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1608 =head2 RequestorAddresses
1610 B<Returns> String: All Ticket Requestor email addresses as a string.
1614 sub RequestorAddresses {
1617 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1621 return ( $self->Requestors->MemberEmailAddressesAsString );
1625 =head2 AdminCcAddresses
1627 returns String: All Ticket AdminCc email addresses as a string
1631 sub AdminCcAddresses {
1634 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1638 return ( $self->AdminCc->MemberEmailAddressesAsString )
1644 returns String: All Ticket Ccs as a string of email addresses
1651 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1655 return ( $self->Cc->MemberEmailAddressesAsString);
1661 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1663 # {{{ sub Requestors
1668 Returns this ticket's Requestors as an RT::Group object
1675 my $group = RT::Group->new($self->CurrentUser);
1676 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1677 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1690 Returns an RT::Group object which contains this ticket's Ccs.
1691 If the user doesn't have "ShowTicket" permission, returns an empty group
1698 my $group = RT::Group->new($self->CurrentUser);
1699 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1700 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1713 Returns an RT::Group object which contains this ticket's AdminCcs.
1714 If the user doesn't have "ShowTicket" permission, returns an empty group
1721 my $group = RT::Group->new($self->CurrentUser);
1722 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1723 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1733 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1736 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1738 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1740 Takes a param hash with the attributes Type and either PrincipalId or Email
1742 Type is one of Requestor, Cc, AdminCc and Owner
1744 PrincipalId is an RT::Principal id, and Email is an email address.
1746 Returns true if the specified principal (or the one corresponding to the
1747 specified address) is a member of the group Type for this ticket.
1754 my %args = ( Type => 'Requestor',
1755 PrincipalId => undef,
1760 # Load the relevant group.
1761 my $group = RT::Group->new($self->CurrentUser);
1762 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1764 # Find the relevant principal.
1765 my $principal = RT::Principal->new($self->CurrentUser);
1766 if (!$args{PrincipalId} && $args{Email}) {
1767 # Look up the specified user.
1768 my $user = RT::User->new($self->CurrentUser);
1769 $user->LoadByEmail($args{Email});
1771 $args{PrincipalId} = $user->PrincipalId;
1774 # A non-existent user can't be a group member.
1778 $principal->Load($args{'PrincipalId'});
1780 # Ask if it has the member in question
1781 return ($group->HasMember($principal));
1786 # {{{ sub IsRequestor
1788 =head2 IsRequestor PRINCIPAL_ID
1790 Takes an RT::Principal id
1791 Returns true if the principal is a requestor of the current ticket.
1800 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1808 =head2 IsCc PRINCIPAL_ID
1810 Takes an RT::Principal id.
1811 Returns true if the principal is a requestor of the current ticket.
1820 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1828 =head2 IsAdminCc PRINCIPAL_ID
1830 Takes an RT::Principal id.
1831 Returns true if the principal is a requestor of the current ticket.
1839 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1849 Takes an RT::User object. Returns true if that user is this ticket's owner.
1850 returns undef otherwise
1858 # no ACL check since this is used in acl decisions
1859 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1863 #Tickets won't yet have owners when they're being created.
1864 unless ( $self->OwnerObj->id ) {
1868 if ( $person->id == $self->OwnerObj->id ) {
1882 # {{{ Routines dealing with queues
1884 # {{{ sub ValidateQueue
1891 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1895 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1896 my $id = $QueueObj->Load($Value);
1912 my $NewQueue = shift;
1914 #Redundant. ACL gets checked in _Set;
1915 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1916 return ( 0, $self->loc("Permission Denied") );
1919 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1920 $NewQueueObj->Load($NewQueue);
1922 unless ( $NewQueueObj->Id() ) {
1923 return ( 0, $self->loc("That queue does not exist") );
1926 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1927 return ( 0, $self->loc('That is the same value') );
1930 $self->CurrentUser->HasRight(
1931 Right => 'CreateTicket',
1932 Object => $NewQueueObj
1936 return ( 0, $self->loc("You may not create requests in that queue.") );
1940 $self->OwnerObj->HasRight(
1941 Right => 'OwnTicket',
1942 Object => $NewQueueObj
1949 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
1959 Takes nothing. returns this ticket's queue object
1966 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1968 #We call __Value so that we can avoid the ACL decision and some deep recursion
1969 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1970 return ($queue_obj);
1977 # {{{ Date printing routines
1983 Returns an RT::Date object containing this ticket's due date
1990 my $time = new RT::Date( $self->CurrentUser );
1992 # -1 is RT::Date slang for never
1994 $time->Set( Format => 'sql', Value => $self->Due );
1997 $time->Set( Format => 'unix', Value => -1 );
2005 # {{{ sub DueAsString
2009 Returns this ticket's due date as a human readable string
2015 return $self->DueObj->AsString();
2020 # {{{ sub ResolvedObj
2024 Returns an RT::Date object of this ticket's 'resolved' time.
2031 my $time = new RT::Date( $self->CurrentUser );
2032 $time->Set( Format => 'sql', Value => $self->Resolved );
2038 # {{{ sub SetStarted
2042 Takes a date in ISO format or undef
2043 Returns a transaction id and a message
2044 The client calls "Start" to note that the project was started on the date in $date.
2045 A null date means "now"
2051 my $time = shift || 0;
2053 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2054 return ( 0, self->loc("Permission Denied") );
2057 #We create a date object to catch date weirdness
2058 my $time_obj = new RT::Date( $self->CurrentUser() );
2060 $time_obj->Set( Format => 'ISO', Value => $time );
2063 $time_obj->SetToNow();
2066 #Now that we're starting, open this ticket
2067 #TODO do we really want to force this as policy? it should be a scrip
2069 #We need $TicketAsSystem, in case the current user doesn't have
2072 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2073 $TicketAsSystem->Load( $self->Id );
2074 if ( $TicketAsSystem->Status eq 'new' ) {
2075 $TicketAsSystem->Open();
2078 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2084 # {{{ sub StartedObj
2088 Returns an RT::Date object which contains this ticket's
2096 my $time = new RT::Date( $self->CurrentUser );
2097 $time->Set( Format => 'sql', Value => $self->Started );
2107 Returns an RT::Date object which contains this ticket's
2115 my $time = new RT::Date( $self->CurrentUser );
2116 $time->Set( Format => 'sql', Value => $self->Starts );
2126 Returns an RT::Date object which contains this ticket's
2134 my $time = new RT::Date( $self->CurrentUser );
2135 $time->Set( Format => 'sql', Value => $self->Told );
2141 # {{{ sub ToldAsString
2145 A convenience method that returns ToldObj->AsString
2147 TODO: This should be deprecated
2153 if ( $self->Told ) {
2154 return $self->ToldObj->AsString();
2163 # {{{ sub TimeWorkedAsString
2165 =head2 TimeWorkedAsString
2167 Returns the amount of time worked on this ticket as a Text String
2171 sub TimeWorkedAsString {
2173 return "0" unless $self->TimeWorked;
2175 #This is not really a date object, but if we diff a number of seconds
2176 #vs the epoch, we'll get a nice description of time worked.
2178 my $worked = new RT::Date( $self->CurrentUser );
2180 #return the #of minutes worked turned into seconds and written as
2181 # a simple text string
2183 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2190 # {{{ Routines dealing with correspondence/comments
2196 Comment on this ticket.
2197 Takes a hashref with the following attributes:
2198 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2201 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content.
2205 ## Please see file perltidy.ERR
2209 my %args = ( CcMessageTo => undef,
2210 BccMessageTo => undef,
2216 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2217 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2218 return ( 0, $self->loc("Permission Denied") );
2221 unless ( $args{'MIMEObj'} ) {
2222 if ( $args{'Content'} ) {
2224 $args{'MIMEObj'} = MIME::Entity->build(
2225 Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
2230 return ( 0, $self->loc("No correspondence attached") );
2234 RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
2236 # If we've been passed in CcMessageTo and BccMessageTo fields,
2237 # add them to the mime object for passing on to the transaction handler
2238 # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
2239 # RT-Send-Bcc: headers
2241 $args{'MIMEObj'}->head->add( 'RT-Send-Cc',
2242 RT::User::CanonicalizeEmailAddress(undef, $args{'CcMessageTo'}) )
2243 if defined $args{'CcMessageTo'};
2244 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2245 RT::User::CanonicalizeEmailAddress(undef, $args{'BccMessageTo'}) )
2246 if defined $args{'BccMessageTo'};
2248 #Record the correspondence (write the transaction)
2249 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2251 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2252 TimeTaken => $args{'TimeTaken'},
2253 MIMEObj => $args{'MIMEObj'}
2256 return ( $Trans, $self->loc("The comment has been recorded") );
2261 # {{{ sub Correspond
2265 Correspond on this ticket.
2266 Takes a hashref with the following attributes:
2269 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content
2271 if there's no MIMEObj, Content is used to build a MIME::Entity object
2278 my %args = ( CcMessageTo => undef,
2279 BccMessageTo => undef,
2285 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2286 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2287 return ( 0, $self->loc("Permission Denied") );
2290 unless ( $args{'MIMEObj'} ) {
2291 if ( $args{'Content'} ) {
2293 $args{'MIMEObj'} = MIME::Entity->build(
2294 Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
2300 return ( 0, $self->loc("No correspondence attached") );
2304 RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
2306 # If we've been passed in CcMessageTo and BccMessageTo fields,
2307 # add them to the mime object for passing on to the transaction handler
2308 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2311 $args{'MIMEObj'}->head->add( 'RT-Send-Cc',
2312 RT::User::CanonicalizeEmailAddress(undef, $args{'CcMessageTo'}) )
2313 if defined $args{'CcMessageTo'};
2314 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2315 RT::User::CanonicalizeEmailAddress(undef, $args{'BccMessageTo'}) )
2316 if defined $args{'BccMessageTo'};
2318 #Record the correspondence (write the transaction)
2319 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2320 Type => 'Correspond',
2321 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2322 TimeTaken => $args{'TimeTaken'},
2323 MIMEObj => $args{'MIMEObj'} );
2326 $RT::Logger->err( "$self couldn't init a transaction $msg");
2327 return ( $Trans, $self->loc("correspondence (probably) not sent"), $args{'MIMEObj'} );
2330 #Set the last told date to now if this isn't mail from the requestor.
2331 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2333 unless ( $TransObj->IsInbound ) {
2337 return ( $Trans, $self->loc("correspondence sent") );
2344 # {{{ Routines dealing with Links and Relations between tickets
2346 # {{{ Link Collections
2352 This returns an RT::Links object which references all the tickets
2353 which are 'MembersOf' this ticket
2359 return ( $self->_Links( 'Target', 'MemberOf' ) );
2368 This returns an RT::Links object which references all the tickets that this
2369 ticket is a 'MemberOf'
2375 return ( $self->_Links( 'Base', 'MemberOf' ) );
2384 This returns an RT::Links object which shows all references for which this ticket is a base
2390 return ( $self->_Links( 'Base', 'RefersTo' ) );
2399 This returns an RT::Links object which shows all references for which this ticket is a target
2405 return ( $self->_Links( 'Target', 'RefersTo' ) );
2414 This returns an RT::Links object which references all the tickets that depend on this one
2420 return ( $self->_Links( 'Target', 'DependsOn' ) );
2427 =head2 HasUnresolvedDependencies
2429 Takes a paramhash of Type (default to '__any'). Returns true if
2430 $self->UnresolvedDependencies returns an object with one or more members
2431 of that type. Returns false otherwise
2436 my $t1 = RT::Ticket->new($RT::SystemUser);
2437 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
2438 ok($id, "Created dep test 1 - $msg");
2440 my $t2 = RT::Ticket->new($RT::SystemUser);
2441 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
2442 ok($id2, "Created dep test 2 - $msg2");
2443 my $t3 = RT::Ticket->new($RT::SystemUser);
2444 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
2445 ok($id3, "Created dep test 3 - $msg3");
2447 ok ($t1->AddLink( Type => 'DependsOn', Target => $t2->id));
2448 ok ($t1->AddLink( Type => 'DependsOn', Target => $t3->id));
2450 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
2451 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
2452 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
2453 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
2454 my ($rid, $rmsg)= $t1->Resolve();
2457 ($rid, $rmsg)= $t1->Resolve();
2460 ($rid, $rmsg)= $t1->Resolve();
2468 sub HasUnresolvedDependencies {
2475 my $deps = $self->UnresolvedDependencies;
2478 $deps->Limit( FIELD => 'Type',
2480 VALUE => $args{Type});
2486 if ($deps->Count > 0) {
2495 # {{{ UnresolvedDependencies
2497 =head2 UnresolvedDependencies
2499 Returns an RT::Tickets object of tickets which this ticket depends on
2500 and which have a status of new, open or stalled. (That list comes from
2501 RT::Queue->ActiveStatusArray
2506 sub UnresolvedDependencies {
2508 my $deps = RT::Tickets->new($self->CurrentUser);
2510 my @live_statuses = RT::Queue->ActiveStatusArray();
2511 foreach my $status (@live_statuses) {
2512 $deps->LimitStatus(VALUE => $status);
2514 $deps->LimitDependedOnBy($self->Id);
2522 # {{{ AllDependedOnBy
2524 =head2 AllDependedOnBy
2526 Returns an array of RT::Ticket objects which (directly or indirectly)
2527 depends on this ticket; takes an optional 'Type' argument in the param
2528 hash, which will limit returned tickets to that type, as well as cause
2529 tickets with that type to serve as 'leaf' nodes that stops the recursive
2534 sub AllDependedOnBy {
2536 my $dep = $self->DependedOnBy;
2544 while (my $link = $dep->Next()) {
2545 next unless ($link->BaseURI->IsLocal());
2546 next if $args{_found}{$link->BaseObj->Id};
2549 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
2550 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
2552 elsif ($link->BaseObj->Type eq $args{Type}) {
2553 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
2556 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
2561 return map { $args{_found}{$_} } sort keys %{$args{_found}};
2574 This returns an RT::Links object which references all the tickets that this ticket depends on
2580 return ( $self->_Links( 'Base', 'DependsOn' ) );
2593 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2596 my $type = shift || "";
2598 unless ( $self->{"$field$type"} ) {
2599 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2600 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2601 # Maybe this ticket is a merged ticket
2602 my $Tickets = new RT::Tickets( $self->CurrentUser );
2603 # at least to myself
2604 $self->{"$field$type"}->Limit( FIELD => $field,
2605 VALUE => $self->URI,
2606 ENTRYAGGREGATOR => 'OR' );
2607 $Tickets->Limit( FIELD => 'EffectiveId',
2608 VALUE => $self->EffectiveId );
2609 while (my $Ticket = $Tickets->Next) {
2610 $self->{"$field$type"}->Limit( FIELD => $field,
2611 VALUE => $Ticket->URI,
2612 ENTRYAGGREGATOR => 'OR' );
2614 $self->{"$field$type"}->Limit( FIELD => 'Type',
2619 return ( $self->{"$field$type"} );
2626 # {{{ sub DeleteLink
2630 Delete a link. takes a paramhash of Base, Target and Type.
2631 Either Base or Target must be null. The null value will
2632 be replaced with this ticket\'s id
2646 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2647 $RT::Logger->debug("No permission to delete links\n");
2648 return ( 0, $self->loc('Permission Denied'))
2652 #we want one of base and target. we don't care which
2653 #but we only want _one_
2658 if ( $args{'Base'} and $args{'Target'} ) {
2659 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
2660 return ( 0, $self->loc("Can't specifiy both base and target") );
2662 elsif ( $args{'Base'} ) {
2663 $args{'Target'} = $self->URI();
2664 $remote_link = $args{'Base'};
2665 $direction = 'Target';
2667 elsif ( $args{'Target'} ) {
2668 $args{'Base'} = $self->URI();
2669 $remote_link = $args{'Target'};
2673 $RT::Logger->debug("$self: Base or Target must be specified\n");
2674 return ( 0, $self->loc('Either base or target must be specified') );
2677 my $link = new RT::Link( $self->CurrentUser );
2678 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
2681 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
2685 my $linkid = $link->id;
2688 my $TransString = "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
2689 my $remote_uri = RT::URI->new( $RT::SystemUser );
2690 $remote_uri->FromURI( $remote_link );
2692 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2693 Type => 'DeleteLink',
2694 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2695 OldValue => $remote_uri->URI || $remote_link,
2699 return ( $Trans, $self->loc("Link deleted ([_1])", $TransString));
2702 #if it's not a link we can find
2704 $RT::Logger->debug("Couldn't find that link\n");
2705 return ( 0, $self->loc("Link not found") );
2715 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2722 my %args = ( Target => '',
2728 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2729 return ( 0, $self->loc("Permission Denied") );
2732 # Remote_link is the URI of the object that is not this ticket
2736 if ( $args{'Base'} and $args{'Target'} ) {
2738 "$self tried to delete a link. both base and target were specified\n" );
2739 return ( 0, $self->loc("Can't specifiy both base and target") );
2741 elsif ( $args{'Base'} ) {
2742 $args{'Target'} = $self->URI();
2743 $remote_link = $args{'Base'};
2744 $direction = 'Target';
2746 elsif ( $args{'Target'} ) {
2747 $args{'Base'} = $self->URI();
2748 $remote_link = $args{'Target'};
2752 return ( 0, $self->loc('Either base or target must be specified') );
2755 # If the base isn't a URI, make it a URI.
2756 # If the target isn't a URI, make it a URI.
2758 # {{{ Check if the link already exists - we don't want duplicates
2760 my $old_link = RT::Link->new( $self->CurrentUser );
2761 $old_link->LoadByParams( Base => $args{'Base'},
2762 Type => $args{'Type'},
2763 Target => $args{'Target'} );
2764 if ( $old_link->Id ) {
2765 $RT::Logger->debug("$self Somebody tried to duplicate a link");
2766 return ( $old_link->id, $self->loc("Link already exists"), 0 );
2771 # Storing the link in the DB.
2772 my $link = RT::Link->new( $self->CurrentUser );
2773 my ($linkid) = $link->Create( Target => $args{Target},
2774 Base => $args{Base},
2775 Type => $args{Type} );
2778 return ( 0, $self->loc("Link could not be created") );
2782 "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
2784 # Don't write the transaction if we're doing this on create
2785 if ( $args{'Silent'} ) {
2786 return ( 1, $self->loc( "Link created ([_1])", $TransString ) );
2789 my $remote_uri = RT::URI->new( $RT::SystemUser );
2790 $remote_uri->FromURI( $remote_link );
2792 #Write the transaction
2793 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2795 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2796 NewValue => $remote_uri->URI || $remote_link,
2798 return ( $Trans, $self->loc( "Link created ([_1])", $TransString ) );
2809 Returns this ticket's URI
2815 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
2816 return($uri->URIForObject($self));
2824 MergeInto take the id of the ticket to merge this ticket into.
2830 my $MergeInto = shift;
2832 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2833 return ( 0, $self->loc("Permission Denied") );
2836 # Load up the new ticket.
2837 my $NewTicket = RT::Ticket->new($RT::SystemUser);
2838 $NewTicket->Load($MergeInto);
2840 # make sure it exists.
2841 unless ( defined $NewTicket->Id ) {
2842 return ( 0, $self->loc("New ticket doesn't exist") );
2845 # Make sure the current user can modify the new ticket.
2846 unless ( $NewTicket->CurrentUserHasRight('ModifyTicket') ) {
2847 $RT::Logger->debug("failed...");
2848 return ( 0, $self->loc("Permission Denied") );
2852 "checking if the new ticket has the same id and effective id...");
2853 unless ( $NewTicket->id == $NewTicket->EffectiveId ) {
2854 $RT::Logger->err( "$self trying to merge into "
2856 . " which is itself merged.\n" );
2858 $self->loc("Can't merge into a merged ticket. You should never get this error") );
2861 # We use EffectiveId here even though it duplicates information from
2862 # the links table becasue of the massive performance hit we'd take
2863 # by trying to do a seperate database query for merge info everytime
2866 #update this ticket's effective id to the new ticket's id.
2867 my ( $id_val, $id_msg ) = $self->__Set(
2868 Field => 'EffectiveId',
2869 Value => $NewTicket->Id()
2874 "Couldn't set effective ID for " . $self->Id . ": $id_msg" );
2875 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2878 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2880 unless ($status_val) {
2881 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2885 # update all the links that point to that old ticket
2886 my $old_links_to = RT::Links->new($self->CurrentUser);
2887 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2889 while (my $link = $old_links_to->Next) {
2890 if ($link->Base eq $NewTicket->URI) {
2893 $link->SetTarget($NewTicket->URI);
2898 my $old_links_from = RT::Links->new($self->CurrentUser);
2899 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2901 while (my $link = $old_links_from->Next) {
2902 if ($link->Target eq $NewTicket->URI) {
2905 $link->SetBase($NewTicket->URI);
2911 #add all of this ticket's watchers to that ticket.
2912 my $requestors = $self->Requestors->MembersObj;
2913 while (my $watcher = $requestors->Next) {
2914 $NewTicket->_AddWatcher( Type => 'Requestor',
2916 PrincipalId => $watcher->MemberId);
2919 my $Ccs = $self->Cc->MembersObj;
2920 while (my $watcher = $Ccs->Next) {
2921 $NewTicket->_AddWatcher( Type => 'Cc',
2923 PrincipalId => $watcher->MemberId);
2926 my $AdminCcs = $self->AdminCc->MembersObj;
2927 while (my $watcher = $AdminCcs->Next) {
2928 $NewTicket->_AddWatcher( Type => 'AdminCc',
2930 PrincipalId => $watcher->MemberId);
2934 #find all of the tickets that were merged into this ticket.
2935 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2936 $old_mergees->Limit(
2937 FIELD => 'EffectiveId',
2942 # update their EffectiveId fields to the new ticket's id
2943 while ( my $ticket = $old_mergees->Next() ) {
2944 my ( $val, $msg ) = $ticket->__Set(
2945 Field => 'EffectiveId',
2946 Value => $NewTicket->Id()
2950 #make a new link: this ticket is merged into that other ticket.
2951 $self->AddLink( Type => 'MergedInto', Target => $NewTicket->Id());
2953 $NewTicket->_SetLastUpdated;
2955 return ( 1, $self->loc("Merge Successful") );
2962 # {{{ Routines dealing with ownership
2968 Takes nothing and returns an RT::User object of
2976 #If this gets ACLed, we lose on a rights check in User.pm and
2977 #get deep recursion. if we need ACLs here, we need
2978 #an equiv without ACLs
2980 my $owner = new RT::User( $self->CurrentUser );
2981 $owner->Load( $self->__Value('Owner') );
2983 #Return the owner object
2989 # {{{ sub OwnerAsString
2991 =head2 OwnerAsString
2993 Returns the owner's email address
2999 return ( $self->OwnerObj->EmailAddress );
3009 Takes two arguments:
3010 the Id or Name of the owner
3011 and (optionally) the type of the SetOwner Transaction. It defaults
3012 to 'Give'. 'Steal' is also a valid option.
3016 my $root = RT::User->new($RT::SystemUser);
3017 $root->Load('root');
3018 ok ($root->Id, "Loaded the root user");
3019 my $t = RT::Ticket->new($RT::SystemUser);
3021 $t->SetOwner('root');
3022 ok ($t->OwnerObj->Name eq 'root' , "Root owns the ticket");
3024 ok ($t->OwnerObj->id eq $RT::SystemUser->id , "SystemUser owns the ticket");
3025 my $txns = RT::Transactions->new($RT::SystemUser);
3026 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3027 $txns->Limit(FIELD => 'Ticket', VALUE => '1');
3028 my $steal = $txns->First;
3029 ok($steal->OldValue == $root->Id , "Stolen from root");
3030 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3038 my $NewOwner = shift;
3039 my $Type = shift || "Give";
3041 # must have ModifyTicket rights
3042 # or TakeTicket/StealTicket and $NewOwner is self
3043 # see if it's a take
3044 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
3045 unless ( $self->CurrentUserHasRight('ModifyTicket')
3046 || $self->CurrentUserHasRight('TakeTicket') ) {
3047 return ( 0, $self->loc("Permission Denied") );
3051 # see if it's a steal
3052 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
3053 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
3055 unless ( $self->CurrentUserHasRight('ModifyTicket')
3056 || $self->CurrentUserHasRight('StealTicket') ) {
3057 return ( 0, $self->loc("Permission Denied") );
3061 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3062 return ( 0, $self->loc("Permission Denied") );
3065 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3066 my $OldOwnerObj = $self->OwnerObj;
3068 $NewOwnerObj->Load($NewOwner);
3069 if ( !$NewOwnerObj->Id ) {
3070 return ( 0, $self->loc("That user does not exist") );
3073 #If thie ticket has an owner and it's not the current user
3075 if ( ( $Type ne 'Steal' )
3076 and ( $Type ne 'Force' )
3077 and #If we're not stealing
3078 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
3079 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
3080 ) { #and it's not us
3083 "You can only reassign tickets that you own or that are unowned" ) );
3086 #If we've specified a new owner and that user can't modify the ticket
3087 elsif ( ( $NewOwnerObj->Id )
3088 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
3091 return ( 0, $self->loc("That user may not own tickets in that queue") );
3094 #If the ticket has an owner and it's the new owner, we don't need
3096 elsif ( ( $self->OwnerObj )
3097 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
3098 return ( 0, $self->loc("That user already owns that ticket") );
3101 $RT::Handle->BeginTransaction();
3103 # Delete the owner in the owner group, then add a new one
3104 # TODO: is this safe? it's not how we really want the API to work
3105 # for most things, but it's fast.
3106 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3108 $RT::Handle->Rollback();
3109 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3112 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3113 PrincipalId => $NewOwnerObj->PrincipalId,
3114 InsideTransaction => 1 );
3116 $RT::Handle->Rollback();
3117 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3120 # We call set twice with slightly different arguments, so
3121 # as to not have an SQL transaction span two RT transactions
3123 my ( $val, $msg ) = $self->_Set(
3125 RecordTransaction => 0,
3126 Value => $NewOwnerObj->Id,
3128 TransactionType => $Type,
3129 CheckACL => 0, # don't check acl
3133 $RT::Handle->Rollback;
3134 return ( 0, $self->loc("Could not change owner. ") . $msg );
3137 $RT::Handle->Commit();
3139 my ( $trans, $msg, undef ) = $self->_NewTransaction(
3142 NewValue => $NewOwnerObj->Id,
3143 OldValue => $OldOwnerObj->Id,
3147 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3148 $OldOwnerObj->Name, $NewOwnerObj->Name );
3150 # TODO: make sure the trans committed properly
3152 return ( $trans, $msg );
3162 A convenince method to set the ticket's owner to the current user
3168 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3177 Convenience method to set the owner to 'nobody' if the current user is the owner.
3183 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3192 A convenience method to change the owner of the current ticket to the
3193 current user. Even if it's owned by another user.
3200 if ( $self->IsOwner( $self->CurrentUser ) ) {
3201 return ( 0, $self->loc("You already own this ticket") );
3204 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3214 # {{{ Routines dealing with status
3216 # {{{ sub ValidateStatus
3218 =head2 ValidateStatus STATUS
3220 Takes a string. Returns true if that status is a valid status for this ticket.
3221 Returns false otherwise.
3225 sub ValidateStatus {
3229 #Make sure the status passed in is valid
3230 unless ( $self->QueueObj->IsValidStatus($status) ) {
3242 =head2 SetStatus STATUS
3244 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3246 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.
3250 my $tt = RT::Ticket->new($RT::SystemUser);
3251 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3254 ok($tt->Status eq 'new', "New ticket is created as new");
3256 ($id, $msg) = $tt->SetStatus('open');
3258 ok ($msg =~ /open/i, "Status message is correct");
3259 ($id, $msg) = $tt->SetStatus('resolved');
3261 ok ($msg =~ /resolved/i, "Status message is correct");
3262 ($id, $msg) = $tt->SetStatus('resolved');
3276 $args{Status} = shift;
3283 if ( $args{Status} eq 'deleted') {
3284 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3285 return ( 0, $self->loc('Permission Denied') );
3288 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3289 return ( 0, $self->loc('Permission Denied') );
3293 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3294 return (0, $self->loc('That ticket has unresolved dependencies'));
3297 my $now = RT::Date->new( $self->CurrentUser );
3300 #If we're changing the status from new, record that we've started
3301 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3303 #Set the Started time to "now"
3304 $self->_Set( Field => 'Started',
3306 RecordTransaction => 0 );
3309 if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) {
3311 #When we resolve a ticket, set the 'Resolved' attribute to now.
3312 $self->_Set( Field => 'Resolved',
3314 RecordTransaction => 0 );
3317 #Actually update the status
3318 my ($val, $msg)= $self->_Set( Field => 'Status',
3319 Value => $args{Status},
3321 TransactionType => 'Status' );
3332 Takes no arguments. Marks this ticket for garbage collection
3338 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead.");
3339 return $self->Delete;
3344 return ( $self->SetStatus('deleted') );
3346 # TODO: garbage collection
3355 Sets this ticket's status to stalled
3361 return ( $self->SetStatus('stalled') );
3370 Sets this ticket's status to rejected
3376 return ( $self->SetStatus('rejected') );
3385 Sets this ticket\'s status to Open
3391 return ( $self->SetStatus('open') );
3400 Sets this ticket\'s status to Resolved
3406 return ( $self->SetStatus('resolved') );
3413 # {{{ Routines dealing with custom fields
3416 # {{{ FirstCustomFieldValue
3418 =item FirstCustomFieldValue FIELD
3420 Return the content of the first value of CustomField FIELD for this ticket
3421 Takes a field id or name
3425 sub FirstCustomFieldValue {
3428 my $values = $self->CustomFieldValues($field);
3429 if ($values->First) {
3430 return $values->First->Content;
3439 # {{{ CustomFieldValues
3441 =item CustomFieldValues FIELD
3443 Return a TicketCustomFieldValues object of all values of CustomField FIELD for this ticket.
3444 Takes a field id or name.
3449 sub CustomFieldValues {
3453 my $cf = RT::CustomField->new($self->CurrentUser);
3455 if ($field =~ /^\d+$/) {
3456 $cf->LoadById($field);
3458 $cf->LoadByNameAndQueue(Name => $field, Queue => $self->QueueObj->Id);
3460 my $cf_values = RT::TicketCustomFieldValues->new( $self->CurrentUser );
3461 $cf_values->LimitToCustomField($cf->id);
3462 $cf_values->LimitToTicket($self->Id());
3463 $cf_values->OrderBy( FIELD => 'id' );
3465 # @values is a CustomFieldValues object;
3466 return ($cf_values);
3471 # {{{ AddCustomFieldValue
3473 =item AddCustomFieldValue { Field => FIELD, Value => VALUE }
3475 VALUE should be a string.
3476 FIELD can be a CustomField object OR a CustomField ID.
3479 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
3480 deletes the old value.
3481 If VALUE isn't a valid value for the custom field, returns
3482 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3486 sub AddCustomFieldValue {
3488 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3489 return ( 0, $self->loc("Permission Denied") );
3491 $self->_AddCustomFieldValue(@_);
3494 sub _AddCustomFieldValue {
3499 RecordTransaction => 1,
3503 my $cf = RT::CustomField->new( $self->CurrentUser );
3504 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3505 $cf->Load( $args{'Field'}->id );
3508 $cf->Load( $args{'Field'} );
3511 unless ( $cf->Id ) {
3512 return ( 0, $self->loc("Custom field [_1] not found", $args{'Field'}) );
3515 # Load up a TicketCustomFieldValues object for this custom field and this ticket
3516 my $values = $cf->ValuesForTicket( $self->id );
3518 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
3519 return ( 0, $self->loc("Invalid value for custom field") );
3522 # If the custom field only accepts a single value, delete the existing
3523 # value and record a "changed from foo to bar" transaction
3524 if ( $cf->SingleValue ) {
3526 # We need to whack any old values here. In most cases, the custom field should
3527 # only have one value to delete. In the pathalogical case, this custom field
3528 # used to be a multiple and we have many values to whack....
3529 my $cf_values = $values->Count;
3531 if ( $cf_values > 1 ) {
3532 my $i = 0; #We want to delete all but the last one, so we can then
3533 # execute the same code to "change" the value from old to new
3534 while ( my $value = $values->Next ) {
3536 if ( $i < $cf_values ) {
3537 my $old_value = $value->Content;
3538 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $value->Content);
3542 my ( $TransactionId, $Msg, $TransactionObj ) =
3543 $self->_NewTransaction(
3544 Type => 'CustomField',
3546 OldValue => $old_value
3553 if (my $value = $cf->ValuesForTicket( $self->Id )->First) {
3554 $old_value = $value->Content();
3555 return (1) if $old_value eq $args{'Value'};
3558 my ( $new_value_id, $value_msg ) = $cf->AddValueForTicket(
3559 Ticket => $self->Id,
3560 Content => $args{'Value'}
3563 unless ($new_value_id) {
3565 $self->loc("Could not add new custom field value for ticket. [_1] ",
3569 my $new_value = RT::TicketCustomFieldValue->new( $self->CurrentUser );
3570 $new_value->Load($new_value_id);
3572 # now that adding the new value was successful, delete the old one
3574 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $old_value);
3580 if ($args{'RecordTransaction'}) {
3581 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3582 Type => 'CustomField',
3584 OldValue => $old_value,
3585 NewValue => $new_value->Content
3589 if ( $old_value eq '' ) {
3590 return ( 1, $self->loc("[_1] [_2] added", $cf->Name, $new_value->Content) );
3592 elsif ( $new_value->Content eq '' ) {
3593 return ( 1, $self->loc("[_1] [_2] deleted", $cf->Name, $old_value) );
3596 return ( 1, $self->loc("[_1] [_2] changed to [_3]", $cf->Name, $old_value, $new_value->Content ) );
3601 # otherwise, just add a new value and record "new value added"
3603 my ( $new_value_id ) = $cf->AddValueForTicket(
3604 Ticket => $self->Id,
3605 Content => $args{'Value'}
3608 unless ($new_value_id) {
3610 $self->loc("Could not add new custom field value for ticket. "));
3612 if ( $args{'RecordTransaction'} ) {
3613 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3614 Type => 'CustomField',
3616 NewValue => $args{'Value'}
3618 unless ($TransactionId) {
3620 $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
3623 return ( 1, $self->loc("[_1] added as a value for [_2]",$args{'Value'}, $cf->Name));
3630 # {{{ DeleteCustomFieldValue
3632 =item DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
3634 Deletes VALUE as a value of CustomField FIELD.
3636 VALUE can be a string, a CustomFieldValue or a TicketCustomFieldValue.
3638 If VALUE isn't a valid value for the custom field, returns
3639 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3643 sub DeleteCustomFieldValue {
3650 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3651 return ( 0, $self->loc("Permission Denied") );
3653 my $cf = RT::CustomField->new( $self->CurrentUser );
3654 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3655 $cf->LoadById( $args{'Field'}->id );
3658 $cf->LoadById( $args{'Field'} );
3661 unless ( $cf->Id ) {
3662 return ( 0, $self->loc("Custom field not found") );
3666 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $args{'Value'});
3670 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3671 Type => 'CustomField',
3673 OldValue => $args{'Value'}
3675 unless($TransactionId) {
3676 return(0, $self->loc("Couldn't create a transaction: [_1]", $Msg));
3679 return($TransactionId, $self->loc("[_1] is no longer a value for custom field [_2]", $args{'Value'}, $cf->Name));
3686 # {{{ Actions + Routines dealing with transactions
3688 # {{{ sub SetTold and _SetTold
3690 =head2 SetTold ISO [TIMETAKEN]
3692 Updates the told and records a transaction
3699 $told = shift if (@_);
3700 my $timetaken = shift || 0;
3702 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3703 return ( 0, $self->loc("Permission Denied") );
3706 my $datetold = new RT::Date( $self->CurrentUser );
3708 $datetold->Set( Format => 'iso',
3712 $datetold->SetToNow();
3715 return ( $self->_Set( Field => 'Told',
3716 Value => $datetold->ISO,
3717 TimeTaken => $timetaken,
3718 TransactionType => 'Told' ) );
3723 Updates the told without a transaction or acl check. Useful when we're sending replies.
3730 my $now = new RT::Date( $self->CurrentUser );
3733 #use __Set to get no ACLs ;)
3734 return ( $self->__Set( Field => 'Told',
3735 Value => $now->ISO ) );
3740 # {{{ sub Transactions
3744 Returns an RT::Transactions object of all transactions on this ticket
3751 use RT::Transactions;
3752 my $transactions = RT::Transactions->new( $self->CurrentUser );
3754 #If the user has no rights, return an empty object
3755 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3756 my $tickets = $transactions->NewAlias('Tickets');
3757 $transactions->Join(
3763 $transactions->Limit(
3765 FIELD => 'EffectiveId',
3766 VALUE => $self->id()
3769 # if the user may not see comments do not return them
3770 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3771 $transactions->Limit(
3779 return ($transactions);
3784 # {{{ sub _NewTransaction
3786 sub _NewTransaction {
3799 require RT::Transaction;
3800 my $trans = new RT::Transaction( $self->CurrentUser );
3801 my ( $transaction, $msg ) = $trans->Create(
3802 Ticket => $self->Id,
3803 TimeTaken => $args{'TimeTaken'},
3804 Type => $args{'Type'},
3805 Data => $args{'Data'},
3806 Field => $args{'Field'},
3807 NewValue => $args{'NewValue'},
3808 OldValue => $args{'OldValue'},
3809 MIMEObj => $args{'MIMEObj'}
3813 $self->Load($self->Id);
3815 $RT::Logger->warning($msg) unless $transaction;
3817 $self->_SetLastUpdated;
3819 if ( defined $args{'TimeTaken'} ) {
3820 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
3822 if ( $RT::UseTransactionBatch and $transaction ) {
3823 push @{$self->{_TransactionBatch}}, $trans;
3825 return ( $transaction, $msg, $trans );
3830 =head2 TransactionBatch
3832 Returns an array reference of all transactions created on this ticket during
3833 this ticket object's lifetime, or undef if there were none.
3835 Only works when the $RT::UseTransactionBatch config variable is set to true.
3839 sub TransactionBatch {
3841 return $self->{_TransactionBatch};
3847 # The following line eliminates reentrancy.
3848 # It protects against the fact that perl doesn't deal gracefully
3849 # when an object's refcount is changed in its destructor.
3850 return if $self->{_Destroyed}++;
3852 my $batch = $self->TransactionBatch or return;
3854 RT::Scrips->new($RT::SystemUser)->Apply(
3855 Stage => 'TransactionBatch',
3857 TransactionObj => $batch->[0],
3863 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3865 # {{{ sub _ClassAccessible
3867 sub _ClassAccessible {
3869 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3870 Queue => { 'read' => 1, 'write' => 1 },
3871 Requestors => { 'read' => 1, 'write' => 1 },
3872 Owner => { 'read' => 1, 'write' => 1 },
3873 Subject => { 'read' => 1, 'write' => 1 },
3874 InitialPriority => { 'read' => 1, 'write' => 1 },
3875 FinalPriority => { 'read' => 1, 'write' => 1 },
3876 Priority => { 'read' => 1, 'write' => 1 },
3877 Status => { 'read' => 1, 'write' => 1 },
3878 TimeEstimated => { 'read' => 1, 'write' => 1 },
3879 TimeWorked => { 'read' => 1, 'write' => 1 },
3880 TimeLeft => { 'read' => 1, 'write' => 1 },
3881 Created => { 'read' => 1, 'auto' => 1 },
3882 Creator => { 'read' => 1, 'auto' => 1 },
3883 Told => { 'read' => 1, 'write' => 1 },
3884 Resolved => { 'read' => 1 },
3885 Type => { 'read' => 1 },
3886 Starts => { 'read' => 1, 'write' => 1 },
3887 Started => { 'read' => 1, 'write' => 1 },
3888 Due => { 'read' => 1, 'write' => 1 },
3889 Creator => { 'read' => 1, 'auto' => 1 },
3890 Created => { 'read' => 1, 'auto' => 1 },
3891 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3892 LastUpdated => { 'read' => 1, 'auto' => 1 }
3904 my %args = ( Field => undef,
3907 RecordTransaction => 1,
3910 TransactionType => 'Set',
3913 if ($args{'CheckACL'}) {
3914 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3915 return ( 0, $self->loc("Permission Denied"));
3919 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3920 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3921 return(0, $self->loc("Internal Error"));
3924 #if the user is trying to modify the record
3926 #Take care of the old value we really don't want to get in an ACL loop.
3927 # so ask the super::_Value
3928 my $Old = $self->SUPER::_Value("$args{'Field'}");
3931 if ( $args{'UpdateTicket'} ) {
3934 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3935 Value => $args{'Value'} );
3937 #If we can't actually set the field to the value, don't record
3938 # a transaction. instead, get out of here.
3939 if ( $ret == 0 ) { return ( 0, $msg ); }
3942 if ( $args{'RecordTransaction'} == 1 ) {
3944 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3945 Type => $args{'TransactionType'},
3946 Field => $args{'Field'},
3947 NewValue => $args{'Value'},
3949 TimeTaken => $args{'TimeTaken'},
3951 return ( $Trans, scalar $TransObj->Description );
3954 return ( $ret, $msg );
3964 Takes the name of a table column.
3965 Returns its value as a string, if the user passes an ACL check
3974 #if the field is public, return it.
3975 if ( $self->_Accessible( $field, 'public' ) ) {
3977 #$RT::Logger->debug("Skipping ACL check for $field\n");
3978 return ( $self->SUPER::_Value($field) );
3982 #If the current user doesn't have ACLs, don't let em at it.
3984 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3987 return ( $self->SUPER::_Value($field) );
3993 # {{{ sub _UpdateTimeTaken
3995 =head2 _UpdateTimeTaken
3997 This routine will increment the timeworked counter. it should
3998 only be called from _NewTransaction
4002 sub _UpdateTimeTaken {
4004 my $Minutes = shift;
4007 $Total = $self->SUPER::_Value("TimeWorked");
4008 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
4010 Field => "TimeWorked",
4021 # {{{ Routines dealing with ACCESS CONTROL
4023 # {{{ sub CurrentUserHasRight
4025 =head2 CurrentUserHasRight
4027 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
4028 1 if the user has that right. It returns 0 if the user doesn't have that right.
4032 sub CurrentUserHasRight {
4038 Principal => $self->CurrentUser->UserObj(),
4051 Takes a paramhash with the attributes 'Right' and 'Principal'
4052 'Right' is a ticket-scoped textual right from RT::ACE
4053 'Principal' is an RT::User object
4055 Returns 1 if the principal has the right. Returns undef if not.
4067 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
4069 $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
4073 $args{'Principal'}->HasRight(
4075 Right => $args{'Right'}
4088 Jesse Vincent, jesse@bestpractical.com