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,
315 InitialPriority => undef,
316 FinalPriority => undef,
326 _RecordTransaction => 1,
332 my ( $ErrStr, $Owner, $resolved );
333 my (@non_fatal_errors);
335 my $QueueObj = RT::Queue->new($RT::SystemUser);
338 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
339 $QueueObj->Load( $args{'Queue'} );
341 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
342 $QueueObj->Load( $args{'Queue'}->Id );
345 $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object.");
349 #Can't create a ticket without a queue.
350 unless ( defined($QueueObj) && $QueueObj->Id ) {
351 $RT::Logger->debug("$self No queue given for ticket creation.");
352 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
355 #Now that we have a queue, Check the ACLS
356 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket',
357 Object => $QueueObj )
360 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name ) );
363 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
364 return ( 0, 0, $self->loc('Invalid value for status') );
368 #Since we have a queue, we can set queue defaults
371 # If there's no queue default initial priority and it's not set, set it to 0
372 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
373 unless ( defined $args{'InitialPriority'} );
377 # If there's no queue default final priority and it's not set, set it to 0
378 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
379 unless ( defined $args{'FinalPriority'} );
382 #TODO we should see what sort of due date we're getting, rather +
383 # than assuming it's in ISO format.
385 #Set the due date. if we didn't get fed one, use the queue default due in
386 my $Due = new RT::Date( $self->CurrentUser );
388 if ( $args{'Due'} ) {
389 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
391 elsif ( $QueueObj->DefaultDueIn ) {
393 $Due->AddDays( $QueueObj->DefaultDueIn );
396 my $Starts = new RT::Date( $self->CurrentUser );
397 if ( defined $args{'Starts'} ) {
398 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
401 my $Started = new RT::Date( $self->CurrentUser );
402 if ( defined $args{'Started'} ) {
403 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
406 my $Resolved = new RT::Date( $self->CurrentUser );
407 if ( defined $args{'Resolved'} ) {
408 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
412 #If the status is an inactive status, set the resolved date
413 if ($QueueObj->IsInactiveStatus($args{'Status'}) && !$args{'Resolved'}) {
414 $RT::Logger->debug("Got a ".$args{'Status'} . "ticket with a resolved of ".$args{'Resolved'});
420 # {{{ Dealing with time fields
422 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
423 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
424 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
428 # {{{ Deal with setting the owner
430 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
431 $Owner = $args{'Owner'};
434 #If we've been handed something else, try to load the user.
435 elsif ( defined $args{'Owner'} ) {
436 $Owner = RT::User->new( $self->CurrentUser );
437 $Owner->Load( $args{'Owner'} );
441 #If we have a proposed owner and they don't have the right
442 #to own a ticket, scream about it and make them not the owner
443 if ( ( defined($Owner) )
445 and ( $Owner->Id != $RT::Nobody->Id )
446 and ( !$Owner->HasRight( Object => $QueueObj,
447 Right => 'OwnTicket' ) )
450 $RT::Logger->warning( "User "
454 . "as a ticket owner but has no rights to own "
455 . "tickets in ".$QueueObj->Name );
457 push @non_fatal_errors, $self->loc("Invalid owner. Defaulting to 'nobody'.");
462 #If we haven't been handed a valid owner, make it nobody.
463 unless ( defined($Owner) && $Owner->Id ) {
464 $Owner = new RT::User( $self->CurrentUser );
465 $Owner->Load( $RT::Nobody->Id );
470 # We attempt to load or create each of the people who might have a role for this ticket
471 # _outside_ the transaction, so we don't get into ticket creation races
472 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
473 next unless (defined $args{$type});
474 foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
475 my $user = RT::User->new($RT::SystemUser);
476 $user->LoadOrCreateByEmail($watcher) if ($watcher !~ /^\d+$/);
481 $RT::Handle->BeginTransaction();
483 my %params =( Queue => $QueueObj->Id,
485 Subject => $args{'Subject'},
486 InitialPriority => $args{'InitialPriority'},
487 FinalPriority => $args{'FinalPriority'},
488 Priority => $args{'InitialPriority'},
489 Status => $args{'Status'},
490 TimeWorked => $args{'TimeWorked'},
491 TimeEstimated => $args{'TimeEstimated'},
492 TimeLeft => $args{'TimeLeft'},
493 Type => $args{'Type'},
494 Starts => $Starts->ISO,
495 Started => $Started->ISO,
496 Resolved => $Resolved->ISO,
499 # Parameters passed in during an import that we probably don't want to touch, otherwise
500 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
501 $params{$attr} = $args{$attr} if ($args{$attr});
504 # Delete null integer parameters
505 foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
506 delete $params{$attr} unless (exists $params{$attr} && $params{$attr});
510 my $id = $self->SUPER::Create( %params);
512 $RT::Logger->crit( "Couldn't create a ticket");
513 $RT::Handle->Rollback();
514 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
517 #Set the ticket's effective ID now that we've created it.
518 my ( $val, $msg ) = $self->__Set( Field => 'EffectiveId', Value => $id );
521 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
522 $RT::Handle->Rollback();
523 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") );
526 my $create_groups_ret = $self->_CreateTicketGroups();
527 unless ($create_groups_ret) {
528 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
530 . ". aborting Ticket creation." );
531 $RT::Handle->Rollback();
533 $self->loc( "Ticket could not be created due to an internal error") );
536 # Set the owner in the Groups table
537 # We denormalize it into the Ticket table too because doing otherwise would
538 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
540 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId , InsideTransaction => 1);
542 # {{{ Deal with setting up watchers
545 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
546 next unless (defined $args{$type});
547 foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) {
549 # we reason that all-digits number must be a principal id, not email
550 # this is the only way to can add
552 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
556 if ( $type eq 'AdminCc' ) {
558 # Note that we're using AddWatcher, rather than _AddWatcher, as we
559 # actually _want_ that ACL check. Otherwise, random ticket creators
560 # could make themselves adminccs and maybe get ticket rights. that would
562 ( $wval, $wmsg ) = $self->AddWatcher( Type => $type,
567 ( $wval, $wmsg ) = $self->_AddWatcher( Type => $type,
572 push @non_fatal_errors, $wmsg unless ($wval);
577 # {{{ Deal with setting up links
580 foreach my $type ( keys %LINKTYPEMAP ) {
581 next unless (defined $args{$type});
583 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
585 my ( $wval, $wmsg ) = $self->AddLink(
586 Type => $LINKTYPEMAP{$type}->{'Type'},
587 $LINKTYPEMAP{$type}->{'Mode'} => $link,
591 push @non_fatal_errors, $wmsg unless ($wval);
597 # {{{ Add all the custom fields
599 foreach my $arg ( keys %args ) {
600 next unless ( $arg =~ /^CustomField-(\d+)$/i );
603 my $value ( ref( $args{$arg} ) ? @{ $args{$arg} } : ( $args{$arg} ) ) {
604 next unless ($value);
605 $self->_AddCustomFieldValue( Field => $cfid,
607 RecordTransaction => 0
613 if ( $args{'_RecordTransaction'} ) {
614 # {{{ Add a transaction for the create
615 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
618 MIMEObj => $args{'MIMEObj'}
622 if ( $self->Id && $Trans ) {
623 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
624 $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
626 $RT::Logger->info("Ticket ".$self->Id. " created in queue '".$QueueObj->Name."' by ".$self->CurrentUser->Name);
629 $RT::Handle->Rollback();
631 # TODO where does this get errstr from?
632 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
633 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
636 $RT::Handle->Commit();
637 return ( $self->Id, $TransObj->Id, $ErrStr );
642 # Not going to record a transaction
643 $RT::Handle->Commit();
644 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
645 $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors );
646 return ( $self->Id, $0, $ErrStr );
654 # {{{ sub CreateFromEmailMessage
657 =head2 CreateFromEmailMessage { Message, Queue, ExtractActorFromHeaders }
659 This code replaces what was once a large part of the email gateway.
660 It takes an email message as a parameter, parses out the sender, subject
661 and a MIME object. It then creates a ticket based on those attributes
665 sub CreateFromEmailMessage {
667 my %args = ( Message => undef,
669 ExtractActorFromSender => undef,
689 CreateTickets uses the template as a template for an ordered set of tickets
690 to create. The basic format is as follows:
693 ===Create-Ticket: identifier
701 =head2 Acceptable fields
703 A complete list of acceptable fields for this beastie:
706 * Queue => Name or id# of a queue
707 Subject => A text string
708 Status => A valid status. defaults to 'new'
710 Due => Dates can be specified in seconds since the epoch
711 to be handled literally or in a semi-free textual
712 format which RT will attempt to parse.
716 Owner => Username or id of an RT user who can and should own
718 + Requestor => Email address
719 + Cc => Email address
720 + AdminCc => Email address
733 Content => content. Can extend to multiple lines. Everything
734 within a template after a Content: header is treated
735 as content until we hit a line containing only
737 ContentType => the content-type of the Content field
738 CustomField-<id#> => custom field value
740 Fields marked with an * are required.
742 Fields marked with a + man have multiple values, simply
743 by repeating the fieldname on a new line with an additional value.
746 When parsed, field names are converted to lowercase and have -s stripped.
747 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
748 be treated as the same thing.
766 my %args = $self->_Parse822HeadersForAttributes($content);
768 # Now we have a %args to work with.
769 # Make sure we have at least the minimum set of
770 # reasonable data and do our thang
771 my $ticket = RT::Ticket->new($RT::SystemUser);
774 Queue => $args{'queue'},
775 Subject => $args{'subject'},
776 Status => $args{'status'},
778 Starts => $args{'starts'},
779 Started => $args{'started'},
780 Resolved => $args{'resolved'},
781 Owner => $args{'owner'},
782 Requestor => $args{'requestor'},
784 AdminCc => $args{'admincc'},
785 TimeWorked => $args{'timeworked'},
786 TimeEstimated => $args{'timeestimated'},
787 TimeLeft => $args{'timeleft'},
788 InitialPriority => $args{'initialpriority'},
789 FinalPriority => $args{'finalpriority'},
790 Type => $args{'type'},
791 DependsOn => $args{'dependson'},
792 DependedOnBy => $args{'dependedonby'},
793 RefersTo => $args{'refersto'},
794 ReferredToBy => $args{'referredtoby'},
795 Members => $args{'members'},
796 MemberOf => $args{'memberof'},
797 MIMEObj => $args{'mimeobj'}
800 # Add custom field entries to %ticketargs.
801 # TODO: allow named custom fields
803 /^customfield-(\d+)$/
804 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
807 my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs);
809 $RT::Logger->error( "Couldn't create a related ticket for "
810 . $self->TicketObj->Id . " "
821 =head2 UpdateFrom822 $MESSAGE
823 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
824 Returns an um. ask me again when the code exists
829 my $simple_update = <<EOF;
831 AddRequestor: jesse\@example.com
834 my $ticket = RT::Ticket->new($RT::SystemUser);
835 $ticket->Create(Subject => 'first', Queue => 'general');
836 ok($ticket->Id, "Created the test ticket");
837 $ticket->UpdateFrom822($simple_update);
838 is($ticket->Subject, 'target', "changed the subject");
839 my $jesse = RT::User->new($RT::SystemUser);
840 $jesse->LoadByEmail('jesse@example.com');
841 ok ($jesse->Id, "There's a user for jesse");
842 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
852 my %args = $self->_Parse822HeadersForAttributes($content);
856 Queue => $args{'queue'},
857 Subject => $args{'subject'},
858 Status => $args{'status'},
860 Starts => $args{'starts'},
861 Started => $args{'started'},
862 Resolved => $args{'resolved'},
863 Owner => $args{'owner'},
864 Requestor => $args{'requestor'},
866 AdminCc => $args{'admincc'},
867 TimeWorked => $args{'timeworked'},
868 TimeEstimated => $args{'timeestimated'},
869 TimeLeft => $args{'timeleft'},
870 InitialPriority => $args{'initialpriority'},
871 Priority => $args{'priority'},
872 FinalPriority => $args{'finalpriority'},
873 Type => $args{'type'},
874 DependsOn => $args{'dependson'},
875 DependedOnBy => $args{'dependedonby'},
876 RefersTo => $args{'refersto'},
877 ReferredToBy => $args{'referredtoby'},
878 Members => $args{'members'},
879 MemberOf => $args{'memberof'},
880 MIMEObj => $args{'mimeobj'}
883 foreach my $type qw(Requestor Cc Admincc) {
885 foreach my $action ( 'Add', 'Del', '' ) {
887 my $lctag = lc($action) . lc($type);
888 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
890 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
891 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
896 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
901 # Add custom field entries to %ticketargs.
902 # TODO: allow named custom fields
904 /^customfield-(\d+)$/
905 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
908 # for each ticket we've been told to update, iterate through the set of
909 # rfc822 headers and perform that update to the ticket.
912 # {{{ Set basic fields
926 # Resolve the queue from a name to a numeric id.
927 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
928 my $tempqueue = RT::Queue->new($RT::SystemUser);
929 $tempqueue->Load( $ticketargs{'Queue'} );
930 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
933 # die "updaterecordobject is a webui thingy";
936 foreach my $attribute (@attribs) {
937 my $value = $ticketargs{$attribute};
939 if ( $value ne $self->$attribute() ) {
941 my $method = "Set$attribute";
942 my ( $code, $msg ) = $self->$method($value);
944 push @results, $self->loc($attribute) . ': ' . $msg;
949 # We special case owner changing, so we can use ForceOwnerChange
950 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
951 my $ChownType = "Give";
952 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
954 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
955 push ( @results, $msg );
959 # Deal with setting watchers
962 # Acceptable arguments:
969 foreach my $type qw(Requestor Cc AdminCc) {
971 # If we've been given a number of delresses to del, do it.
972 foreach my $address (@{$ticketargs{'Del'.$type}}) {
973 my ($id, $msg) = $self->DelWatcher( Type => $type, Email => $address);
974 push (@results, $msg) ;
977 # If we've been given a number of addresses to add, do it.
978 foreach my $address (@{$ticketargs{'Add'.$type}}) {
979 $RT::Logger->debug("Adding $address as a $type");
980 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
981 push (@results, $msg) ;
992 # {{{ _Parse822HeadersForAttributes Content
994 =head2 _Parse822HeadersForAttributes Content
996 Takes an RFC822 style message and parses its attributes into a hash.
1000 sub _Parse822HeadersForAttributes {
1002 my $content = shift;
1005 my @lines = ( split ( /\n/, $content ) );
1006 while ( defined( my $line = shift @lines ) ) {
1007 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1012 if ( defined( $args{$tag} ) )
1013 { #if we're about to get a second value, make it an array
1014 $args{$tag} = [ $args{$tag} ];
1016 if ( ref( $args{$tag} ) )
1017 { #If it's an array, we want to push the value
1018 push @{ $args{$tag} }, $value;
1020 else { #if there's nothing there, just set the value
1021 $args{$tag} = $value;
1023 } elsif ($line =~ /^$/) {
1025 #TODO: this won't work, since "" isn't of the form "foo:value"
1027 while ( defined( my $l = shift @lines ) ) {
1028 push @{ $args{'content'} }, $l;
1034 foreach my $date qw(due starts started resolved) {
1035 my $dateobj = RT::Date->new($RT::SystemUser);
1036 if ( $args{$date} =~ /^\d+$/ ) {
1037 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1040 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1042 $args{$date} = $dateobj->ISO;
1044 $args{'mimeobj'} = MIME::Entity->new();
1045 $args{'mimeobj'}->build(
1046 Type => ( $args{'contenttype'} || 'text/plain' ),
1047 Data => ($args{'content'} || '')
1057 =head2 Import PARAMHASH
1060 Doesn\'t create a transaction.
1061 Doesn\'t supply queue defaults, etc.
1069 my ( $ErrStr, $QueueObj, $Owner );
1073 EffectiveId => undef,
1077 Owner => $RT::Nobody->Id,
1078 Subject => '[no subject]',
1079 InitialPriority => undef,
1080 FinalPriority => undef,
1091 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1092 $QueueObj = RT::Queue->new($RT::SystemUser);
1093 $QueueObj->Load( $args{'Queue'} );
1095 #TODO error check this and return 0 if it\'s not loading properly +++
1097 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1098 $QueueObj = RT::Queue->new($RT::SystemUser);
1099 $QueueObj->Load( $args{'Queue'}->Id );
1103 "$self " . $args{'Queue'} . " not a recognised queue object." );
1106 #Can't create a ticket without a queue.
1107 unless ( defined($QueueObj) and $QueueObj->Id ) {
1108 $RT::Logger->debug("$self No queue given for ticket creation.");
1109 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1112 #Now that we have a queue, Check the ACLS
1114 $self->CurrentUser->HasRight(
1115 Right => 'CreateTicket',
1121 $self->loc("No permission to create tickets in the queue '[_1]'"
1122 , $QueueObj->Name));
1125 # {{{ Deal with setting the owner
1127 # Attempt to take user object, user name or user id.
1128 # Assign to nobody if lookup fails.
1129 if ( defined( $args{'Owner'} ) ) {
1130 if ( ref( $args{'Owner'} ) ) {
1131 $Owner = $args{'Owner'};
1134 $Owner = new RT::User( $self->CurrentUser );
1135 $Owner->Load( $args{'Owner'} );
1136 if ( !defined( $Owner->id ) ) {
1137 $Owner->Load( $RT::Nobody->id );
1142 #If we have a proposed owner and they don't have the right
1143 #to own a ticket, scream about it and make them not the owner
1146 and ( $Owner->Id != $RT::Nobody->Id )
1149 Object => $QueueObj,
1150 Right => 'OwnTicket'
1156 $RT::Logger->warning( "$self user "
1157 . $Owner->Name . "("
1160 . "as a ticket owner but has no rights to own "
1162 . $QueueObj->Name . "'\n" );
1167 #If we haven't been handed a valid owner, make it nobody.
1168 unless ( defined($Owner) ) {
1169 $Owner = new RT::User( $self->CurrentUser );
1170 $Owner->Load( $RT::Nobody->UserObj->Id );
1175 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1176 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1179 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1180 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1181 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1182 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1184 # If we're coming in with an id, set that now.
1185 my $EffectiveId = undef;
1186 if ( $args{'id'} ) {
1187 $EffectiveId = $args{'id'};
1191 my $id = $self->SUPER::Create(
1193 EffectiveId => $EffectiveId,
1194 Queue => $QueueObj->Id,
1195 Owner => $Owner->Id,
1196 Subject => $args{'Subject'}, # loc
1197 InitialPriority => $args{'InitialPriority'}, # loc
1198 FinalPriority => $args{'FinalPriority'}, # loc
1199 Priority => $args{'InitialPriority'}, # loc
1200 Status => $args{'Status'}, # loc
1201 TimeWorked => $args{'TimeWorked'}, # loc
1202 Type => $args{'Type'}, # loc
1203 Created => $args{'Created'}, # loc
1204 Told => $args{'Told'}, # loc
1205 LastUpdated => $args{'Updated'}, # loc
1206 Resolved => $args{'Resolved'}, # loc
1207 Due => $args{'Due'}, # loc
1210 # If the ticket didn't have an id
1211 # Set the ticket's effective ID now that we've created it.
1212 if ( $args{'id'} ) {
1213 $self->Load( $args{'id'} );
1217 $self->__Set( Field => 'EffectiveId', Value => $id );
1221 $self . "->Import couldn't set EffectiveId: $msg\n" );
1226 foreach $watcher ( @{ $args{'Cc'} } ) {
1227 $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 );
1229 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1230 $self->_AddWatcher( Type => 'AdminCc', Person => $watcher,
1233 foreach $watcher ( @{ $args{'Requestor'} } ) {
1234 $self->_AddWatcher( Type => 'Requestor', Person => $watcher,
1238 return ( $self->Id, $ErrStr );
1244 # {{{ Routines dealing with watchers.
1246 # {{{ _CreateTicketGroups
1248 =head2 _CreateTicketGroups
1250 Create the ticket groups and relationships for this ticket.
1251 This routine expects to be called from Ticket->Create _inside of a transaction_
1253 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1255 It will return true on success and undef on failure.
1259 my $ticket = RT::Ticket->new($RT::SystemUser);
1260 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1261 Owner => $RT::SystemUser->Id,
1263 Requestor => ['jesse@example.com'],
1266 ok ($id, "Ticket $id was created");
1267 ok(my $group = RT::Group->new($RT::SystemUser));
1268 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1269 ok ($group->Id, "Found the requestors object for this ticket");
1271 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1272 $jesse->LoadByEmail('jesse@example.com');
1273 ok($jesse->Id, "Found the jesse rt user");
1276 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1277 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1278 ok ($add_id, "Add succeeded: ($add_msg)");
1279 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1280 $bob->LoadByEmail('bob@fsck.com');
1281 ok($bob->Id, "Found the bob rt user");
1282 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1283 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1284 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1287 $group = RT::Group->new($RT::SystemUser);
1288 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1289 ok ($group->Id, "Found the cc object for this ticket");
1290 $group = RT::Group->new($RT::SystemUser);
1291 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1292 ok ($group->Id, "Found the AdminCc object for this ticket");
1293 $group = RT::Group->new($RT::SystemUser);
1294 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1295 ok ($group->Id, "Found the Owner object for this ticket");
1296 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1303 sub _CreateTicketGroups {
1306 my @types = qw(Requestor Owner Cc AdminCc);
1308 foreach my $type (@types) {
1309 my $type_obj = RT::Group->new($self->CurrentUser);
1310 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1311 Instance => $self->Id,
1314 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1315 $self->Id.": ".$msg);
1325 # {{{ sub OwnerGroup
1329 A constructor which returns an RT::Group object containing the owner of this ticket.
1335 my $owner_obj = RT::Group->new($self->CurrentUser);
1336 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1337 return ($owner_obj);
1343 # {{{ sub AddWatcher
1347 AddWatcher takes a parameter hash. The keys are as follows:
1349 Type One of Requestor, Cc, AdminCc
1351 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1353 Email The email address of the new watcher. If a user with this
1354 email address can't be found, a new nonprivileged user will be created.
1356 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.
1364 PrincipalId => undef,
1370 #If the watcher we're trying to add is for the current user
1371 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1372 # If it's an AdminCc and they don't have
1373 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1374 if ( $args{'Type'} eq 'AdminCc' ) {
1375 unless ( $self->CurrentUserHasRight('ModifyTicket')
1376 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1377 return ( 0, $self->loc('Permission Denied'))
1381 # If it's a Requestor or Cc and they don't have
1382 # 'Watch' or 'ModifyTicket', bail
1383 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1385 unless ( $self->CurrentUserHasRight('ModifyTicket')
1386 or $self->CurrentUserHasRight('Watch') ) {
1387 return ( 0, $self->loc('Permission Denied'))
1391 $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type");
1392 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1396 # If the watcher isn't the current user
1397 # and the current user doesn't have 'ModifyTicket'
1400 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1401 return ( 0, $self->loc("Permission Denied") );
1407 return ( $self->_AddWatcher(%args) );
1410 #This contains the meat of AddWatcher. but can be called from a routine like
1411 # Create, which doesn't need the additional acl check
1417 PrincipalId => undef,
1423 my $principal = RT::Principal->new($self->CurrentUser);
1424 if ($args{'Email'}) {
1425 my $user = RT::User->new($RT::SystemUser);
1426 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1428 $args{'PrincipalId'} = $pid;
1431 if ($args{'PrincipalId'}) {
1432 $principal->Load($args{'PrincipalId'});
1436 # If we can't find this watcher, we need to bail.
1437 unless ($principal->Id) {
1438 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1439 return(0, $self->loc("Could not find or create that user"));
1443 my $group = RT::Group->new($self->CurrentUser);
1444 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1445 unless ($group->id) {
1446 return(0,$self->loc("Group not found"));
1449 if ( $group->HasMember( $principal)) {
1451 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1455 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1456 InsideTransaction => 1 );
1458 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1460 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1463 unless ( $args{'Silent'} ) {
1464 $self->_NewTransaction(
1465 Type => 'AddWatcher',
1466 NewValue => $principal->Id,
1467 Field => $args{'Type'}
1471 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1477 # {{{ sub DeleteWatcher
1479 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1482 Deletes a Ticket watcher. Takes two arguments:
1484 Type (one of Requestor,Cc,AdminCc)
1488 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1490 Email (the email address of an existing wathcer)
1499 my %args = ( Type => undef,
1500 PrincipalId => undef,
1504 unless ($args{'PrincipalId'} || $args{'Email'} ) {
1505 return(0, $self->loc("No principal specified"));
1507 my $principal = RT::Principal->new($self->CurrentUser);
1508 if ($args{'PrincipalId'} ) {
1510 $principal->Load($args{'PrincipalId'});
1512 my $user = RT::User->new($self->CurrentUser);
1513 $user->LoadByEmail($args{'Email'});
1514 $principal->Load($user->Id);
1516 # If we can't find this watcher, we need to bail.
1517 unless ($principal->Id) {
1518 return(0, $self->loc("Could not find that principal"));
1521 my $group = RT::Group->new($self->CurrentUser);
1522 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1523 unless ($group->id) {
1524 return(0,$self->loc("Group not found"));
1528 #If the watcher we're trying to add is for the current user
1529 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1530 # If it's an AdminCc and they don't have
1531 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1532 if ( $args{'Type'} eq 'AdminCc' ) {
1533 unless ( $self->CurrentUserHasRight('ModifyTicket')
1534 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1535 return ( 0, $self->loc('Permission Denied'))
1539 # If it's a Requestor or Cc and they don't have
1540 # 'Watch' or 'ModifyTicket', bail
1541 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1542 unless ( $self->CurrentUserHasRight('ModifyTicket')
1543 or $self->CurrentUserHasRight('Watch') ) {
1544 return ( 0, $self->loc('Permission Denied'))
1548 $RT::Logger->warn( "$self -> DeleteWatcher got passed a bogus type");
1549 return ( 0, $self->loc('Error in parameters to Ticket->DelWatcher') );
1553 # If the watcher isn't the current user
1554 # and the current user doesn't have 'ModifyTicket' bail
1556 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1557 return ( 0, $self->loc("Permission Denied") );
1564 # see if this user is already a watcher.
1566 unless ( $group->HasMember($principal)) {
1568 $self->loc('That principal is not a [_1] for this ticket', $args{'Type'}) );
1571 my ($m_id, $m_msg) = $group->_DeleteMember($principal->Id);
1573 $RT::Logger->error("Failed to delete ".$principal->Id.
1574 " as a member of group ".$group->Id."\n".$m_msg);
1576 return ( 0, $self->loc('Could not remove that principal as a [_1] for this ticket', $args{'Type'}) );
1579 unless ( $args{'Silent'} ) {
1580 $self->_NewTransaction(
1581 Type => 'DelWatcher',
1582 OldValue => $principal->Id,
1583 Field => $args{'Type'}
1587 return ( 1, $self->loc("[_1] is no longer a [_2] for this ticket.", $principal->Object->Name, $args{'Type'} ));
1596 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1598 =head2 RequestorAddresses
1600 B<Returns> String: All Ticket Requestor email addresses as a string.
1604 sub RequestorAddresses {
1607 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1611 return ( $self->Requestors->MemberEmailAddressesAsString );
1615 =head2 AdminCcAddresses
1617 returns String: All Ticket AdminCc email addresses as a string
1621 sub AdminCcAddresses {
1624 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1628 return ( $self->AdminCc->MemberEmailAddressesAsString )
1634 returns String: All Ticket Ccs as a string of email addresses
1641 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1645 return ( $self->Cc->MemberEmailAddressesAsString);
1651 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1653 # {{{ sub Requestors
1658 Returns this ticket's Requestors as an RT::Group object
1665 my $group = RT::Group->new($self->CurrentUser);
1666 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1667 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1680 Returns an RT::Group object which contains this ticket's Ccs.
1681 If the user doesn't have "ShowTicket" permission, returns an empty group
1688 my $group = RT::Group->new($self->CurrentUser);
1689 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1690 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1703 Returns an RT::Group object which contains this ticket's AdminCcs.
1704 If the user doesn't have "ShowTicket" permission, returns an empty group
1711 my $group = RT::Group->new($self->CurrentUser);
1712 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1713 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1723 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1726 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1728 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1730 Takes a param hash with the attributes Type and either PrincipalId or Email
1732 Type is one of Requestor, Cc, AdminCc and Owner
1734 PrincipalId is an RT::Principal id, and Email is an email address.
1736 Returns true if the specified principal (or the one corresponding to the
1737 specified address) is a member of the group Type for this ticket.
1744 my %args = ( Type => 'Requestor',
1745 PrincipalId => undef,
1750 # Load the relevant group.
1751 my $group = RT::Group->new($self->CurrentUser);
1752 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1754 # Find the relevant principal.
1755 my $principal = RT::Principal->new($self->CurrentUser);
1756 if (!$args{PrincipalId} && $args{Email}) {
1757 # Look up the specified user.
1758 my $user = RT::User->new($self->CurrentUser);
1759 $user->LoadByEmail($args{Email});
1761 $args{PrincipalId} = $user->PrincipalId;
1764 # A non-existent user can't be a group member.
1768 $principal->Load($args{'PrincipalId'});
1770 # Ask if it has the member in question
1771 return ($group->HasMember($principal));
1776 # {{{ sub IsRequestor
1778 =head2 IsRequestor PRINCIPAL_ID
1780 Takes an RT::Principal id
1781 Returns true if the principal is a requestor of the current ticket.
1790 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1798 =head2 IsCc PRINCIPAL_ID
1800 Takes an RT::Principal id.
1801 Returns true if the principal is a requestor of the current ticket.
1810 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1818 =head2 IsAdminCc PRINCIPAL_ID
1820 Takes an RT::Principal id.
1821 Returns true if the principal is a requestor of the current ticket.
1829 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1839 Takes an RT::User object. Returns true if that user is this ticket's owner.
1840 returns undef otherwise
1848 # no ACL check since this is used in acl decisions
1849 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1853 #Tickets won't yet have owners when they're being created.
1854 unless ( $self->OwnerObj->id ) {
1858 if ( $person->id == $self->OwnerObj->id ) {
1872 # {{{ Routines dealing with queues
1874 # {{{ sub ValidateQueue
1881 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1885 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1886 my $id = $QueueObj->Load($Value);
1902 my $NewQueue = shift;
1904 #Redundant. ACL gets checked in _Set;
1905 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1906 return ( 0, $self->loc("Permission Denied") );
1909 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1910 $NewQueueObj->Load($NewQueue);
1912 unless ( $NewQueueObj->Id() ) {
1913 return ( 0, $self->loc("That queue does not exist") );
1916 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1917 return ( 0, $self->loc('That is the same value') );
1920 $self->CurrentUser->HasRight(
1921 Right => 'CreateTicket',
1922 Object => $NewQueueObj
1926 return ( 0, $self->loc("You may not create requests in that queue.") );
1930 $self->OwnerObj->HasRight(
1931 Right => 'OwnTicket',
1932 Object => $NewQueueObj
1939 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
1949 Takes nothing. returns this ticket's queue object
1956 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1958 #We call __Value so that we can avoid the ACL decision and some deep recursion
1959 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1960 return ($queue_obj);
1967 # {{{ Date printing routines
1973 Returns an RT::Date object containing this ticket's due date
1980 my $time = new RT::Date( $self->CurrentUser );
1982 # -1 is RT::Date slang for never
1984 $time->Set( Format => 'sql', Value => $self->Due );
1987 $time->Set( Format => 'unix', Value => -1 );
1995 # {{{ sub DueAsString
1999 Returns this ticket's due date as a human readable string
2005 return $self->DueObj->AsString();
2010 # {{{ sub ResolvedObj
2014 Returns an RT::Date object of this ticket's 'resolved' time.
2021 my $time = new RT::Date( $self->CurrentUser );
2022 $time->Set( Format => 'sql', Value => $self->Resolved );
2028 # {{{ sub SetStarted
2032 Takes a date in ISO format or undef
2033 Returns a transaction id and a message
2034 The client calls "Start" to note that the project was started on the date in $date.
2035 A null date means "now"
2041 my $time = shift || 0;
2043 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2044 return ( 0, self->loc("Permission Denied") );
2047 #We create a date object to catch date weirdness
2048 my $time_obj = new RT::Date( $self->CurrentUser() );
2050 $time_obj->Set( Format => 'ISO', Value => $time );
2053 $time_obj->SetToNow();
2056 #Now that we're starting, open this ticket
2057 #TODO do we really want to force this as policy? it should be a scrip
2059 #We need $TicketAsSystem, in case the current user doesn't have
2062 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2063 $TicketAsSystem->Load( $self->Id );
2064 if ( $TicketAsSystem->Status eq 'new' ) {
2065 $TicketAsSystem->Open();
2068 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2074 # {{{ sub StartedObj
2078 Returns an RT::Date object which contains this ticket's
2086 my $time = new RT::Date( $self->CurrentUser );
2087 $time->Set( Format => 'sql', Value => $self->Started );
2097 Returns an RT::Date object which contains this ticket's
2105 my $time = new RT::Date( $self->CurrentUser );
2106 $time->Set( Format => 'sql', Value => $self->Starts );
2116 Returns an RT::Date object which contains this ticket's
2124 my $time = new RT::Date( $self->CurrentUser );
2125 $time->Set( Format => 'sql', Value => $self->Told );
2131 # {{{ sub ToldAsString
2135 A convenience method that returns ToldObj->AsString
2137 TODO: This should be deprecated
2143 if ( $self->Told ) {
2144 return $self->ToldObj->AsString();
2153 # {{{ sub TimeWorkedAsString
2155 =head2 TimeWorkedAsString
2157 Returns the amount of time worked on this ticket as a Text String
2161 sub TimeWorkedAsString {
2163 return "0" unless $self->TimeWorked;
2165 #This is not really a date object, but if we diff a number of seconds
2166 #vs the epoch, we'll get a nice description of time worked.
2168 my $worked = new RT::Date( $self->CurrentUser );
2170 #return the #of minutes worked turned into seconds and written as
2171 # a simple text string
2173 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2180 # {{{ Routines dealing with correspondence/comments
2186 Comment on this ticket.
2187 Takes a hashref with the following attributes:
2188 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2191 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content.
2195 ## Please see file perltidy.ERR
2199 my %args = ( CcMessageTo => undef,
2200 BccMessageTo => undef,
2206 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2207 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2208 return ( 0, $self->loc("Permission Denied") );
2211 unless ( $args{'MIMEObj'} ) {
2212 if ( $args{'Content'} ) {
2214 $args{'MIMEObj'} = MIME::Entity->build(
2215 Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
2220 return ( 0, $self->loc("No correspondence attached") );
2224 RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
2226 # If we've been passed in CcMessageTo and BccMessageTo fields,
2227 # add them to the mime object for passing on to the transaction handler
2228 # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
2229 # RT-Send-Bcc: headers
2231 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', $args{'CcMessageTo'} )
2232 if defined $args{'CcMessageTo'};
2233 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', $args{'BccMessageTo'} )
2234 if defined $args{'BccMessageTo'};
2236 #Record the correspondence (write the transaction)
2237 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2239 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2240 TimeTaken => $args{'TimeTaken'},
2241 MIMEObj => $args{'MIMEObj'}
2244 return ( $Trans, $self->loc("The comment has been recorded") );
2249 # {{{ sub Correspond
2253 Correspond on this ticket.
2254 Takes a hashref with the following attributes:
2257 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content
2259 if there's no MIMEObj, Content is used to build a MIME::Entity object
2266 my %args = ( CcMessageTo => undef,
2267 BccMessageTo => undef,
2273 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2274 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2275 return ( 0, $self->loc("Permission Denied") );
2278 unless ( $args{'MIMEObj'} ) {
2279 if ( $args{'Content'} ) {
2281 $args{'MIMEObj'} = MIME::Entity->build(
2282 Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] )
2288 return ( 0, $self->loc("No correspondence attached") );
2292 RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8
2294 # If we've been passed in CcMessageTo and BccMessageTo fields,
2295 # add them to the mime object for passing on to the transaction handler
2296 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2299 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', $args{'CcMessageTo'} )
2300 if defined $args{'CcMessageTo'};
2301 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', $args{'BccMessageTo'} )
2302 if defined $args{'BccMessageTo'};
2304 #Record the correspondence (write the transaction)
2305 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2306 Type => 'Correspond',
2307 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2308 TimeTaken => $args{'TimeTaken'},
2309 MIMEObj => $args{'MIMEObj'} );
2312 $RT::Logger->err( "$self couldn't init a transaction $msg");
2313 return ( $Trans, $self->loc("correspondence (probably) not sent"), $args{'MIMEObj'} );
2316 #Set the last told date to now if this isn't mail from the requestor.
2317 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2319 unless ( $TransObj->IsInbound ) {
2323 return ( $Trans, $self->loc("correspondence sent") );
2330 # {{{ Routines dealing with Links and Relations between tickets
2332 # {{{ Link Collections
2338 This returns an RT::Links object which references all the tickets
2339 which are 'MembersOf' this ticket
2345 return ( $self->_Links( 'Target', 'MemberOf' ) );
2354 This returns an RT::Links object which references all the tickets that this
2355 ticket is a 'MemberOf'
2361 return ( $self->_Links( 'Base', 'MemberOf' ) );
2370 This returns an RT::Links object which shows all references for which this ticket is a base
2376 return ( $self->_Links( 'Base', 'RefersTo' ) );
2385 This returns an RT::Links object which shows all references for which this ticket is a target
2391 return ( $self->_Links( 'Target', 'RefersTo' ) );
2400 This returns an RT::Links object which references all the tickets that depend on this one
2406 return ( $self->_Links( 'Target', 'DependsOn' ) );
2413 =head2 HasUnresolvedDependencies
2415 Takes a paramhash of Type (default to '__any'). Returns true if
2416 $self->UnresolvedDependencies returns an object with one or more members
2417 of that type. Returns false otherwise
2422 my $t1 = RT::Ticket->new($RT::SystemUser);
2423 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
2424 ok($id, "Created dep test 1 - $msg");
2426 my $t2 = RT::Ticket->new($RT::SystemUser);
2427 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
2428 ok($id2, "Created dep test 2 - $msg2");
2429 my $t3 = RT::Ticket->new($RT::SystemUser);
2430 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
2431 ok($id3, "Created dep test 3 - $msg3");
2433 ok ($t1->AddLink( Type => 'DependsOn', Target => $t2->id));
2434 ok ($t1->AddLink( Type => 'DependsOn', Target => $t3->id));
2436 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
2437 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
2438 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
2439 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
2440 my ($rid, $rmsg)= $t1->Resolve();
2443 ($rid, $rmsg)= $t1->Resolve();
2446 ($rid, $rmsg)= $t1->Resolve();
2454 sub HasUnresolvedDependencies {
2461 my $deps = $self->UnresolvedDependencies;
2464 $deps->Limit( FIELD => 'Type',
2466 VALUE => $args{Type});
2472 if ($deps->Count > 0) {
2481 # {{{ UnresolvedDependencies
2483 =head2 UnresolvedDependencies
2485 Returns an RT::Tickets object of tickets which this ticket depends on
2486 and which have a status of new, open or stalled. (That list comes from
2487 RT::Queue->ActiveStatusArray
2492 sub UnresolvedDependencies {
2494 my $deps = RT::Tickets->new($self->CurrentUser);
2496 my @live_statuses = RT::Queue->ActiveStatusArray();
2497 foreach my $status (@live_statuses) {
2498 $deps->LimitStatus(VALUE => $status);
2500 $deps->LimitDependedOnBy($self->Id);
2508 # {{{ AllDependedOnBy
2510 =head2 AllDependedOnBy
2512 Returns an array of RT::Ticket objects which (directly or indirectly)
2513 depends on this ticket; takes an optional 'Type' argument in the param
2514 hash, which will limit returned tickets to that type, as well as cause
2515 tickets with that type to serve as 'leaf' nodes that stops the recursive
2520 sub AllDependedOnBy {
2522 my $dep = $self->DependedOnBy;
2530 while (my $link = $dep->Next()) {
2531 next unless ($link->BaseURI->IsLocal());
2532 next if $args{_found}{$link->BaseObj->Id};
2535 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
2536 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
2538 elsif ($link->BaseObj->Type eq $args{Type}) {
2539 $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
2542 $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
2547 return map { $args{_found}{$_} } sort keys %{$args{_found}};
2560 This returns an RT::Links object which references all the tickets that this ticket depends on
2566 return ( $self->_Links( 'Base', 'DependsOn' ) );
2579 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2582 my $type = shift || "";
2584 unless ( $self->{"$field$type"} ) {
2585 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2586 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2587 # Maybe this ticket is a merged ticket
2588 my $Tickets = new RT::Tickets( $self->CurrentUser );
2589 # at least to myself
2590 $self->{"$field$type"}->Limit( FIELD => $field,
2591 VALUE => $self->URI,
2592 ENTRYAGGREGATOR => 'OR' );
2593 $Tickets->Limit( FIELD => 'EffectiveId',
2594 VALUE => $self->EffectiveId );
2595 while (my $Ticket = $Tickets->Next) {
2596 $self->{"$field$type"}->Limit( FIELD => $field,
2597 VALUE => $Ticket->URI,
2598 ENTRYAGGREGATOR => 'OR' );
2600 $self->{"$field$type"}->Limit( FIELD => 'Type',
2605 return ( $self->{"$field$type"} );
2612 # {{{ sub DeleteLink
2616 Delete a link. takes a paramhash of Base, Target and Type.
2617 Either Base or Target must be null. The null value will
2618 be replaced with this ticket\'s id
2632 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2633 $RT::Logger->debug("No permission to delete links\n");
2634 return ( 0, $self->loc('Permission Denied'))
2638 #we want one of base and target. we don't care which
2639 #but we only want _one_
2644 if ( $args{'Base'} and $args{'Target'} ) {
2645 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
2646 return ( 0, $self->loc("Can't specifiy both base and target") );
2648 elsif ( $args{'Base'} ) {
2649 $args{'Target'} = $self->URI();
2650 $remote_link = $args{'Base'};
2651 $direction = 'Target';
2653 elsif ( $args{'Target'} ) {
2654 $args{'Base'} = $self->URI();
2655 $remote_link = $args{'Target'};
2659 $RT::Logger->debug("$self: Base or Target must be specified\n");
2660 return ( 0, $self->loc('Either base or target must be specified') );
2663 my $link = new RT::Link( $self->CurrentUser );
2664 $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
2667 $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} );
2671 my $linkid = $link->id;
2674 my $TransString = "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
2675 my $remote_uri = RT::URI->new( $RT::SystemUser );
2676 $remote_uri->FromURI( $remote_link );
2678 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2679 Type => 'DeleteLink',
2680 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2681 OldValue => $remote_uri->URI || $remote_link,
2685 return ( $Trans, $self->loc("Link deleted ([_1])", $TransString));
2688 #if it's not a link we can find
2690 $RT::Logger->debug("Couldn't find that link\n");
2691 return ( 0, $self->loc("Link not found") );
2701 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2708 my %args = ( Target => '',
2714 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2715 return ( 0, $self->loc("Permission Denied") );
2718 # Remote_link is the URI of the object that is not this ticket
2722 if ( $args{'Base'} and $args{'Target'} ) {
2724 "$self tried to delete a link. both base and target were specified\n" );
2725 return ( 0, $self->loc("Can't specifiy both base and target") );
2727 elsif ( $args{'Base'} ) {
2728 $args{'Target'} = $self->URI();
2729 $remote_link = $args{'Base'};
2730 $direction = 'Target';
2732 elsif ( $args{'Target'} ) {
2733 $args{'Base'} = $self->URI();
2734 $remote_link = $args{'Target'};
2738 return ( 0, $self->loc('Either base or target must be specified') );
2741 # If the base isn't a URI, make it a URI.
2742 # If the target isn't a URI, make it a URI.
2744 # {{{ Check if the link already exists - we don't want duplicates
2746 my $old_link = RT::Link->new( $self->CurrentUser );
2747 $old_link->LoadByParams( Base => $args{'Base'},
2748 Type => $args{'Type'},
2749 Target => $args{'Target'} );
2750 if ( $old_link->Id ) {
2751 $RT::Logger->debug("$self Somebody tried to duplicate a link");
2752 return ( $old_link->id, $self->loc("Link already exists"), 0 );
2757 # Storing the link in the DB.
2758 my $link = RT::Link->new( $self->CurrentUser );
2759 my ($linkid) = $link->Create( Target => $args{Target},
2760 Base => $args{Base},
2761 Type => $args{Type} );
2764 return ( 0, $self->loc("Link could not be created") );
2768 "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
2770 # Don't write the transaction if we're doing this on create
2771 if ( $args{'Silent'} ) {
2772 return ( 1, $self->loc( "Link created ([_1])", $TransString ) );
2775 my $remote_uri = RT::URI->new( $RT::SystemUser );
2776 $remote_uri->FromURI( $remote_link );
2778 #Write the transaction
2779 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2781 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2782 NewValue => $remote_uri->URI || $remote_link,
2784 return ( $Trans, $self->loc( "Link created ([_1])", $TransString ) );
2795 Returns this ticket's URI
2801 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
2802 return($uri->URIForObject($self));
2810 MergeInto take the id of the ticket to merge this ticket into.
2816 my $MergeInto = shift;
2818 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2819 return ( 0, $self->loc("Permission Denied") );
2822 # Load up the new ticket.
2823 my $NewTicket = RT::Ticket->new($RT::SystemUser);
2824 $NewTicket->Load($MergeInto);
2826 # make sure it exists.
2827 unless ( defined $NewTicket->Id ) {
2828 return ( 0, $self->loc("New ticket doesn't exist") );
2831 # Make sure the current user can modify the new ticket.
2832 unless ( $NewTicket->CurrentUserHasRight('ModifyTicket') ) {
2833 $RT::Logger->debug("failed...");
2834 return ( 0, $self->loc("Permission Denied") );
2838 "checking if the new ticket has the same id and effective id...");
2839 unless ( $NewTicket->id == $NewTicket->EffectiveId ) {
2840 $RT::Logger->err( "$self trying to merge into "
2842 . " which is itself merged.\n" );
2844 $self->loc("Can't merge into a merged ticket. You should never get this error") );
2847 # We use EffectiveId here even though it duplicates information from
2848 # the links table becasue of the massive performance hit we'd take
2849 # by trying to do a seperate database query for merge info everytime
2852 #update this ticket's effective id to the new ticket's id.
2853 my ( $id_val, $id_msg ) = $self->__Set(
2854 Field => 'EffectiveId',
2855 Value => $NewTicket->Id()
2860 "Couldn't set effective ID for " . $self->Id . ": $id_msg" );
2861 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2864 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2866 unless ($status_val) {
2867 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2871 # update all the links that point to that old ticket
2872 my $old_links_to = RT::Links->new($self->CurrentUser);
2873 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2875 while (my $link = $old_links_to->Next) {
2876 if ($link->Base eq $NewTicket->URI) {
2879 $link->SetTarget($NewTicket->URI);
2884 my $old_links_from = RT::Links->new($self->CurrentUser);
2885 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2887 while (my $link = $old_links_from->Next) {
2888 if ($link->Target eq $NewTicket->URI) {
2891 $link->SetBase($NewTicket->URI);
2897 #make a new link: this ticket is merged into that other ticket.
2898 $self->AddLink( Type => 'MergedInto', Target => $NewTicket->Id());
2900 #add all of this ticket's watchers to that ticket.
2901 my $requestors = $self->Requestors->MembersObj;
2902 while (my $watcher = $requestors->Next) {
2903 $NewTicket->_AddWatcher( Type => 'Requestor',
2905 PrincipalId => $watcher->MemberId);
2908 my $Ccs = $self->Cc->MembersObj;
2909 while (my $watcher = $Ccs->Next) {
2910 $NewTicket->_AddWatcher( Type => 'Cc',
2912 PrincipalId => $watcher->MemberId);
2915 my $AdminCcs = $self->AdminCc->MembersObj;
2916 while (my $watcher = $AdminCcs->Next) {
2917 $NewTicket->_AddWatcher( Type => 'AdminCc',
2919 PrincipalId => $watcher->MemberId);
2923 #find all of the tickets that were merged into this ticket.
2924 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2925 $old_mergees->Limit(
2926 FIELD => 'EffectiveId',
2931 # update their EffectiveId fields to the new ticket's id
2932 while ( my $ticket = $old_mergees->Next() ) {
2933 my ( $val, $msg ) = $ticket->__Set(
2934 Field => 'EffectiveId',
2935 Value => $NewTicket->Id()
2939 $NewTicket->_SetLastUpdated;
2941 return ( 1, $self->loc("Merge Successful") );
2948 # {{{ Routines dealing with ownership
2954 Takes nothing and returns an RT::User object of
2962 #If this gets ACLed, we lose on a rights check in User.pm and
2963 #get deep recursion. if we need ACLs here, we need
2964 #an equiv without ACLs
2966 my $owner = new RT::User( $self->CurrentUser );
2967 $owner->Load( $self->__Value('Owner') );
2969 #Return the owner object
2975 # {{{ sub OwnerAsString
2977 =head2 OwnerAsString
2979 Returns the owner's email address
2985 return ( $self->OwnerObj->EmailAddress );
2995 Takes two arguments:
2996 the Id or Name of the owner
2997 and (optionally) the type of the SetOwner Transaction. It defaults
2998 to 'Give'. 'Steal' is also a valid option.
3002 my $root = RT::User->new($RT::SystemUser);
3003 $root->Load('root');
3004 ok ($root->Id, "Loaded the root user");
3005 my $t = RT::Ticket->new($RT::SystemUser);
3007 $t->SetOwner('root');
3008 ok ($t->OwnerObj->Name eq 'root' , "Root owns the ticket");
3010 ok ($t->OwnerObj->id eq $RT::SystemUser->id , "SystemUser owns the ticket");
3011 my $txns = RT::Transactions->new($RT::SystemUser);
3012 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3013 $txns->Limit(FIELD => 'Ticket', VALUE => '1');
3014 my $steal = $txns->First;
3015 ok($steal->OldValue == $root->Id , "Stolen from root");
3016 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3024 my $NewOwner = shift;
3025 my $Type = shift || "Give";
3027 # must have ModifyTicket rights
3028 # or TakeTicket/StealTicket and $NewOwner is self
3029 # see if it's a take
3030 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
3031 unless ( $self->CurrentUserHasRight('ModifyTicket')
3032 || $self->CurrentUserHasRight('TakeTicket') ) {
3033 return ( 0, $self->loc("Permission Denied") );
3037 # see if it's a steal
3038 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
3039 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
3041 unless ( $self->CurrentUserHasRight('ModifyTicket')
3042 || $self->CurrentUserHasRight('StealTicket') ) {
3043 return ( 0, $self->loc("Permission Denied") );
3047 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3048 return ( 0, $self->loc("Permission Denied") );
3051 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3052 my $OldOwnerObj = $self->OwnerObj;
3054 $NewOwnerObj->Load($NewOwner);
3055 if ( !$NewOwnerObj->Id ) {
3056 return ( 0, $self->loc("That user does not exist") );
3059 #If thie ticket has an owner and it's not the current user
3061 if ( ( $Type ne 'Steal' )
3062 and ( $Type ne 'Force' )
3063 and #If we're not stealing
3064 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
3065 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
3066 ) { #and it's not us
3069 "You can only reassign tickets that you own or that are unowned" ) );
3072 #If we've specified a new owner and that user can't modify the ticket
3073 elsif ( ( $NewOwnerObj->Id )
3074 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
3077 return ( 0, $self->loc("That user may not own tickets in that queue") );
3080 #If the ticket has an owner and it's the new owner, we don't need
3082 elsif ( ( $self->OwnerObj )
3083 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
3084 return ( 0, $self->loc("That user already owns that ticket") );
3087 $RT::Handle->BeginTransaction();
3089 # Delete the owner in the owner group, then add a new one
3090 # TODO: is this safe? it's not how we really want the API to work
3091 # for most things, but it's fast.
3092 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3094 $RT::Handle->Rollback();
3095 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3098 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3099 PrincipalId => $NewOwnerObj->PrincipalId,
3100 InsideTransaction => 1 );
3102 $RT::Handle->Rollback();
3103 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3106 # We call set twice with slightly different arguments, so
3107 # as to not have an SQL transaction span two RT transactions
3109 my ( $val, $msg ) = $self->_Set(
3111 RecordTransaction => 0,
3112 Value => $NewOwnerObj->Id,
3114 TransactionType => $Type,
3115 CheckACL => 0, # don't check acl
3119 $RT::Handle->Rollback;
3120 return ( 0, $self->loc("Could not change owner. ") . $msg );
3123 $RT::Handle->Commit();
3125 my ( $trans, $msg, undef ) = $self->_NewTransaction(
3128 NewValue => $NewOwnerObj->Id,
3129 OldValue => $OldOwnerObj->Id,
3133 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3134 $OldOwnerObj->Name, $NewOwnerObj->Name );
3136 # TODO: make sure the trans committed properly
3138 return ( $trans, $msg );
3148 A convenince method to set the ticket's owner to the current user
3154 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3163 Convenience method to set the owner to 'nobody' if the current user is the owner.
3169 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3178 A convenience method to change the owner of the current ticket to the
3179 current user. Even if it's owned by another user.
3186 if ( $self->IsOwner( $self->CurrentUser ) ) {
3187 return ( 0, $self->loc("You already own this ticket") );
3190 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3200 # {{{ Routines dealing with status
3202 # {{{ sub ValidateStatus
3204 =head2 ValidateStatus STATUS
3206 Takes a string. Returns true if that status is a valid status for this ticket.
3207 Returns false otherwise.
3211 sub ValidateStatus {
3215 #Make sure the status passed in is valid
3216 unless ( $self->QueueObj->IsValidStatus($status) ) {
3228 =head2 SetStatus STATUS
3230 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3232 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.
3236 my $tt = RT::Ticket->new($RT::SystemUser);
3237 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3240 ok($tt->Status eq 'new', "New ticket is created as new");
3242 ($id, $msg) = $tt->SetStatus('open');
3244 ok ($msg =~ /open/i, "Status message is correct");
3245 ($id, $msg) = $tt->SetStatus('resolved');
3247 ok ($msg =~ /resolved/i, "Status message is correct");
3248 ($id, $msg) = $tt->SetStatus('resolved');
3262 $args{Status} = shift;
3269 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3270 return ( 0, $self->loc('Permission Denied') );
3273 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3274 return (0, $self->loc('That ticket has unresolved dependencies'));
3277 my $now = RT::Date->new( $self->CurrentUser );
3280 #If we're changing the status from new, record that we've started
3281 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3283 #Set the Started time to "now"
3284 $self->_Set( Field => 'Started',
3286 RecordTransaction => 0 );
3289 if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) {
3291 #When we resolve a ticket, set the 'Resolved' attribute to now.
3292 $self->_Set( Field => 'Resolved',
3294 RecordTransaction => 0 );
3297 #Actually update the status
3298 my ($val, $msg)= $self->_Set( Field => 'Status',
3299 Value => $args{Status},
3301 TransactionType => 'Status' );
3312 Takes no arguments. Marks this ticket for garbage collection
3318 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead.");
3319 return $self->Delete;
3324 return ( $self->SetStatus('deleted') );
3326 # TODO: garbage collection
3335 Sets this ticket's status to stalled
3341 return ( $self->SetStatus('stalled') );
3350 Sets this ticket's status to rejected
3356 return ( $self->SetStatus('rejected') );
3365 Sets this ticket\'s status to Open
3371 return ( $self->SetStatus('open') );
3380 Sets this ticket\'s status to Resolved
3386 return ( $self->SetStatus('resolved') );
3393 # {{{ Routines dealing with custom fields
3396 # {{{ FirstCustomFieldValue
3398 =item FirstCustomFieldValue FIELD
3400 Return the content of the first value of CustomField FIELD for this ticket
3401 Takes a field id or name
3405 sub FirstCustomFieldValue {
3408 my $values = $self->CustomFieldValues($field);
3409 if ($values->First) {
3410 return $values->First->Content;
3419 # {{{ CustomFieldValues
3421 =item CustomFieldValues FIELD
3423 Return a TicketCustomFieldValues object of all values of CustomField FIELD for this ticket.
3424 Takes a field id or name.
3429 sub CustomFieldValues {
3433 my $cf = RT::CustomField->new($self->CurrentUser);
3435 if ($field =~ /^\d+$/) {
3436 $cf->LoadById($field);
3438 $cf->LoadByNameAndQueue(Name => $field, Queue => $self->QueueObj->Id);
3440 my $cf_values = RT::TicketCustomFieldValues->new( $self->CurrentUser );
3441 $cf_values->LimitToCustomField($cf->id);
3442 $cf_values->LimitToTicket($self->Id());
3444 # @values is a CustomFieldValues object;
3445 return ($cf_values);
3450 # {{{ AddCustomFieldValue
3452 =item AddCustomFieldValue { Field => FIELD, Value => VALUE }
3454 VALUE can either be a CustomFieldValue object or a string.
3455 FIELD can be a CustomField object OR a CustomField ID.
3458 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
3459 deletes the old value.
3460 If VALUE isn't a valid value for the custom field, returns
3461 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3465 sub AddCustomFieldValue {
3467 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3468 return ( 0, $self->loc("Permission Denied") );
3470 $self->_AddCustomFieldValue(@_);
3473 sub _AddCustomFieldValue {
3478 RecordTransaction => 1,
3482 my $cf = RT::CustomField->new( $self->CurrentUser );
3483 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3484 $cf->Load( $args{'Field'}->id );
3487 $cf->Load( $args{'Field'} );
3490 unless ( $cf->Id ) {
3491 return ( 0, $self->loc("Custom field [_1] not found", $args{'Field'}) );
3494 # Load up a TicketCustomFieldValues object for this custom field and this ticket
3495 my $values = $cf->ValuesForTicket( $self->id );
3497 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
3498 return ( 0, $self->loc("Invalid value for custom field") );
3501 # If the custom field only accepts a single value, delete the existing
3502 # value and record a "changed from foo to bar" transaction
3503 if ( $cf->SingleValue ) {
3505 # We need to whack any old values here. In most cases, the custom field should
3506 # only have one value to delete. In the pathalogical case, this custom field
3507 # used to be a multiple and we have many values to whack....
3508 my $cf_values = $values->Count;
3510 if ( $cf_values > 1 ) {
3511 my $i = 0; #We want to delete all but the last one, so we can then
3512 # execute the same code to "change" the value from old to new
3513 while ( my $value = $values->Next ) {
3515 if ( $i < $cf_values ) {
3516 my $old_value = $value->Content;
3517 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $value->Content);
3521 my ( $TransactionId, $Msg, $TransactionObj ) =
3522 $self->_NewTransaction(
3523 Type => 'CustomField',
3525 OldValue => $old_value
3532 if (my $value = $cf->ValuesForTicket( $self->Id )->First) {
3533 $old_value = $value->Content();
3534 return (1) if $old_value eq $args{'Value'};
3537 my ( $new_value_id, $value_msg ) = $cf->AddValueForTicket(
3538 Ticket => $self->Id,
3539 Content => $args{'Value'}
3542 unless ($new_value_id) {
3544 $self->loc("Could not add new custom field value for ticket. [_1] ",
3548 my $new_value = RT::TicketCustomFieldValue->new( $self->CurrentUser );
3549 $new_value->Load($new_value_id);
3551 # now that adding the new value was successful, delete the old one
3553 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $old_value);
3559 if ($args{'RecordTransaction'}) {
3560 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3561 Type => 'CustomField',
3563 OldValue => $old_value,
3564 NewValue => $new_value->Content
3568 if ( $old_value eq '' ) {
3569 return ( 1, $self->loc("[_1] [_2] added", $cf->Name, $new_value->Content) );
3571 elsif ( $new_value->Content eq '' ) {
3572 return ( 1, $self->loc("[_1] [_2] deleted", $cf->Name, $old_value) );
3575 return ( 1, $self->loc("[_1] [_2] changed to [_3]", $cf->Name, $old_value, $new_value->Content ) );
3580 # otherwise, just add a new value and record "new value added"
3582 my ( $new_value_id ) = $cf->AddValueForTicket(
3583 Ticket => $self->Id,
3584 Content => $args{'Value'}
3587 unless ($new_value_id) {
3589 $self->loc("Could not add new custom field value for ticket. "));
3591 if ( $args{'RecordTransaction'} ) {
3592 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3593 Type => 'CustomField',
3595 NewValue => $args{'Value'}
3597 unless ($TransactionId) {
3599 $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
3602 return ( 1, $self->loc("[_1] added as a value for [_2]",$args{'Value'}, $cf->Name));
3609 # {{{ DeleteCustomFieldValue
3611 =item DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
3613 Deletes VALUE as a value of CustomField FIELD.
3615 VALUE can be a string, a CustomFieldValue or a TicketCustomFieldValue.
3617 If VALUE isn't a valid value for the custom field, returns
3618 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3622 sub DeleteCustomFieldValue {
3629 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3630 return ( 0, $self->loc("Permission Denied") );
3632 my $cf = RT::CustomField->new( $self->CurrentUser );
3633 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3634 $cf->LoadById( $args{'Field'}->id );
3637 $cf->LoadById( $args{'Field'} );
3640 unless ( $cf->Id ) {
3641 return ( 0, $self->loc("Custom field not found") );
3645 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $args{'Value'});
3649 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3650 Type => 'CustomField',
3652 OldValue => $args{'Value'}
3654 unless($TransactionId) {
3655 return(0, $self->loc("Couldn't create a transaction: [_1]", $Msg));
3658 return($TransactionId, $self->loc("[_1] is no longer a value for custom field [_2]", $args{'Value'}, $cf->Name));
3665 # {{{ Actions + Routines dealing with transactions
3667 # {{{ sub SetTold and _SetTold
3669 =head2 SetTold ISO [TIMETAKEN]
3671 Updates the told and records a transaction
3678 $told = shift if (@_);
3679 my $timetaken = shift || 0;
3681 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3682 return ( 0, $self->loc("Permission Denied") );
3685 my $datetold = new RT::Date( $self->CurrentUser );
3687 $datetold->Set( Format => 'iso',
3691 $datetold->SetToNow();
3694 return ( $self->_Set( Field => 'Told',
3695 Value => $datetold->ISO,
3696 TimeTaken => $timetaken,
3697 TransactionType => 'Told' ) );
3702 Updates the told without a transaction or acl check. Useful when we're sending replies.
3709 my $now = new RT::Date( $self->CurrentUser );
3712 #use __Set to get no ACLs ;)
3713 return ( $self->__Set( Field => 'Told',
3714 Value => $now->ISO ) );
3719 # {{{ sub Transactions
3723 Returns an RT::Transactions object of all transactions on this ticket
3730 use RT::Transactions;
3731 my $transactions = RT::Transactions->new( $self->CurrentUser );
3733 #If the user has no rights, return an empty object
3734 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3735 my $tickets = $transactions->NewAlias('Tickets');
3736 $transactions->Join(
3742 $transactions->Limit(
3744 FIELD => 'EffectiveId',
3745 VALUE => $self->id()
3748 # if the user may not see comments do not return them
3749 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3750 $transactions->Limit(
3758 return ($transactions);
3763 # {{{ sub _NewTransaction
3765 sub _NewTransaction {
3778 require RT::Transaction;
3779 my $trans = new RT::Transaction( $self->CurrentUser );
3780 my ( $transaction, $msg ) = $trans->Create(
3781 Ticket => $self->Id,
3782 TimeTaken => $args{'TimeTaken'},
3783 Type => $args{'Type'},
3784 Data => $args{'Data'},
3785 Field => $args{'Field'},
3786 NewValue => $args{'NewValue'},
3787 OldValue => $args{'OldValue'},
3788 MIMEObj => $args{'MIMEObj'}
3792 $self->Load($self->Id);
3794 $RT::Logger->warning($msg) unless $transaction;
3796 $self->_SetLastUpdated;
3798 if ( defined $args{'TimeTaken'} ) {
3799 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
3801 return ( $transaction, $msg, $trans );
3808 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3810 # {{{ sub _ClassAccessible
3812 sub _ClassAccessible {
3814 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3815 Queue => { 'read' => 1, 'write' => 1 },
3816 Requestors => { 'read' => 1, 'write' => 1 },
3817 Owner => { 'read' => 1, 'write' => 1 },
3818 Subject => { 'read' => 1, 'write' => 1 },
3819 InitialPriority => { 'read' => 1, 'write' => 1 },
3820 FinalPriority => { 'read' => 1, 'write' => 1 },
3821 Priority => { 'read' => 1, 'write' => 1 },
3822 Status => { 'read' => 1, 'write' => 1 },
3823 TimeEstimated => { 'read' => 1, 'write' => 1 },
3824 TimeWorked => { 'read' => 1, 'write' => 1 },
3825 TimeLeft => { 'read' => 1, 'write' => 1 },
3826 Created => { 'read' => 1, 'auto' => 1 },
3827 Creator => { 'read' => 1, 'auto' => 1 },
3828 Told => { 'read' => 1, 'write' => 1 },
3829 Resolved => { 'read' => 1 },
3830 Type => { 'read' => 1 },
3831 Starts => { 'read' => 1, 'write' => 1 },
3832 Started => { 'read' => 1, 'write' => 1 },
3833 Due => { 'read' => 1, 'write' => 1 },
3834 Creator => { 'read' => 1, 'auto' => 1 },
3835 Created => { 'read' => 1, 'auto' => 1 },
3836 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3837 LastUpdated => { 'read' => 1, 'auto' => 1 }
3849 my %args = ( Field => undef,
3852 RecordTransaction => 1,
3855 TransactionType => 'Set',
3858 if ($args{'CheckACL'}) {
3859 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3860 return ( 0, $self->loc("Permission Denied"));
3864 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3865 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3866 return(0, $self->loc("Internal Error"));
3869 #if the user is trying to modify the record
3871 #Take care of the old value we really don't want to get in an ACL loop.
3872 # so ask the super::_Value
3873 my $Old = $self->SUPER::_Value("$args{'Field'}");
3876 if ( $args{'UpdateTicket'} ) {
3879 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3880 Value => $args{'Value'} );
3882 #If we can't actually set the field to the value, don't record
3883 # a transaction. instead, get out of here.
3884 if ( $ret == 0 ) { return ( 0, $msg ); }
3887 if ( $args{'RecordTransaction'} == 1 ) {
3889 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3890 Type => $args{'TransactionType'},
3891 Field => $args{'Field'},
3892 NewValue => $args{'Value'},
3894 TimeTaken => $args{'TimeTaken'},
3896 return ( $Trans, scalar $TransObj->Description );
3899 return ( $ret, $msg );
3909 Takes the name of a table column.
3910 Returns its value as a string, if the user passes an ACL check
3919 #if the field is public, return it.
3920 if ( $self->_Accessible( $field, 'public' ) ) {
3922 #$RT::Logger->debug("Skipping ACL check for $field\n");
3923 return ( $self->SUPER::_Value($field) );
3927 #If the current user doesn't have ACLs, don't let em at it.
3929 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3932 return ( $self->SUPER::_Value($field) );
3938 # {{{ sub _UpdateTimeTaken
3940 =head2 _UpdateTimeTaken
3942 This routine will increment the timeworked counter. it should
3943 only be called from _NewTransaction
3947 sub _UpdateTimeTaken {
3949 my $Minutes = shift;
3952 $Total = $self->SUPER::_Value("TimeWorked");
3953 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3955 Field => "TimeWorked",
3966 # {{{ Routines dealing with ACCESS CONTROL
3968 # {{{ sub CurrentUserHasRight
3970 =head2 CurrentUserHasRight
3972 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3973 1 if the user has that right. It returns 0 if the user doesn't have that right.
3977 sub CurrentUserHasRight {
3983 Principal => $self->CurrentUser->UserObj(),
3996 Takes a paramhash with the attributes 'Right' and 'Principal'
3997 'Right' is a ticket-scoped textual right from RT::ACE
3998 'Principal' is an RT::User object
4000 Returns 1 if the principal has the right. Returns undef if not.
4012 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
4014 $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
4018 $args{'Principal'}->HasRight(
4020 Right => $args{'Right'}
4033 Jesse Vincent, jesse@bestpractical.com