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',
160 MergedInto => { Type => 'MergedInto',
168 # A helper table for relationships mapping to make it easier
169 # to build and parse links between tickets
171 use vars '%LINKDIRMAP';
174 MemberOf => { Base => 'MemberOf',
175 Target => 'HasMember', },
176 RefersTo => { Base => 'RefersTo',
177 Target => 'ReferredToBy', },
178 DependsOn => { Base => 'DependsOn',
179 Target => 'DependedOnBy', },
180 MergedInto => { Base => 'MergedInto',
181 Target => 'MergedInto', },
191 Takes a single argument. This can be a ticket id, ticket alias or
192 local ticket uri. If the ticket can't be loaded, returns undef.
193 Otherwise, returns the ticket id.
201 #TODO modify this routine to look at EffectiveId and do the recursive load
202 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
204 #If it's a local URI, turn it into a ticket id
205 if ( $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
209 #If it's a remote URI, we're going to punt for now
210 elsif ( $id =~ '://' ) {
214 #If we have an integer URI, load the ticket
215 if ( $id =~ /^\d+$/ ) {
216 my $ticketid = $self->LoadById($id);
219 $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
224 #It's not a URI. It's not a numerical ticket ID. Punt!
229 #If we're merged, resolve the merge.
230 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
231 return ( $self->Load( $self->EffectiveId ) );
234 #Ok. we're loaded. lets get outa here.
235 return ( $self->Id );
245 Given a local ticket URI, loads the specified ticket.
253 if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
255 return ( $self->Load($id) );
268 Arguments: ARGS is a hash of named parameters. Valid parameters are:
271 Queue - Either a Queue object or a Queue Name
272 Requestor - A reference to a list of RT::User objects, email addresses or RT user Names
273 Cc - A reference to a list of RT::User objects, email addresses or Names
274 AdminCc - A reference to a list of RT::User objects, email addresses or Names
275 Type -- The ticket\'s type. ignore this for now
276 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
277 Subject -- A string describing the subject of the ticket
278 InitialPriority -- an integer from 0 to 99
279 FinalPriority -- an integer from 0 to 99
280 Status -- any valid status (Defined in RT::Queue)
281 TimeEstimated -- an integer. estimated time for this task in minutes
282 TimeWorked -- an integer. time worked so far in minutes
283 TimeLeft -- an integer. time remaining in minutes
284 Starts -- an ISO date describing the ticket\'s start date and time in GMT
285 Due -- an ISO date describing the ticket\'s due date and time in GMT
286 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
287 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
290 Returns: TICKETID, Transaction Object, Error Message
295 my $t = RT::Ticket->new($RT::SystemUser);
297 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");
299 ok ( my $id = $t->Id, "Got ticket id");
300 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
301 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
302 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
311 my %args = ( id => undef,
312 EffectiveId => undef,
320 InitialPriority => undef,
321 FinalPriority => undef,
332 _RecordTransaction => 1,
338 my ( $ErrStr, $Owner, $resolved );
339 my (@non_fatal_errors);
341 my $QueueObj = RT::Queue->new($RT::SystemUser);
344 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
345 $QueueObj->Load( $args{'Queue'} );
347 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
348 $QueueObj->Load( $args{'Queue'}->Id );
351 $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object.");
355 #Can't create a ticket without a queue.
356 unless ( defined($QueueObj) && $QueueObj->Id ) {
357 $RT::Logger->debug("$self No queue given for ticket creation.");
358 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
361 #Now that we have a queue, Check the ACLS
362 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket',
363 Object => $QueueObj )
366 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name ) );
369 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
370 return ( 0, 0, $self->loc('Invalid value for status') );
374 #Since we have a queue, we can set queue defaults
377 # If there's no queue default initial priority and it's not set, set it to 0
378 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
379 unless ( defined $args{'InitialPriority'} );
383 # If there's no queue default final priority and it's not set, set it to 0
384 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
385 unless ( defined $args{'FinalPriority'} );
387 # Priority may have changed from InitialPriority, for the case
388 # where we're importing tickets (eg, from an older RT version.)
389 my $priority = $args{'Priority'} || $args{'InitialPriority'};
393 #TODO we should see what sort of due date we're getting, rather +
394 # than assuming it's in ISO format.
396 #Set the due date. if we didn't get fed one, use the queue default due in
397 my $Due = new RT::Date( $self->CurrentUser );
399 if ( $args{'Due'} ) {
400 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
402 elsif ( $QueueObj->DefaultDueIn ) {
404 $Due->AddDays( $QueueObj->DefaultDueIn );
407 my $Starts = new RT::Date( $self->CurrentUser );
408 if ( defined $args{'Starts'} ) {
409 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
412 my $Started = new RT::Date( $self->CurrentUser );
413 if ( defined $args{'Started'} ) {
414 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
417 my $Resolved = new RT::Date( $self->CurrentUser );
418 if ( defined $args{'Resolved'} ) {
419 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
423 #If the status is an inactive status, set the resolved date
424 if ($QueueObj->IsInactiveStatus($args{'Status'}) && !$args{'Resolved'}) {
425 $RT::Logger->debug("Got a ".$args{'Status'} . "ticket with a resolved of ".$args{'Resolved'});
431 # {{{ Dealing with time fields
433 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
434 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
435 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
439 # {{{ Deal with setting the owner
441 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
442 $Owner = $args{'Owner'};
445 #If we've been handed something else, try to load the user.
446 elsif ( defined $args{'Owner'} ) {
447 $Owner = RT::User->new( $self->CurrentUser );
448 $Owner->Load( $args{'Owner'} );
452 #If we have a proposed owner and they don't have the right
453 #to own a ticket, scream about it and make them not the owner
454 if ( ( defined($Owner) )
456 and ( $Owner->Id != $RT::Nobody->Id )
457 and ( !$Owner->HasRight( Object => $QueueObj,
458 Right => 'OwnTicket' ) )
461 $RT::Logger->warning( "User "
465 . "as a ticket owner but has no rights to own "
466 . "tickets in ".$QueueObj->Name );
468 push @non_fatal_errors, $self->loc("Invalid owner. Defaulting to 'nobody'.");
473 #If we haven't been handed a valid owner, make it nobody.
474 unless ( defined($Owner) && $Owner->Id ) {
475 $Owner = new RT::User( $self->CurrentUser );
476 $Owner->Load( $RT::Nobody->Id );
481 # We attempt to load or create each of the people who might have a role for this ticket
482 # _outside_ the transaction, so we don't get into ticket creation races
483 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
484 next unless (defined $args{$type});
485 foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
486 my $user = RT::User->new($RT::SystemUser);
487 $user->LoadOrCreateByEmail($watcher) if ($watcher && $watcher !~ /^\d+$/);
492 $RT::Handle->BeginTransaction();
494 my %params =( Queue => $QueueObj->Id,
496 Subject => $args{'Subject'},
497 InitialPriority => $args{'InitialPriority'},
498 FinalPriority => $args{'FinalPriority'},
499 Priority => $priority,
500 Status => $args{'Status'},
501 TimeWorked => $args{'TimeWorked'},
502 TimeEstimated => $args{'TimeEstimated'},
503 TimeLeft => $args{'TimeLeft'},
504 Type => $args{'Type'},
505 Starts => $Starts->ISO,
506 Started => $Started->ISO,
507 Resolved => $Resolved->ISO,
510 # Parameters passed in during an import that we probably don't want to touch, otherwise
511 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
512 $params{$attr} = $args{$attr} if ($args{$attr});
515 # Delete null integer parameters
516 foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
517 delete $params{$attr} unless (exists $params{$attr} && $params{$attr});
520 # Delete the time worked if we're counting it in the transaction
521 delete $params{TimeWorked} if $args{'_RecordTransaction'};
523 my ($id,$ticket_message) = $self->SUPER::Create( %params);
525 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message);
526 $RT::Handle->Rollback();
527 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
530 #Set the ticket's effective ID now that we've created it.
531 my ( $val, $msg ) = $self->__Set( Field => 'EffectiveId', Value => ($args{'EffectiveId'} || $id ) );
534 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
535 $RT::Handle->Rollback();
536 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
539 my $create_groups_ret = $self->_CreateTicketGroups();
540 unless ($create_groups_ret) {
541 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
543 . ". aborting Ticket creation." );
544 $RT::Handle->Rollback();
546 $self->loc( "Ticket could not be created due to an internal error") );
549 # Set the owner in the Groups table
550 # We denormalize it into the Ticket table too because doing otherwise would
551 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
553 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId , InsideTransaction => 1);
555 # {{{ Deal with setting up watchers
558 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
559 next unless (defined $args{$type});
560 foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
562 # If there is an empty entry in the list, let's get out of here.
563 next unless $watcher;
565 # we reason that all-digits number must be a principal id, not email
566 # this is the only way to can add
568 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
572 if ( $type eq 'AdminCc' ) {
574 # Note that we're using AddWatcher, rather than _AddWatcher, as we
575 # actually _want_ that ACL check. Otherwise, random ticket creators
576 # could make themselves adminccs and maybe get ticket rights. that would
578 ( $wval, $wmsg ) = $self->AddWatcher( Type => $type,
583 ( $wval, $wmsg ) = $self->_AddWatcher( Type => $type,
588 push @non_fatal_errors, $wmsg unless ($wval);
593 # {{{ Deal with setting up links
596 foreach my $type ( keys %LINKTYPEMAP ) {
597 next unless (defined $args{$type});
599 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
601 my ( $wval, $wmsg ) = $self->AddLink(
602 Type => $LINKTYPEMAP{$type}->{'Type'},
603 $LINKTYPEMAP{$type}->{'Mode'} => $link,
607 push @non_fatal_errors, $wmsg unless ($wval);
613 # {{{ Add all the custom fields
615 foreach my $arg ( keys %args ) {
616 next unless ( $arg =~ /^CustomField-(\d+)$/i );
619 my $value ( ref( $args{$arg} ) ? @{ $args{$arg} } : ( $args{$arg} ) ) {
620 next unless (length($value));
621 $self->_AddCustomFieldValue( Field => $cfid,
623 RecordTransaction => 0
629 if ( $args{'_RecordTransaction'} ) {
630 # {{{ Add a transaction for the create
631 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
633 TimeTaken => $args{'TimeWorked'},
634 MIMEObj => $args{'MIMEObj'}
638 if ( $self->Id && $Trans ) {
639 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
640 $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
642 $RT::Logger->info("Ticket ".$self->Id. " created in queue '".$QueueObj->Name."' by ".$self->CurrentUser->Name);
645 $RT::Handle->Rollback();
647 # TODO where does this get errstr from?
648 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
649 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
652 $RT::Handle->Commit();
653 return ( $self->Id, $TransObj->Id, $ErrStr );
658 # Not going to record a transaction
659 $RT::Handle->Commit();
660 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
661 $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
662 return ( $self->Id, $0, $ErrStr );
670 # {{{ sub CreateFromEmailMessage
673 =head2 CreateFromEmailMessage { Message, Queue, ExtractActorFromHeaders }
675 This code replaces what was once a large part of the email gateway.
676 It takes an email message as a parameter, parses out the sender, subject
677 and a MIME object. It then creates a ticket based on those attributes
681 sub CreateFromEmailMessage {
683 my %args = ( Message => undef,
685 ExtractActorFromSender => undef,
705 CreateTickets uses the template as a template for an ordered set of tickets
706 to create. The basic format is as follows:
709 ===Create-Ticket: identifier
717 =head2 Acceptable fields
719 A complete list of acceptable fields for this beastie:
722 * Queue => Name or id# of a queue
723 Subject => A text string
724 Status => A valid status. defaults to 'new'
726 Due => Dates can be specified in seconds since the epoch
727 to be handled literally or in a semi-free textual
728 format which RT will attempt to parse.
732 Owner => Username or id of an RT user who can and should own
734 + Requestor => Email address
735 + Cc => Email address
736 + AdminCc => Email address
749 Content => content. Can extend to multiple lines. Everything
750 within a template after a Content: header is treated
751 as content until we hit a line containing only
753 ContentType => the content-type of the Content field
754 CustomField-<id#> => custom field value
756 Fields marked with an * are required.
758 Fields marked with a + man have multiple values, simply
759 by repeating the fieldname on a new line with an additional value.
762 When parsed, field names are converted to lowercase and have -s stripped.
763 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
764 be treated as the same thing.
782 my %args = $self->_Parse822HeadersForAttributes($content);
784 # Now we have a %args to work with.
785 # Make sure we have at least the minimum set of
786 # reasonable data and do our thang
787 my $ticket = RT::Ticket->new($RT::SystemUser);
790 Queue => $args{'queue'},
791 Subject => $args{'subject'},
792 Status => $args{'status'},
794 Starts => $args{'starts'},
795 Started => $args{'started'},
796 Resolved => $args{'resolved'},
797 Owner => $args{'owner'},
798 Requestor => $args{'requestor'},
800 AdminCc => $args{'admincc'},
801 TimeWorked => $args{'timeworked'},
802 TimeEstimated => $args{'timeestimated'},
803 TimeLeft => $args{'timeleft'},
804 InitialPriority => $args{'initialpriority'},
805 FinalPriority => $args{'finalpriority'},
806 Type => $args{'type'},
807 DependsOn => $args{'dependson'},
808 DependedOnBy => $args{'dependedonby'},
809 RefersTo => $args{'refersto'},
810 ReferredToBy => $args{'referredtoby'},
811 Members => $args{'members'},
812 MemberOf => $args{'memberof'},
813 MIMEObj => $args{'mimeobj'}
816 # Add custom field entries to %ticketargs.
817 # TODO: allow named custom fields
819 /^customfield-(\d+)$/
820 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
823 my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs);
825 $RT::Logger->error( "Couldn't create a related ticket for "
826 . $self->TicketObj->Id . " "
837 =head2 UpdateFrom822 $MESSAGE
839 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
840 Returns an um. ask me again when the code exists
845 my $simple_update = <<EOF;
847 AddRequestor: jesse\@example.com
850 my $ticket = RT::Ticket->new($RT::SystemUser);
851 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
852 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
853 $ticket->UpdateFrom822($simple_update);
854 is($ticket->Subject, 'target', "changed the subject");
855 my $jesse = RT::User->new($RT::SystemUser);
856 $jesse->LoadByEmail('jesse@example.com');
857 ok ($jesse->Id, "There's a user for jesse");
858 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
868 my %args = $self->_Parse822HeadersForAttributes($content);
872 Queue => $args{'queue'},
873 Subject => $args{'subject'},
874 Status => $args{'status'},
876 Starts => $args{'starts'},
877 Started => $args{'started'},
878 Resolved => $args{'resolved'},
879 Owner => $args{'owner'},
880 Requestor => $args{'requestor'},
882 AdminCc => $args{'admincc'},
883 TimeWorked => $args{'timeworked'},
884 TimeEstimated => $args{'timeestimated'},
885 TimeLeft => $args{'timeleft'},
886 InitialPriority => $args{'initialpriority'},
887 Priority => $args{'priority'},
888 FinalPriority => $args{'finalpriority'},
889 Type => $args{'type'},
890 DependsOn => $args{'dependson'},
891 DependedOnBy => $args{'dependedonby'},
892 RefersTo => $args{'refersto'},
893 ReferredToBy => $args{'referredtoby'},
894 Members => $args{'members'},
895 MemberOf => $args{'memberof'},
896 MIMEObj => $args{'mimeobj'}
899 foreach my $type qw(Requestor Cc Admincc) {
901 foreach my $action ( 'Add', 'Del', '' ) {
903 my $lctag = lc($action) . lc($type);
904 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
906 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
907 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
912 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
917 # Add custom field entries to %ticketargs.
918 # TODO: allow named custom fields
920 /^customfield-(\d+)$/
921 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
924 # for each ticket we've been told to update, iterate through the set of
925 # rfc822 headers and perform that update to the ticket.
928 # {{{ Set basic fields
942 # Resolve the queue from a name to a numeric id.
943 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
944 my $tempqueue = RT::Queue->new($RT::SystemUser);
945 $tempqueue->Load( $ticketargs{'Queue'} );
946 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
949 # die "updaterecordobject is a webui thingy";
952 foreach my $attribute (@attribs) {
953 my $value = $ticketargs{$attribute};
955 if ( $value ne $self->$attribute() ) {
957 my $method = "Set$attribute";
958 my ( $code, $msg ) = $self->$method($value);
960 push @results, $self->loc($attribute) . ': ' . $msg;
965 # We special case owner changing, so we can use ForceOwnerChange
966 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
967 my $ChownType = "Give";
968 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
970 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
971 push ( @results, $msg );
975 # Deal with setting watchers
978 # Acceptable arguments:
985 foreach my $type qw(Requestor Cc AdminCc) {
987 # If we've been given a number of delresses to del, do it.
988 foreach my $address (@{$ticketargs{'Del'.$type}}) {
989 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
990 push (@results, $msg) ;
993 # If we've been given a number of addresses to add, do it.
994 foreach my $address (@{$ticketargs{'Add'.$type}}) {
995 $RT::Logger->debug("Adding $address as a $type");
996 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
997 push (@results, $msg) ;
1008 # {{{ _Parse822HeadersForAttributes Content
1010 =head2 _Parse822HeadersForAttributes Content
1012 Takes an RFC822 style message and parses its attributes into a hash.
1016 sub _Parse822HeadersForAttributes {
1018 my $content = shift;
1021 my @lines = ( split ( /\n/, $content ) );
1022 while ( defined( my $line = shift @lines ) ) {
1023 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1028 if ( defined( $args{$tag} ) )
1029 { #if we're about to get a second value, make it an array
1030 $args{$tag} = [ $args{$tag} ];
1032 if ( ref( $args{$tag} ) )
1033 { #If it's an array, we want to push the value
1034 push @{ $args{$tag} }, $value;
1036 else { #if there's nothing there, just set the value
1037 $args{$tag} = $value;
1039 } elsif ($line =~ /^$/) {
1041 #TODO: this won't work, since "" isn't of the form "foo:value"
1043 while ( defined( my $l = shift @lines ) ) {
1044 push @{ $args{'content'} }, $l;
1050 foreach my $date qw(due starts started resolved) {
1051 my $dateobj = RT::Date->new($RT::SystemUser);
1052 if ( $args{$date} =~ /^\d+$/ ) {
1053 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1056 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1058 $args{$date} = $dateobj->ISO;
1060 $args{'mimeobj'} = MIME::Entity->new();
1061 $args{'mimeobj'}->build(
1062 Type => ( $args{'contenttype'} || 'text/plain' ),
1063 Data => ($args{'content'} || '')
1073 =head2 Import PARAMHASH
1076 Doesn\'t create a transaction.
1077 Doesn\'t supply queue defaults, etc.
1085 my ( $ErrStr, $QueueObj, $Owner );
1089 EffectiveId => undef,
1093 Owner => $RT::Nobody->Id,
1094 Subject => '[no subject]',
1095 InitialPriority => undef,
1096 FinalPriority => undef,
1107 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1108 $QueueObj = RT::Queue->new($RT::SystemUser);
1109 $QueueObj->Load( $args{'Queue'} );
1111 #TODO error check this and return 0 if it\'s not loading properly +++
1113 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1114 $QueueObj = RT::Queue->new($RT::SystemUser);
1115 $QueueObj->Load( $args{'Queue'}->Id );
1119 "$self " . $args{'Queue'} . " not a recognised queue object." );
1122 #Can't create a ticket without a queue.
1123 unless ( defined($QueueObj) and $QueueObj->Id ) {
1124 $RT::Logger->debug("$self No queue given for ticket creation.");
1125 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1128 #Now that we have a queue, Check the ACLS
1130 $self->CurrentUser->HasRight(
1131 Right => 'CreateTicket',
1137 $self->loc("No permission to create tickets in the queue '[_1]'"
1138 , $QueueObj->Name));
1141 # {{{ Deal with setting the owner
1143 # Attempt to take user object, user name or user id.
1144 # Assign to nobody if lookup fails.
1145 if ( defined( $args{'Owner'} ) ) {
1146 if ( ref( $args{'Owner'} ) ) {
1147 $Owner = $args{'Owner'};
1150 $Owner = new RT::User( $self->CurrentUser );
1151 $Owner->Load( $args{'Owner'} );
1152 if ( !defined( $Owner->id ) ) {
1153 $Owner->Load( $RT::Nobody->id );
1158 #If we have a proposed owner and they don't have the right
1159 #to own a ticket, scream about it and make them not the owner
1162 and ( $Owner->Id != $RT::Nobody->Id )
1165 Object => $QueueObj,
1166 Right => 'OwnTicket'
1172 $RT::Logger->warning( "$self user "
1173 . $Owner->Name . "("
1176 . "as a ticket owner but has no rights to own "
1178 . $QueueObj->Name . "'\n" );
1183 #If we haven't been handed a valid owner, make it nobody.
1184 unless ( defined($Owner) ) {
1185 $Owner = new RT::User( $self->CurrentUser );
1186 $Owner->Load( $RT::Nobody->UserObj->Id );
1191 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1192 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1195 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1196 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1197 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1198 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1200 # If we're coming in with an id, set that now.
1201 my $EffectiveId = undef;
1202 if ( $args{'id'} ) {
1203 $EffectiveId = $args{'id'};
1207 my $id = $self->SUPER::Create(
1209 EffectiveId => $EffectiveId,
1210 Queue => $QueueObj->Id,
1211 Owner => $Owner->Id,
1212 Subject => $args{'Subject'}, # loc
1213 InitialPriority => $args{'InitialPriority'}, # loc
1214 FinalPriority => $args{'FinalPriority'}, # loc
1215 Priority => $args{'InitialPriority'}, # loc
1216 Status => $args{'Status'}, # loc
1217 TimeWorked => $args{'TimeWorked'}, # loc
1218 Type => $args{'Type'}, # loc
1219 Created => $args{'Created'}, # loc
1220 Told => $args{'Told'}, # loc
1221 LastUpdated => $args{'Updated'}, # loc
1222 Resolved => $args{'Resolved'}, # loc
1223 Due => $args{'Due'}, # loc
1226 # If the ticket didn't have an id
1227 # Set the ticket's effective ID now that we've created it.
1228 if ( $args{'id'} ) {
1229 $self->Load( $args{'id'} );
1233 $self->__Set( Field => 'EffectiveId', Value => $id );
1237 $self . "->Import couldn't set EffectiveId: $msg\n" );
1242 foreach $watcher ( @{ $args{'Cc'} } ) {
1243 $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 );
1245 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1246 $self->_AddWatcher( Type => 'AdminCc', Person => $watcher,
1249 foreach $watcher ( @{ $args{'Requestor'} } ) {
1250 $self->_AddWatcher( Type => 'Requestor', Person => $watcher,
1254 return ( $self->Id, $ErrStr );
1260 # {{{ Routines dealing with watchers.
1262 # {{{ _CreateTicketGroups
1264 =head2 _CreateTicketGroups
1266 Create the ticket groups and relationships for this ticket.
1267 This routine expects to be called from Ticket->Create _inside of a transaction_
1269 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1271 It will return true on success and undef on failure.
1275 my $ticket = RT::Ticket->new($RT::SystemUser);
1276 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1277 Owner => $RT::SystemUser->Id,
1279 Requestor => ['jesse@example.com'],
1282 ok ($id, "Ticket $id was created");
1283 ok(my $group = RT::Group->new($RT::SystemUser));
1284 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1285 ok ($group->Id, "Found the requestors object for this ticket");
1287 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1288 $jesse->LoadByEmail('jesse@example.com');
1289 ok($jesse->Id, "Found the jesse rt user");
1292 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1293 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1294 ok ($add_id, "Add succeeded: ($add_msg)");
1295 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1296 $bob->LoadByEmail('bob@fsck.com');
1297 ok($bob->Id, "Found the bob rt user");
1298 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1299 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1300 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1303 $group = RT::Group->new($RT::SystemUser);
1304 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1305 ok ($group->Id, "Found the cc object for this ticket");
1306 $group = RT::Group->new($RT::SystemUser);
1307 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1308 ok ($group->Id, "Found the AdminCc object for this ticket");
1309 $group = RT::Group->new($RT::SystemUser);
1310 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1311 ok ($group->Id, "Found the Owner object for this ticket");
1312 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1319 sub _CreateTicketGroups {
1322 my @types = qw(Requestor Owner Cc AdminCc);
1324 foreach my $type (@types) {
1325 my $type_obj = RT::Group->new($self->CurrentUser);
1326 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1327 Instance => $self->Id,
1330 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1331 $self->Id.": ".$msg);
1341 # {{{ sub OwnerGroup
1345 A constructor which returns an RT::Group object containing the owner of this ticket.
1351 my $owner_obj = RT::Group->new($self->CurrentUser);
1352 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1353 return ($owner_obj);
1359 # {{{ sub AddWatcher
1363 AddWatcher takes a parameter hash. The keys are as follows:
1365 Type One of Requestor, Cc, AdminCc
1367 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1369 Email The email address of the new watcher. If a user with this
1370 email address can't be found, a new nonprivileged user will be created.
1372 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.
1380 PrincipalId => undef,
1386 #If the watcher we're trying to add is for the current user
1387 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1388 # If it's an AdminCc and they don't have
1389 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1390 if ( $args{'Type'} eq 'AdminCc' ) {
1391 unless ( $self->CurrentUserHasRight('ModifyTicket')
1392 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1393 return ( 0, $self->loc('Permission Denied'))
1397 # If it's a Requestor or Cc and they don't have
1398 # 'Watch' or 'ModifyTicket', bail
1399 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1401 unless ( $self->CurrentUserHasRight('ModifyTicket')
1402 or $self->CurrentUserHasRight('Watch') ) {
1403 return ( 0, $self->loc('Permission Denied'))
1407 $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type");
1408 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1412 # If the watcher isn't the current user
1413 # and the current user doesn't have 'ModifyTicket'
1416 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1417 return ( 0, $self->loc("Permission Denied") );
1423 return ( $self->_AddWatcher(%args) );
1426 #This contains the meat of AddWatcher. but can be called from a routine like
1427 # Create, which doesn't need the additional acl check
1433 PrincipalId => undef,
1439 my $principal = RT::Principal->new($self->CurrentUser);
1440 if ($args{'Email'}) {
1441 my $user = RT::User->new($RT::SystemUser);
1442 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1444 $args{'PrincipalId'} = $pid;
1447 if ($args{'PrincipalId'}) {
1448 $principal->Load($args{'PrincipalId'});
1452 # If we can't find this watcher, we need to bail.
1453 unless ($principal->Id) {
1454 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1455 return(0, $self->loc("Could not find or create that user"));
1459 my $group = RT::Group->new($self->CurrentUser);
1460 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1461 unless ($group->id) {
1462 return(0,$self->loc("Group not found"));
1465 if ( $group->HasMember( $principal)) {
1467 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1471 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1472 InsideTransaction => 1 );
1474 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1476 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1479 unless ( $args{'Silent'} ) {
1480 $self->_NewTransaction(
1481 Type => 'AddWatcher',
1482 NewValue => $principal->Id,
1483 Field => $args{'Type'}
1487 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1493 # {{{ sub DeleteWatcher
1495 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1498 Deletes a Ticket watcher. Takes two arguments:
1500 Type (one of Requestor,Cc,AdminCc)
1504 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1506 Email (the email address of an existing wathcer)
1515 my %args = ( Type => undef,
1516 PrincipalId => undef,
1520 unless ($args{'PrincipalId'} || $args{'Email'} ) {
1521 return(0, $self->loc("No principal specified"));
1523 my $principal = RT::Principal->new($self->CurrentUser);
1524 if ($args{'PrincipalId'} ) {
1526 $principal->Load($args{'PrincipalId'});
1528 my $user = RT::User->new($self->CurrentUser);
1529 $user->LoadByEmail($args{'Email'});
1530 $principal->Load($user->Id);
1532 # If we can't find this watcher, we need to bail.
1533 unless ($principal->Id) {
1534 return(0, $self->loc("Could not find that principal"));
1537 my $group = RT::Group->new($self->CurrentUser);
1538 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1539 unless ($group->id) {
1540 return(0,$self->loc("Group not found"));
1544 #If the watcher we're trying to add is for the current user
1545 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1546 # If it's an AdminCc and they don't have
1547 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1548 if ( $args{'Type'} eq 'AdminCc' ) {
1549 unless ( $self->CurrentUserHasRight('ModifyTicket')
1550 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1551 return ( 0, $self->loc('Permission Denied'))
1555 # If it's a Requestor or Cc and they don't have
1556 # 'Watch' or 'ModifyTicket', bail
1557 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1558 unless ( $self->CurrentUserHasRight('ModifyTicket')
1559 or $self->CurrentUserHasRight('Watch') ) {
1560 return ( 0, $self->loc('Permission Denied'))
1564 $RT::Logger->warn( "$self -> DeleteWatcher got passed a bogus type");
1565 return ( 0, $self->loc('Error in parameters to Ticket->DelWatcher') );
1569 # If the watcher isn't the current user
1570 # and the current user doesn't have 'ModifyTicket' bail
1572 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1573 return ( 0, $self->loc("Permission Denied") );
1580 # see if this user is already a watcher.
1582 unless ( $group->HasMember($principal)) {
1584 $self->loc('That principal is not a [_1] for this ticket', $args{'Type'}) );
1587 my ($m_id, $m_msg) = $group->_DeleteMember($principal->Id);
1589 $RT::Logger->error("Failed to delete ".$principal->Id.
1590 " as a member of group ".$group->Id."\n".$m_msg);
1592 return ( 0, $self->loc('Could not remove that principal as a [_1] for this ticket', $args{'Type'}) );
1595 unless ( $args{'Silent'} ) {
1596 $self->_NewTransaction(
1597 Type => 'DelWatcher',
1598 OldValue => $principal->Id,
1599 Field => $args{'Type'}
1603 return ( 1, $self->loc("[_1] is no longer a [_2] for this ticket.", $principal->Object->Name, $args{'Type'} ));
1612 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1614 =head2 RequestorAddresses
1616 B<Returns> String: All Ticket Requestor email addresses as a string.
1620 sub RequestorAddresses {
1623 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1627 return ( $self->Requestors->MemberEmailAddressesAsString );
1631 =head2 AdminCcAddresses
1633 returns String: All Ticket AdminCc email addresses as a string
1637 sub AdminCcAddresses {
1640 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1644 return ( $self->AdminCc->MemberEmailAddressesAsString )
1650 returns String: All Ticket Ccs as a string of email addresses
1657 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1661 return ( $self->Cc->MemberEmailAddressesAsString);
1667 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1669 # {{{ sub Requestors
1674 Returns this ticket's Requestors as an RT::Group object
1681 my $group = RT::Group->new($self->CurrentUser);
1682 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1683 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1696 Returns an RT::Group object which contains this ticket's Ccs.
1697 If the user doesn't have "ShowTicket" permission, returns an empty group
1704 my $group = RT::Group->new($self->CurrentUser);
1705 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1706 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1719 Returns an RT::Group object which contains this ticket's AdminCcs.
1720 If the user doesn't have "ShowTicket" permission, returns an empty group
1727 my $group = RT::Group->new($self->CurrentUser);
1728 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1729 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1739 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1742 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1744 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1746 Takes a param hash with the attributes Type and either PrincipalId or Email
1748 Type is one of Requestor, Cc, AdminCc and Owner
1750 PrincipalId is an RT::Principal id, and Email is an email address.
1752 Returns true if the specified principal (or the one corresponding to the
1753 specified address) is a member of the group Type for this ticket.
1755 XX TODO: This should be Memoized.
1762 my %args = ( Type => 'Requestor',
1763 PrincipalId => undef,
1768 # Load the relevant group.
1769 my $group = RT::Group->new($self->CurrentUser);
1770 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1772 # Find the relevant principal.
1773 my $principal = RT::Principal->new($self->CurrentUser);
1774 if (!$args{PrincipalId} && $args{Email}) {
1775 # Look up the specified user.
1776 my $user = RT::User->new($self->CurrentUser);
1777 $user->LoadByEmail($args{Email});
1779 $args{PrincipalId} = $user->PrincipalId;
1782 # A non-existent user can't be a group member.
1786 $principal->Load($args{'PrincipalId'});
1788 # Ask if it has the member in question
1789 return ($group->HasMember($principal));
1794 # {{{ sub IsRequestor
1796 =head2 IsRequestor PRINCIPAL_ID
1798 Takes an RT::Principal id
1799 Returns true if the principal is a requestor of the current ticket.
1808 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1816 =head2 IsCc PRINCIPAL_ID
1818 Takes an RT::Principal id.
1819 Returns true if the principal is a requestor of the current ticket.
1828 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1836 =head2 IsAdminCc PRINCIPAL_ID
1838 Takes an RT::Principal id.
1839 Returns true if the principal is a requestor of the current ticket.
1847 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1857 Takes an RT::User object. Returns true if that user is this ticket's owner.
1858 returns undef otherwise
1866 # no ACL check since this is used in acl decisions
1867 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1871 #Tickets won't yet have owners when they're being created.
1872 unless ( $self->OwnerObj->id ) {
1876 if ( $person->id == $self->OwnerObj->id ) {
1890 # {{{ Routines dealing with queues
1892 # {{{ sub ValidateQueue
1899 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1903 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1904 my $id = $QueueObj->Load($Value);
1920 my $NewQueue = shift;
1922 #Redundant. ACL gets checked in _Set;
1923 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1924 return ( 0, $self->loc("Permission Denied") );
1927 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1928 $NewQueueObj->Load($NewQueue);
1930 unless ( $NewQueueObj->Id() ) {
1931 return ( 0, $self->loc("That queue does not exist") );
1934 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1935 return ( 0, $self->loc('That is the same value') );
1938 $self->CurrentUser->HasRight(
1939 Right => 'CreateTicket',
1940 Object => $NewQueueObj
1944 return ( 0, $self->loc("You may not create requests in that queue.") );
1948 $self->OwnerObj->HasRight(
1949 Right => 'OwnTicket',
1950 Object => $NewQueueObj
1957 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
1967 Takes nothing. returns this ticket's queue object
1974 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1976 #We call __Value so that we can avoid the ACL decision and some deep recursion
1977 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1978 return ($queue_obj);
1985 # {{{ Date printing routines
1991 Returns an RT::Date object containing this ticket's due date
1998 my $time = new RT::Date( $self->CurrentUser );
2000 # -1 is RT::Date slang for never
2002 $time->Set( Format => 'sql', Value => $self->Due );
2005 $time->Set( Format => 'unix', Value => -1 );
2013 # {{{ sub DueAsString
2017 Returns this ticket's due date as a human readable string
2023 return $self->DueObj->AsString();
2028 # {{{ sub ResolvedObj
2032 Returns an RT::Date object of this ticket's 'resolved' time.
2039 my $time = new RT::Date( $self->CurrentUser );
2040 $time->Set( Format => 'sql', Value => $self->Resolved );
2046 # {{{ sub SetStarted
2050 Takes a date in ISO format or undef
2051 Returns a transaction id and a message
2052 The client calls "Start" to note that the project was started on the date in $date.
2053 A null date means "now"
2059 my $time = shift || 0;
2061 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2062 return ( 0, self->loc("Permission Denied") );
2065 #We create a date object to catch date weirdness
2066 my $time_obj = new RT::Date( $self->CurrentUser() );
2068 $time_obj->Set( Format => 'ISO', Value => $time );
2071 $time_obj->SetToNow();
2074 #Now that we're starting, open this ticket
2075 #TODO do we really want to force this as policy? it should be a scrip
2077 #We need $TicketAsSystem, in case the current user doesn't have
2080 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2081 $TicketAsSystem->Load( $self->Id );
2082 if ( $TicketAsSystem->Status eq 'new' ) {
2083 $TicketAsSystem->Open();
2086 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2092 # {{{ sub StartedObj
2096 Returns an RT::Date object which contains this ticket's
2104 my $time = new RT::Date( $self->CurrentUser );
2105 $time->Set( Format => 'sql', Value => $self->Started );
2115 Returns an RT::Date object which contains this ticket's
2123 my $time = new RT::Date( $self->CurrentUser );
2124 $time->Set( Format => 'sql', Value => $self->Starts );
2134 Returns an RT::Date object which contains this ticket's
2142 my $time = new RT::Date( $self->CurrentUser );
2143 $time->Set( Format => 'sql', Value => $self->Told );
2149 # {{{ sub ToldAsString
2153 A convenience method that returns ToldObj->AsString
2155 TODO: This should be deprecated
2161 if ( $self->Told ) {
2162 return $self->ToldObj->AsString();
2171 # {{{ sub TimeWorkedAsString
2173 =head2 TimeWorkedAsString
2175 Returns the amount of time worked on this ticket as a Text String
2179 sub TimeWorkedAsString {
2181 return "0" unless $self->TimeWorked;
2183 #This is not really a date object, but if we diff a number of seconds
2184 #vs the epoch, we'll get a nice description of time worked.
2186 my $worked = new RT::Date( $self->CurrentUser );
2188 #return the #of minutes worked turned into seconds and written as
2189 # a simple text string
2191 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2198 # {{{ Routines dealing with correspondence/comments
2204 Comment on this ticket.
2205 Takes a hashref with the following attributes:
2206 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2209 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content.
2213 ## Please see file perltidy.ERR
2217 my %args = ( CcMessageTo => undef,
2218 BccMessageTo => undef,
2224 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2225 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2226 return ( 0, $self->loc("Permission Denied") );
2229 unless ( $args{'MIMEObj'} ) {
2230 if ( $args{'Content'} ) {
2232 $args{'MIMEObj'} = MIME::Entity->build(
2233 Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
2238 return ( 0, $self->loc("No correspondence attached") );
2242 RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
2244 # If we've been passed in CcMessageTo and BccMessageTo fields,
2245 # add them to the mime object for passing on to the transaction handler
2246 # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
2247 # RT-Send-Bcc: headers
2249 $args{'MIMEObj'}->head->add( 'RT-Send-Cc',
2250 RT::User::CanonicalizeEmailAddress(undef, $args{'CcMessageTo'}) )
2251 if defined $args{'CcMessageTo'};
2252 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2253 RT::User::CanonicalizeEmailAddress(undef, $args{'BccMessageTo'}) )
2254 if defined $args{'BccMessageTo'};
2256 #Record the correspondence (write the transaction)
2257 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2259 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2260 TimeTaken => $args{'TimeTaken'},
2261 MIMEObj => $args{'MIMEObj'}
2264 return ( $Trans, $self->loc("The comment has been recorded") );
2269 # {{{ sub Correspond
2273 Correspond on this ticket.
2274 Takes a hashref with the following attributes:
2277 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content
2279 if there's no MIMEObj, Content is used to build a MIME::Entity object
2286 my %args = ( CcMessageTo => undef,
2287 BccMessageTo => undef,
2293 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2294 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2295 return ( 0, $self->loc("Permission Denied") );
2298 unless ( $args{'MIMEObj'} ) {
2299 if ( $args{'Content'} ) {
2301 $args{'MIMEObj'} = MIME::Entity->build(
2302 Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
2308 return ( 0, $self->loc("No correspondence attached") );
2312 RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
2314 # If we've been passed in CcMessageTo and BccMessageTo fields,
2315 # add them to the mime object for passing on to the transaction handler
2316 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2319 $args{'MIMEObj'}->head->add( 'RT-Send-Cc',
2320 RT::User::CanonicalizeEmailAddress(undef, $args{'CcMessageTo'}) )
2321 if defined $args{'CcMessageTo'};
2322 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2323 RT::User::CanonicalizeEmailAddress(undef, $args{'BccMessageTo'}) )
2324 if defined $args{'BccMessageTo'};
2326 #Record the correspondence (write the transaction)
2327 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2328 Type => 'Correspond',
2329 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2330 TimeTaken => $args{'TimeTaken'},
2331 MIMEObj => $args{'MIMEObj'} );
2334 $RT::Logger->err( "$self couldn't init a transaction $msg");
2335 return ( $Trans, $self->loc("correspondence (probably) not sent"), $args{'MIMEObj'} );
2338 #Set the last told date to now if this isn't mail from the requestor.
2339 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2341 unless ( $TransObj->IsInbound ) {
2345 return ( $Trans, $self->loc("correspondence sent") );
2352 # {{{ Routines dealing with Links and Relations between tickets
2354 # {{{ Link Collections
2360 This returns an RT::Links object which references all the tickets
2361 which are 'MembersOf' this ticket
2367 return ( $self->_Links( 'Target', 'MemberOf' ) );
2376 This returns an RT::Links object which references all the tickets that this
2377 ticket is a 'MemberOf'
2383 return ( $self->_Links( 'Base', 'MemberOf' ) );
2392 This returns an RT::Links object which shows all references for which this ticket is a base
2398 return ( $self->_Links( 'Base', 'RefersTo' ) );
2407 This returns an RT::Links object which shows all references for which this ticket is a target
2413 return ( $self->_Links( 'Target', 'RefersTo' ) );
2422 This returns an RT::Links object which references all the tickets that depend on this one
2428 return ( $self->_Links( 'Target', 'DependsOn' ) );
2435 =head2 HasUnresolvedDependencies
2437 Takes a paramhash of Type (default to '__any'). Returns true if
2438 $self->UnresolvedDependencies returns an object with one or more members
2439 of that type. Returns false otherwise
2444 my $t1 = RT::Ticket->new($RT::SystemUser);
2445 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
2446 ok($id, "Created dep test 1 - $msg");
2448 my $t2 = RT::Ticket->new($RT::SystemUser);
2449 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
2450 ok($id2, "Created dep test 2 - $msg2");
2451 my $t3 = RT::Ticket->new($RT::SystemUser);
2452 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
2453 ok($id3, "Created dep test 3 - $msg3");
2455 ok ($t1->AddLink( Type => 'DependsOn', Target => $t2->id));
2456 ok ($t1->AddLink( Type => 'DependsOn', Target => $t3->id));
2458 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
2459 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
2460 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
2461 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
2462 my ($rid, $rmsg)= $t1->Resolve();
2465 ($rid, $rmsg)= $t1->Resolve();
2468 ($rid, $rmsg)= $t1->Resolve();
2476 sub HasUnresolvedDependencies {
2483 my $deps = $self->UnresolvedDependencies;
2486 $deps->Limit( FIELD => 'Type',
2488 VALUE => $args{Type});
2494 if ($deps->Count > 0) {
2503 # {{{ UnresolvedDependencies
2505 =head2 UnresolvedDependencies
2507 Returns an RT::Tickets object of tickets which this ticket depends on
2508 and which have a status of new, open or stalled. (That list comes from
2509 RT::Queue->ActiveStatusArray
2514 sub UnresolvedDependencies {
2516 my $deps = RT::Tickets->new($self->CurrentUser);
2518 my @live_statuses = RT::Queue->ActiveStatusArray();
2519 foreach my $status (@live_statuses) {
2520 $deps->LimitStatus(VALUE => $status);
2522 $deps->LimitDependedOnBy($self->Id);
2530 # {{{ AllDependedOnBy
2532 =head2 AllDependedOnBy
2534 Returns an array of RT::Ticket objects which (directly or indirectly)
2535 depends on this ticket; takes an optional 'Type' argument in the param
2536 hash, which will limit returned tickets to that type, as well as cause
2537 tickets with that type to serve as 'leaf' nodes that stops the recursive
2542 sub AllDependedOnBy {
2544 my $dep = $self->DependedOnBy;
2552 while (my $link = $dep->Next()) {
2553 next unless ($link->BaseURI->IsLocal());
2554 next if $args{_found}{$link->BaseObj->Id};
2557 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
2558 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
2560 elsif ($link->BaseObj->Type eq $args{Type}) {
2561 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
2564 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
2569 return map { $args{_found}{$_} } sort keys %{$args{_found}};
2582 This returns an RT::Links object which references all the tickets that this ticket depends on
2588 return ( $self->_Links( 'Base', 'DependsOn' ) );
2601 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2604 my $type = shift || "";
2606 unless ( $self->{"$field$type"} ) {
2607 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2608 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2609 # Maybe this ticket is a merged ticket
2610 my $Tickets = new RT::Tickets( $self->CurrentUser );
2611 # at least to myself
2612 $self->{"$field$type"}->Limit( FIELD => $field,
2613 VALUE => $self->URI,
2614 ENTRYAGGREGATOR => 'OR' );
2615 $Tickets->Limit( FIELD => 'EffectiveId',
2616 VALUE => $self->EffectiveId );
2617 while (my $Ticket = $Tickets->Next) {
2618 $self->{"$field$type"}->Limit( FIELD => $field,
2619 VALUE => $Ticket->URI,
2620 ENTRYAGGREGATOR => 'OR' );
2622 $self->{"$field$type"}->Limit( FIELD => 'Type',
2627 return ( $self->{"$field$type"} );
2634 # {{{ sub DeleteLink
2638 Delete a link. takes a paramhash of Base, Target and Type.
2639 Either Base or Target must be null. The null value will
2640 be replaced with this ticket\'s id
2654 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2655 $RT::Logger->debug("No permission to delete links\n");
2656 return ( 0, $self->loc('Permission Denied'))
2660 #we want one of base and target. we don't care which
2661 #but we only want _one_
2666 if ( $args{'Base'} and $args{'Target'} ) {
2667 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
2668 return ( 0, $self->loc("Can't specifiy both base and target") );
2670 elsif ( $args{'Base'} ) {
2671 $args{'Target'} = $self->URI();
2672 $remote_link = $args{'Base'};
2673 $direction = 'Target';
2675 elsif ( $args{'Target'} ) {
2676 $args{'Base'} = $self->URI();
2677 $remote_link = $args{'Target'};
2681 $RT::Logger->debug("$self: Base or Target must be specified\n");
2682 return ( 0, $self->loc('Either base or target must be specified') );
2685 my $link = new RT::Link( $self->CurrentUser );
2686 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
2689 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
2693 my $linkid = $link->id;
2696 my $TransString = "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
2697 my $remote_uri = RT::URI->new( $RT::SystemUser );
2698 $remote_uri->FromURI( $remote_link );
2700 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2701 Type => 'DeleteLink',
2702 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2703 OldValue => $remote_uri->URI || $remote_link,
2707 return ( $Trans, $self->loc("Link deleted ([_1])", $TransString));
2710 #if it's not a link we can find
2712 $RT::Logger->debug("Couldn't find that link\n");
2713 return ( 0, $self->loc("Link not found") );
2723 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2730 my %args = ( Target => '',
2736 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2737 return ( 0, $self->loc("Permission Denied") );
2740 # Remote_link is the URI of the object that is not this ticket
2744 if ( $args{'Base'} and $args{'Target'} ) {
2746 "$self tried to delete a link. both base and target were specified\n" );
2747 return ( 0, $self->loc("Can't specifiy both base and target") );
2749 elsif ( $args{'Base'} ) {
2750 $args{'Target'} = $self->URI();
2751 $remote_link = $args{'Base'};
2752 $direction = 'Target';
2754 elsif ( $args{'Target'} ) {
2755 $args{'Base'} = $self->URI();
2756 $remote_link = $args{'Target'};
2760 return ( 0, $self->loc('Either base or target must be specified') );
2763 # If the base isn't a URI, make it a URI.
2764 # If the target isn't a URI, make it a URI.
2766 # {{{ Check if the link already exists - we don't want duplicates
2768 my $old_link = RT::Link->new( $self->CurrentUser );
2769 $old_link->LoadByParams( Base => $args{'Base'},
2770 Type => $args{'Type'},
2771 Target => $args{'Target'} );
2772 if ( $old_link->Id ) {
2773 $RT::Logger->debug("$self Somebody tried to duplicate a link");
2774 return ( $old_link->id, $self->loc("Link already exists"), 0 );
2779 # Storing the link in the DB.
2780 my $link = RT::Link->new( $self->CurrentUser );
2781 my ($linkid) = $link->Create( Target => $args{Target},
2782 Base => $args{Base},
2783 Type => $args{Type} );
2786 return ( 0, $self->loc("Link could not be created") );
2790 "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
2792 # Don't write the transaction if we're doing this on create
2793 if ( $args{'Silent'} ) {
2794 return ( 1, $self->loc( "Link created ([_1])", $TransString ) );
2797 my $remote_uri = RT::URI->new( $RT::SystemUser );
2798 $remote_uri->FromURI( $remote_link );
2800 #Write the transaction
2801 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2803 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2804 NewValue => $remote_uri->URI || $remote_link,
2806 return ( $Trans, $self->loc( "Link created ([_1])", $TransString ) );
2817 Returns this ticket's URI
2823 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
2824 return($uri->URIForObject($self));
2832 MergeInto take the id of the ticket to merge this ticket into.
2838 my $MergeInto = shift;
2840 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2841 return ( 0, $self->loc("Permission Denied") );
2844 # Load up the new ticket.
2845 my $NewTicket = RT::Ticket->new($RT::SystemUser);
2846 $NewTicket->Load($MergeInto);
2848 # make sure it exists.
2849 unless ( defined $NewTicket->Id ) {
2850 return ( 0, $self->loc("New ticket doesn't exist") );
2853 # Make sure the current user can modify the new ticket.
2854 unless ( $NewTicket->CurrentUserHasRight('ModifyTicket') ) {
2855 $RT::Logger->debug("failed...");
2856 return ( 0, $self->loc("Permission Denied") );
2860 "checking if the new ticket has the same id and effective id...");
2861 unless ( $NewTicket->id == $NewTicket->EffectiveId ) {
2862 $RT::Logger->err( "$self trying to merge into "
2864 . " which is itself merged.\n" );
2866 $self->loc("Can't merge into a merged ticket. You should never get this error") );
2869 # We use EffectiveId here even though it duplicates information from
2870 # the links table becasue of the massive performance hit we'd take
2871 # by trying to do a separate database query for merge info everytime
2874 #update this ticket's effective id to the new ticket's id.
2875 my ( $id_val, $id_msg ) = $self->__Set(
2876 Field => 'EffectiveId',
2877 Value => $NewTicket->Id()
2882 "Couldn't set effective ID for " . $self->Id . ": $id_msg" );
2883 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2886 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2888 unless ($status_val) {
2889 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2893 # update all the links that point to that old ticket
2894 my $old_links_to = RT::Links->new($self->CurrentUser);
2895 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2897 while (my $link = $old_links_to->Next) {
2898 if ($link->Base eq $NewTicket->URI) {
2901 $link->SetTarget($NewTicket->URI);
2906 my $old_links_from = RT::Links->new($self->CurrentUser);
2907 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2909 while (my $link = $old_links_from->Next) {
2910 if ($link->Target eq $NewTicket->URI) {
2913 $link->SetBase($NewTicket->URI);
2919 #add all of this ticket's watchers to that ticket.
2920 my $requestors = $self->Requestors->MembersObj;
2921 while (my $watcher = $requestors->Next) {
2922 $NewTicket->_AddWatcher( Type => 'Requestor',
2924 PrincipalId => $watcher->MemberId);
2927 my $Ccs = $self->Cc->MembersObj;
2928 while (my $watcher = $Ccs->Next) {
2929 $NewTicket->_AddWatcher( Type => 'Cc',
2931 PrincipalId => $watcher->MemberId);
2934 my $AdminCcs = $self->AdminCc->MembersObj;
2935 while (my $watcher = $AdminCcs->Next) {
2936 $NewTicket->_AddWatcher( Type => 'AdminCc',
2938 PrincipalId => $watcher->MemberId);
2942 #find all of the tickets that were merged into this ticket.
2943 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2944 $old_mergees->Limit(
2945 FIELD => 'EffectiveId',
2950 # update their EffectiveId fields to the new ticket's id
2951 while ( my $ticket = $old_mergees->Next() ) {
2952 my ( $val, $msg ) = $ticket->__Set(
2953 Field => 'EffectiveId',
2954 Value => $NewTicket->Id()
2958 #make a new link: this ticket is merged into that other ticket.
2959 $self->AddLink( Type => 'MergedInto', Target => $NewTicket->Id());
2961 $NewTicket->_SetLastUpdated;
2963 return ( 1, $self->loc("Merge Successful") );
2970 # {{{ Routines dealing with ownership
2976 Takes nothing and returns an RT::User object of
2984 #If this gets ACLed, we lose on a rights check in User.pm and
2985 #get deep recursion. if we need ACLs here, we need
2986 #an equiv without ACLs
2988 my $owner = new RT::User( $self->CurrentUser );
2989 $owner->Load( $self->__Value('Owner') );
2991 #Return the owner object
2997 # {{{ sub OwnerAsString
2999 =head2 OwnerAsString
3001 Returns the owner's email address
3007 return ( $self->OwnerObj->EmailAddress );
3017 Takes two arguments:
3018 the Id or Name of the owner
3019 and (optionally) the type of the SetOwner Transaction. It defaults
3020 to 'Give'. 'Steal' is also a valid option.
3024 my $root = RT::User->new($RT::SystemUser);
3025 $root->Load('root');
3026 ok ($root->Id, "Loaded the root user");
3027 my $t = RT::Ticket->new($RT::SystemUser);
3029 $t->SetOwner('root');
3030 ok ($t->OwnerObj->Name eq 'root' , "Root owns the ticket");
3032 ok ($t->OwnerObj->id eq $RT::SystemUser->id , "SystemUser owns the ticket");
3033 my $txns = RT::Transactions->new($RT::SystemUser);
3034 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3035 $txns->Limit(FIELD => 'Ticket', VALUE => '1');
3036 my $steal = $txns->First;
3037 ok($steal->OldValue == $root->Id , "Stolen from root");
3038 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3046 my $NewOwner = shift;
3047 my $Type = shift || "Give";
3049 # must have ModifyTicket rights
3050 # or TakeTicket/StealTicket and $NewOwner is self
3051 # see if it's a take
3052 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
3053 unless ( $self->CurrentUserHasRight('ModifyTicket')
3054 || $self->CurrentUserHasRight('TakeTicket') ) {
3055 return ( 0, $self->loc("Permission Denied") );
3059 # see if it's a steal
3060 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
3061 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
3063 unless ( $self->CurrentUserHasRight('ModifyTicket')
3064 || $self->CurrentUserHasRight('StealTicket') ) {
3065 return ( 0, $self->loc("Permission Denied") );
3069 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3070 return ( 0, $self->loc("Permission Denied") );
3073 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3074 my $OldOwnerObj = $self->OwnerObj;
3076 $NewOwnerObj->Load($NewOwner);
3077 if ( !$NewOwnerObj->Id ) {
3078 return ( 0, $self->loc("That user does not exist") );
3081 #If thie ticket has an owner and it's not the current user
3083 if ( ( $Type ne 'Steal' )
3084 and ( $Type ne 'Force' )
3085 and #If we're not stealing
3086 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
3087 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
3088 ) { #and it's not us
3091 "You can only reassign tickets that you own or that are unowned" ) );
3094 #If we've specified a new owner and that user can't modify the ticket
3095 elsif ( ( $NewOwnerObj->Id )
3096 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
3099 return ( 0, $self->loc("That user may not own tickets in that queue") );
3102 #If the ticket has an owner and it's the new owner, we don't need
3104 elsif ( ( $self->OwnerObj )
3105 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
3106 return ( 0, $self->loc("That user already owns that ticket") );
3109 $RT::Handle->BeginTransaction();
3111 # Delete the owner in the owner group, then add a new one
3112 # TODO: is this safe? it's not how we really want the API to work
3113 # for most things, but it's fast.
3114 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3116 $RT::Handle->Rollback();
3117 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3120 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3121 PrincipalId => $NewOwnerObj->PrincipalId,
3122 InsideTransaction => 1 );
3124 $RT::Handle->Rollback();
3125 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3128 # We call set twice with slightly different arguments, so
3129 # as to not have an SQL transaction span two RT transactions
3131 my ( $val, $msg ) = $self->_Set(
3133 RecordTransaction => 0,
3134 Value => $NewOwnerObj->Id,
3136 TransactionType => $Type,
3137 CheckACL => 0, # don't check acl
3141 $RT::Handle->Rollback;
3142 return ( 0, $self->loc("Could not change owner. ") . $msg );
3145 $RT::Handle->Commit();
3147 my ( $trans, $msg, undef ) = $self->_NewTransaction(
3150 NewValue => $NewOwnerObj->Id,
3151 OldValue => $OldOwnerObj->Id,
3155 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3156 $OldOwnerObj->Name, $NewOwnerObj->Name );
3158 # TODO: make sure the trans committed properly
3160 return ( $trans, $msg );
3170 A convenince method to set the ticket's owner to the current user
3176 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3185 Convenience method to set the owner to 'nobody' if the current user is the owner.
3191 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3200 A convenience method to change the owner of the current ticket to the
3201 current user. Even if it's owned by another user.
3208 if ( $self->IsOwner( $self->CurrentUser ) ) {
3209 return ( 0, $self->loc("You already own this ticket") );
3212 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3222 # {{{ Routines dealing with status
3224 # {{{ sub ValidateStatus
3226 =head2 ValidateStatus STATUS
3228 Takes a string. Returns true if that status is a valid status for this ticket.
3229 Returns false otherwise.
3233 sub ValidateStatus {
3237 #Make sure the status passed in is valid
3238 unless ( $self->QueueObj->IsValidStatus($status) ) {
3250 =head2 SetStatus STATUS
3252 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3254 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.
3258 my $tt = RT::Ticket->new($RT::SystemUser);
3259 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3262 ok($tt->Status eq 'new', "New ticket is created as new");
3264 ($id, $msg) = $tt->SetStatus('open');
3266 ok ($msg =~ /open/i, "Status message is correct");
3267 ($id, $msg) = $tt->SetStatus('resolved');
3269 ok ($msg =~ /resolved/i, "Status message is correct");
3270 ($id, $msg) = $tt->SetStatus('resolved');
3284 $args{Status} = shift;
3291 if ( $args{Status} eq 'deleted') {
3292 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3293 return ( 0, $self->loc('Permission Denied') );
3296 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3297 return ( 0, $self->loc('Permission Denied') );
3301 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3302 return (0, $self->loc('That ticket has unresolved dependencies'));
3305 my $now = RT::Date->new( $self->CurrentUser );
3308 #If we're changing the status from new, record that we've started
3309 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3311 #Set the Started time to "now"
3312 $self->_Set( Field => 'Started',
3314 RecordTransaction => 0 );
3317 if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) {
3319 #When we resolve a ticket, set the 'Resolved' attribute to now.
3320 $self->_Set( Field => 'Resolved',
3322 RecordTransaction => 0 );
3325 #Actually update the status
3326 my ($val, $msg)= $self->_Set( Field => 'Status',
3327 Value => $args{Status},
3329 TransactionType => 'Status' );
3340 Takes no arguments. Marks this ticket for garbage collection
3346 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead.");
3347 return $self->Delete;
3352 return ( $self->SetStatus('deleted') );
3354 # TODO: garbage collection
3363 Sets this ticket's status to stalled
3369 return ( $self->SetStatus('stalled') );
3378 Sets this ticket's status to rejected
3384 return ( $self->SetStatus('rejected') );
3393 Sets this ticket\'s status to Open
3399 return ( $self->SetStatus('open') );
3408 Sets this ticket\'s status to Resolved
3414 return ( $self->SetStatus('resolved') );
3421 # {{{ Routines dealing with custom fields
3424 # {{{ FirstCustomFieldValue
3426 =item FirstCustomFieldValue FIELD
3428 Return the content of the first value of CustomField FIELD for this ticket
3429 Takes a field id or name
3433 sub FirstCustomFieldValue {
3436 my $values = $self->CustomFieldValues($field);
3437 if ($values->First) {
3438 return $values->First->Content;
3447 # {{{ CustomFieldValues
3449 =item CustomFieldValues FIELD
3451 Return a TicketCustomFieldValues object of all values of CustomField FIELD for this ticket.
3452 Takes a field id or name.
3457 sub CustomFieldValues {
3461 my $cf = RT::CustomField->new($self->CurrentUser);
3463 if ($field =~ /^\d+$/) {
3464 $cf->LoadById($field);
3466 $cf->LoadByNameAndQueue(Name => $field, Queue => $self->QueueObj->Id);
3468 $cf->LoadByNameAndQueue(Name => $field, Queue => '0');
3471 my $cf_values = RT::TicketCustomFieldValues->new( $self->CurrentUser );
3472 $cf_values->LimitToCustomField($cf->id);
3473 $cf_values->LimitToTicket($self->Id());
3474 $cf_values->OrderBy( FIELD => 'id' );
3476 # @values is a CustomFieldValues object;
3477 return ($cf_values);
3482 # {{{ AddCustomFieldValue
3484 =item AddCustomFieldValue { Field => FIELD, Value => VALUE }
3486 VALUE should be a string.
3487 FIELD can be a CustomField object OR a CustomField ID.
3490 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
3491 deletes the old value.
3492 If VALUE isn't a valid value for the custom field, returns
3493 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3497 sub AddCustomFieldValue {
3499 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3500 return ( 0, $self->loc("Permission Denied") );
3502 $self->_AddCustomFieldValue(@_);
3505 sub _AddCustomFieldValue {
3510 RecordTransaction => 1,
3514 my $cf = RT::CustomField->new( $self->CurrentUser );
3515 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3516 $cf->Load( $args{'Field'}->id );
3519 $cf->Load( $args{'Field'} );
3522 unless ( $cf->Id ) {
3523 return ( 0, $self->loc("Custom field [_1] not found", $args{'Field'}) );
3526 # Load up a TicketCustomFieldValues object for this custom field and this ticket
3527 my $values = $cf->ValuesForTicket( $self->id );
3529 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
3530 return ( 0, $self->loc("Invalid value for custom field") );
3533 # If the custom field only accepts a single value, delete the existing
3534 # value and record a "changed from foo to bar" transaction
3535 if ( $cf->SingleValue ) {
3537 # We need to whack any old values here. In most cases, the custom field should
3538 # only have one value to delete. In the pathalogical case, this custom field
3539 # used to be a multiple and we have many values to whack....
3540 my $cf_values = $values->Count;
3542 if ( $cf_values > 1 ) {
3543 my $i = 0; #We want to delete all but the last one, so we can then
3544 # execute the same code to "change" the value from old to new
3545 while ( my $value = $values->Next ) {
3547 if ( $i < $cf_values ) {
3548 my $old_value = $value->Content;
3549 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $value->Content);
3553 my ( $TransactionId, $Msg, $TransactionObj ) =
3554 $self->_NewTransaction(
3555 Type => 'CustomField',
3557 OldValue => $old_value
3564 if (my $value = $cf->ValuesForTicket( $self->Id )->First) {
3565 $old_value = $value->Content();
3566 return (1) if $old_value eq $args{'Value'};
3569 my ( $new_value_id, $value_msg ) = $cf->AddValueForTicket(
3570 Ticket => $self->Id,
3571 Content => $args{'Value'}
3574 unless ($new_value_id) {
3576 $self->loc("Could not add new custom field value for ticket. [_1] ",
3580 my $new_value = RT::TicketCustomFieldValue->new( $self->CurrentUser );
3581 $new_value->Load($new_value_id);
3583 # now that adding the new value was successful, delete the old one
3585 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $old_value);
3591 if ($args{'RecordTransaction'}) {
3592 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3593 Type => 'CustomField',
3595 OldValue => $old_value,
3596 NewValue => $new_value->Content
3600 if ( $old_value eq '' ) {
3601 return ( 1, $self->loc("[_1] [_2] added", $cf->Name, $new_value->Content) );
3603 elsif ( $new_value->Content eq '' ) {
3604 return ( 1, $self->loc("[_1] [_2] deleted", $cf->Name, $old_value) );
3607 return ( 1, $self->loc("[_1] [_2] changed to [_3]", $cf->Name, $old_value, $new_value->Content ) );
3612 # otherwise, just add a new value and record "new value added"
3614 my ( $new_value_id ) = $cf->AddValueForTicket(
3615 Ticket => $self->Id,
3616 Content => $args{'Value'}
3619 unless ($new_value_id) {
3621 $self->loc("Could not add new custom field value for ticket. "));
3623 if ( $args{'RecordTransaction'} ) {
3624 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3625 Type => 'CustomField',
3627 NewValue => $args{'Value'}
3629 unless ($TransactionId) {
3631 $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
3634 return ( 1, $self->loc("[_1] added as a value for [_2]",$args{'Value'}, $cf->Name));
3641 # {{{ DeleteCustomFieldValue
3643 =item DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
3645 Deletes VALUE as a value of CustomField FIELD.
3647 VALUE can be a string, a CustomFieldValue or a TicketCustomFieldValue.
3649 If VALUE isn't a valid value for the custom field, returns
3650 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3654 sub DeleteCustomFieldValue {
3661 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3662 return ( 0, $self->loc("Permission Denied") );
3664 my $cf = RT::CustomField->new( $self->CurrentUser );
3665 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3666 $cf->LoadById( $args{'Field'}->id );
3669 $cf->LoadById( $args{'Field'} );
3672 unless ( $cf->Id ) {
3673 return ( 0, $self->loc("Custom field not found") );
3677 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $args{'Value'});
3681 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3682 Type => 'CustomField',
3684 OldValue => $args{'Value'}
3686 unless($TransactionId) {
3687 return(0, $self->loc("Couldn't create a transaction: [_1]", $Msg));
3690 return($TransactionId, $self->loc("[_1] is no longer a value for custom field [_2]", $args{'Value'}, $cf->Name));
3697 # {{{ Actions + Routines dealing with transactions
3699 # {{{ sub SetTold and _SetTold
3701 =head2 SetTold ISO [TIMETAKEN]
3703 Updates the told and records a transaction
3710 $told = shift if (@_);
3711 my $timetaken = shift || 0;
3713 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3714 return ( 0, $self->loc("Permission Denied") );
3717 my $datetold = new RT::Date( $self->CurrentUser );
3719 $datetold->Set( Format => 'iso',
3723 $datetold->SetToNow();
3726 return ( $self->_Set( Field => 'Told',
3727 Value => $datetold->ISO,
3728 TimeTaken => $timetaken,
3729 TransactionType => 'Told' ) );
3734 Updates the told without a transaction or acl check. Useful when we're sending replies.
3741 my $now = new RT::Date( $self->CurrentUser );
3744 #use __Set to get no ACLs ;)
3745 return ( $self->__Set( Field => 'Told',
3746 Value => $now->ISO ) );
3751 # {{{ sub Transactions
3755 Returns an RT::Transactions object of all transactions on this ticket
3762 use RT::Transactions;
3763 my $transactions = RT::Transactions->new( $self->CurrentUser );
3765 #If the user has no rights, return an empty object
3766 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3767 my $tickets = $transactions->NewAlias('Tickets');
3768 $transactions->Join(
3774 $transactions->Limit(
3776 FIELD => 'EffectiveId',
3777 VALUE => $self->id()
3780 # if the user may not see comments do not return them
3781 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3782 $transactions->Limit(
3790 return ($transactions);
3795 # {{{ sub _NewTransaction
3797 sub _NewTransaction {
3810 require RT::Transaction;
3811 my $trans = new RT::Transaction( $self->CurrentUser );
3812 my ( $transaction, $msg ) = $trans->Create(
3813 Ticket => $self->Id,
3814 TimeTaken => $args{'TimeTaken'},
3815 Type => $args{'Type'},
3816 Data => $args{'Data'},
3817 Field => $args{'Field'},
3818 NewValue => $args{'NewValue'},
3819 OldValue => $args{'OldValue'},
3820 MIMEObj => $args{'MIMEObj'}
3824 $self->Load($self->Id);
3826 $RT::Logger->warning($msg) unless $transaction;
3828 $self->_SetLastUpdated;
3830 if ( defined $args{'TimeTaken'} ) {
3831 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
3833 if ( $RT::UseTransactionBatch and $transaction ) {
3834 push @{$self->{_TransactionBatch}}, $trans;
3836 return ( $transaction, $msg, $trans );
3841 =head2 TransactionBatch
3843 Returns an array reference of all transactions created on this ticket during
3844 this ticket object's lifetime, or undef if there were none.
3846 Only works when the $RT::UseTransactionBatch config variable is set to true.
3850 sub TransactionBatch {
3852 return $self->{_TransactionBatch};
3858 # The following line eliminates reentrancy.
3859 # It protects against the fact that perl doesn't deal gracefully
3860 # when an object's refcount is changed in its destructor.
3861 return if $self->{_Destroyed}++;
3863 my $batch = $self->TransactionBatch or return;
3865 RT::Scrips->new($RT::SystemUser)->Apply(
3866 Stage => 'TransactionBatch',
3868 TransactionObj => $batch->[0],
3874 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3876 # {{{ sub _ClassAccessible
3878 sub _ClassAccessible {
3880 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3881 Queue => { 'read' => 1, 'write' => 1 },
3882 Requestors => { 'read' => 1, 'write' => 1 },
3883 Owner => { 'read' => 1, 'write' => 1 },
3884 Subject => { 'read' => 1, 'write' => 1 },
3885 InitialPriority => { 'read' => 1, 'write' => 1 },
3886 FinalPriority => { 'read' => 1, 'write' => 1 },
3887 Priority => { 'read' => 1, 'write' => 1 },
3888 Status => { 'read' => 1, 'write' => 1 },
3889 TimeEstimated => { 'read' => 1, 'write' => 1 },
3890 TimeWorked => { 'read' => 1, 'write' => 1 },
3891 TimeLeft => { 'read' => 1, 'write' => 1 },
3892 Created => { 'read' => 1, 'auto' => 1 },
3893 Creator => { 'read' => 1, 'auto' => 1 },
3894 Told => { 'read' => 1, 'write' => 1 },
3895 Resolved => { 'read' => 1 },
3896 Type => { 'read' => 1 },
3897 Starts => { 'read' => 1, 'write' => 1 },
3898 Started => { 'read' => 1, 'write' => 1 },
3899 Due => { 'read' => 1, 'write' => 1 },
3900 Creator => { 'read' => 1, 'auto' => 1 },
3901 Created => { 'read' => 1, 'auto' => 1 },
3902 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3903 LastUpdated => { 'read' => 1, 'auto' => 1 }
3915 my %args = ( Field => undef,
3918 RecordTransaction => 1,
3921 TransactionType => 'Set',
3924 if ($args{'CheckACL'}) {
3925 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3926 return ( 0, $self->loc("Permission Denied"));
3930 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3931 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3932 return(0, $self->loc("Internal Error"));
3935 #if the user is trying to modify the record
3937 #Take care of the old value we really don't want to get in an ACL loop.
3938 # so ask the super::_Value
3939 my $Old = $self->SUPER::_Value("$args{'Field'}");
3942 if ( $args{'UpdateTicket'} ) {
3945 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3946 Value => $args{'Value'} );
3948 #If we can't actually set the field to the value, don't record
3949 # a transaction. instead, get out of here.
3950 if ( $ret == 0 ) { return ( 0, $msg ); }
3953 if ( $args{'RecordTransaction'} == 1 ) {
3955 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3956 Type => $args{'TransactionType'},
3957 Field => $args{'Field'},
3958 NewValue => $args{'Value'},
3960 TimeTaken => $args{'TimeTaken'},
3962 return ( $Trans, scalar $TransObj->Description );
3965 return ( $ret, $msg );
3975 Takes the name of a table column.
3976 Returns its value as a string, if the user passes an ACL check
3985 #if the field is public, return it.
3986 if ( $self->_Accessible( $field, 'public' ) ) {
3988 #$RT::Logger->debug("Skipping ACL check for $field\n");
3989 return ( $self->SUPER::_Value($field) );
3993 #If the current user doesn't have ACLs, don't let em at it.
3995 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3998 return ( $self->SUPER::_Value($field) );
4004 # {{{ sub _UpdateTimeTaken
4006 =head2 _UpdateTimeTaken
4008 This routine will increment the timeworked counter. it should
4009 only be called from _NewTransaction
4013 sub _UpdateTimeTaken {
4015 my $Minutes = shift;
4018 $Total = $self->SUPER::_Value("TimeWorked");
4019 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
4021 Field => "TimeWorked",
4032 # {{{ Routines dealing with ACCESS CONTROL
4034 # {{{ sub CurrentUserHasRight
4036 =head2 CurrentUserHasRight
4038 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
4039 1 if the user has that right. It returns 0 if the user doesn't have that right.
4043 sub CurrentUserHasRight {
4049 Principal => $self->CurrentUser->UserObj(),
4062 Takes a paramhash with the attributes 'Right' and 'Principal'
4063 'Right' is a ticket-scoped textual right from RT::ACE
4064 'Principal' is an RT::User object
4066 Returns 1 if the principal has the right. Returns undef if not.
4078 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
4080 $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
4084 $args{'Principal'}->HasRight(
4086 Right => $args{'Right'}
4099 Jesse Vincent, jesse@bestpractical.com