1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # }}} END BPS TAGGED BLOCK
51 my $ticket = new RT::Ticket($CurrentUser);
52 $ticket->Load($ticket_id);
56 This module lets you manipulate RT\'s ticket object.
64 ok(my $testqueue = RT::Queue->new($RT::SystemUser));
65 ok($testqueue->Create( Name => 'ticket tests'));
66 ok($testqueue->Id != 0);
67 use_ok(RT::CustomField);
68 ok(my $testcf = RT::CustomField->new($RT::SystemUser));
69 ok($testcf->Create( Name => 'selectmulti',
70 Queue => $testqueue->id,
71 Type => 'SelectMultiple'));
72 ok($testcf->AddValue ( Name => 'Value1',
74 Description => 'A testing value'));
75 ok($testcf->AddValue ( Name => 'Value2',
77 Description => 'Another testing value'));
78 ok($testcf->AddValue ( Name => 'Value3',
80 Description => 'Yet Another testing value'));
82 ok($testcf->Values->Count == 3);
86 my $u = RT::User->new($RT::SystemUser);
88 ok ($u->Id, "Found the root user");
89 ok(my $t = RT::Ticket->new($RT::SystemUser));
90 ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
95 ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner");
96 ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
98 ok($cfv != 0, "Custom field creation didn't return an error: $cfm");
99 ok($t->CustomFieldValues($testcf->Id)->Count == 1);
100 ok($t->CustomFieldValues($testcf->Id)->First &&
101 $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');;
103 ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
105 ok ($cfdv != 0, "Deleted a custom field value: $cfdm");
106 ok($t->CustomFieldValues($testcf->Id)->Count == 0);
108 ok(my $t2 = RT::Ticket->new($RT::SystemUser));
110 ok($t2->Subject eq 'Testing');
111 ok($t2->QueueObj->Id eq $testqueue->id);
112 ok($t2->OwnerObj->Id == $u->Id);
114 my $t3 = RT::Ticket->new($RT::SystemUser);
115 my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
116 Subject => 'Testing',
118 my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
120 ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
121 my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
123 ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm");
124 my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
126 ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm");
127 ok($t->CustomFieldValues($testcf->Id)->Count == 2,
128 "This ticket has 2 custom field values");
129 ok($t3->CustomFieldValues($testcf->Id)->Count == 1,
130 "This ticket has 1 custom field value");
137 no warnings qw(redefine);
144 use RT::CustomFields;
145 use RT::TicketCustomFieldValues;
147 use RT::URI::fsck_com_rt;
154 ok(require RT::Ticket, "Loading the RT::Ticket library");
163 # A helper table for links mapping to make it easier
164 # to build and parse links between tickets
166 use vars '%LINKTYPEMAP';
169 MemberOf => { Type => 'MemberOf',
171 Parents => { Type => 'MemberOf',
173 Members => { Type => 'MemberOf',
175 Children => { Type => 'MemberOf',
177 HasMember => { Type => 'MemberOf',
179 RefersTo => { Type => 'RefersTo',
181 ReferredToBy => { Type => 'RefersTo',
183 DependsOn => { Type => 'DependsOn',
185 DependedOnBy => { Type => 'DependsOn',
187 MergedInto => { Type => 'MergedInto',
195 # A helper table for links mapping to make it easier
196 # to build and parse links between tickets
198 use vars '%LINKDIRMAP';
201 MemberOf => { Base => 'MemberOf',
202 Target => 'HasMember', },
203 RefersTo => { Base => 'RefersTo',
204 Target => 'ReferredToBy', },
205 DependsOn => { Base => 'DependsOn',
206 Target => 'DependedOnBy', },
207 MergedInto => { Base => 'MergedInto',
208 Target => 'MergedInto', },
214 sub LINKTYPEMAP { return \%LINKTYPEMAP }
215 sub LINKDIRMAP { return \%LINKDIRMAP }
221 Takes a single argument. This can be a ticket id, ticket alias or
222 local ticket uri. If the ticket can't be loaded, returns undef.
223 Otherwise, returns the ticket id.
231 #TODO modify this routine to look at EffectiveId and do the recursive load
232 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
234 #If it's a local URI, turn it into a ticket id
235 if ( $id =~ /^$RT::TicketBaseURI(\d+)$/ ) {
239 #If it's a remote URI, we're going to punt for now
240 elsif ( $id =~ '://' ) {
244 #If we have an integer URI, load the ticket
245 if ( $id =~ /^\d+$/ ) {
246 my $ticketid = $self->LoadById($id);
249 $RT::Logger->crit("$self tried to load a bogus ticket: $id\n");
254 #It's not a URI. It's not a numerical ticket ID. Punt!
259 #If we're merged, resolve the merge.
260 if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) {
261 return ( $self->Load( $self->EffectiveId ) );
264 #Ok. we're loaded. lets get outa here.
265 return ( $self->Id );
275 Given a local ticket URI, loads the specified ticket.
283 if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) {
285 return ( $self->Load($id) );
298 Arguments: ARGS is a hash of named parameters. Valid parameters are:
301 Queue - Either a Queue object or a Queue Name
302 Requestor - A reference to a list of email addresses or RT user Names
303 Cc - A reference to a list of email addresses or Names
304 AdminCc - A reference to a list of email addresses or Names
305 Type -- The ticket\'s type. ignore this for now
306 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
307 Subject -- A string describing the subject of the ticket
308 Priority -- an integer from 0 to 99
309 InitialPriority -- an integer from 0 to 99
310 FinalPriority -- an integer from 0 to 99
311 Status -- any valid status (Defined in RT::Queue)
312 TimeEstimated -- an integer. estimated time for this task in minutes
313 TimeWorked -- an integer. time worked so far in minutes
314 TimeLeft -- an integer. time remaining in minutes
315 Starts -- an ISO date describing the ticket\'s start date and time in GMT
316 Due -- an ISO date describing the ticket\'s due date and time in GMT
317 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
318 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
321 Returns: TICKETID, Transaction Object, Error Message
326 my $t = RT::Ticket->new($RT::SystemUser);
328 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");
330 ok ( my $id = $t->Id, "Got ticket id");
331 ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to");
332 ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby");
333 ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
344 EffectiveId => undef,
352 InitialPriority => undef,
353 FinalPriority => undef,
364 _RecordTransaction => 1,
368 my ( $ErrStr, $Owner, $resolved );
369 my (@non_fatal_errors);
371 my $QueueObj = RT::Queue->new($RT::SystemUser);
373 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
374 $QueueObj->Load( $args{'Queue'} );
376 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
377 $QueueObj->Load( $args{'Queue'}->Id );
381 $args{'Queue'} . " not a recognised queue object." );
384 #Can't create a ticket without a queue.
385 unless ( defined($QueueObj) && $QueueObj->Id ) {
386 $RT::Logger->debug("$self No queue given for ticket creation.");
387 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
390 #Now that we have a queue, Check the ACLS
392 $self->CurrentUser->HasRight(
393 Right => 'CreateTicket',
401 "No permission to create tickets in the queue '[_1]'",
407 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
408 return ( 0, 0, $self->loc('Invalid value for status') );
411 #Since we have a queue, we can set queue defaults
414 # If there's no queue default initial priority and it's not set, set it to 0
415 $args{'InitialPriority'} = ( $QueueObj->InitialPriority || 0 )
416 unless ( $args{'InitialPriority'} );
420 # If there's no queue default final priority and it's not set, set it to 0
421 $args{'FinalPriority'} = ( $QueueObj->FinalPriority || 0 )
422 unless ( $args{'FinalPriority'} );
424 # Priority may have changed from InitialPriority, for the case
425 # where we're importing tickets (eg, from an older RT version.)
426 my $priority = $args{'Priority'} || $args{'InitialPriority'};
429 #TODO we should see what sort of due date we're getting, rather +
430 # than assuming it's in ISO format.
432 #Set the due date. if we didn't get fed one, use the queue default due in
433 my $Due = new RT::Date( $self->CurrentUser );
435 if ( $args{'Due'} ) {
436 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
438 elsif ( $QueueObj->DefaultDueIn ) {
440 $Due->AddDays( $QueueObj->DefaultDueIn );
443 my $Starts = new RT::Date( $self->CurrentUser );
444 if ( defined $args{'Starts'} ) {
445 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
448 my $Started = new RT::Date( $self->CurrentUser );
449 if ( defined $args{'Started'} ) {
450 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
453 my $Resolved = new RT::Date( $self->CurrentUser );
454 if ( defined $args{'Resolved'} ) {
455 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
458 #If the status is an inactive status, set the resolved date
459 if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} )
461 $RT::Logger->debug( "Got a "
463 . "ticket with a resolved of "
464 . $args{'Resolved'} );
470 # {{{ Dealing with time fields
472 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
473 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
474 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
478 # {{{ Deal with setting the owner
480 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
481 $Owner = $args{'Owner'};
484 #If we've been handed something else, try to load the user.
485 elsif ( $args{'Owner'} ) {
486 $Owner = RT::User->new( $self->CurrentUser );
487 $Owner->Load( $args{'Owner'} );
489 push( @non_fatal_errors,
490 $self->loc("Owner could not be set.") . " "
491 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} )
493 unless ( $Owner->Id );
496 #If we have a proposed owner and they don't have the right
497 #to own a ticket, scream about it and make them not the owner
501 and ( $Owner->Id != $RT::Nobody->Id )
511 $RT::Logger->warning( "User "
515 . "as a ticket owner but has no rights to own "
519 push @non_fatal_errors,
520 $self->loc( "Owner '[_1]' does not have rights to own this ticket.",
527 #If we haven't been handed a valid owner, make it nobody.
528 unless ( defined($Owner) && $Owner->Id ) {
529 $Owner = new RT::User( $self->CurrentUser );
530 $Owner->Load( $RT::Nobody->Id );
535 # We attempt to load or create each of the people who might have a role for this ticket
536 # _outside_ the transaction, so we don't get into ticket creation races
537 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
538 next unless ( defined $args{$type} );
539 foreach my $watcher (
540 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
542 my $user = RT::User->new($RT::SystemUser);
543 $user->LoadOrCreateByEmail($watcher)
544 if ( $watcher && $watcher !~ /^\d+$/ );
548 $RT::Handle->BeginTransaction();
551 Queue => $QueueObj->Id,
553 Subject => $args{'Subject'},
554 InitialPriority => $args{'InitialPriority'},
555 FinalPriority => $args{'FinalPriority'},
556 Priority => $priority,
557 Status => $args{'Status'},
558 TimeWorked => $args{'TimeWorked'},
559 TimeEstimated => $args{'TimeEstimated'},
560 TimeLeft => $args{'TimeLeft'},
561 Type => $args{'Type'},
562 Starts => $Starts->ISO,
563 Started => $Started->ISO,
564 Resolved => $Resolved->ISO,
568 # Parameters passed in during an import that we probably don't want to touch, otherwise
569 foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) {
570 $params{$attr} = $args{$attr} if ( $args{$attr} );
573 # Delete null integer parameters
575 qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) {
576 delete $params{$attr}
577 unless ( exists $params{$attr} && $params{$attr} );
580 # Delete the time worked if we're counting it in the transaction
581 delete $params{TimeWorked} if $args{'_RecordTransaction'};
583 my ($id,$ticket_message) = $self->SUPER::Create( %params);
585 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
586 $RT::Handle->Rollback();
588 $self->loc("Ticket could not be created due to an internal error")
592 #Set the ticket's effective ID now that we've created it.
593 my ( $val, $msg ) = $self->__Set(
594 Field => 'EffectiveId',
595 Value => ( $args{'EffectiveId'} || $id )
599 $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n");
600 $RT::Handle->Rollback();
602 $self->loc("Ticket could not be created due to an internal error")
606 my $create_groups_ret = $self->_CreateTicketGroups();
607 unless ($create_groups_ret) {
608 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
610 . ". aborting Ticket creation." );
611 $RT::Handle->Rollback();
613 $self->loc("Ticket could not be created due to an internal error")
617 # Set the owner in the Groups table
618 # We denormalize it into the Ticket table too because doing otherwise would
619 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
621 $self->OwnerGroup->_AddMember(
622 PrincipalId => $Owner->PrincipalId,
623 InsideTransaction => 1
626 # {{{ Deal with setting up watchers
628 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
629 next unless ( defined $args{$type} );
630 foreach my $watcher (
631 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
634 # If there is an empty entry in the list, let's get out of here.
635 next unless $watcher;
637 # we reason that all-digits number must be a principal id, not email
638 # this is the only way to can add
640 $field = 'PrincipalId' if $watcher =~ /^\d+$/;
644 if ( $type eq 'AdminCc' ) {
646 # Note that we're using AddWatcher, rather than _AddWatcher, as we
647 # actually _want_ that ACL check. Otherwise, random ticket creators
648 # could make themselves adminccs and maybe get ticket rights. that would
650 ( $wval, $wmsg ) = $self->AddWatcher(
657 ( $wval, $wmsg ) = $self->_AddWatcher(
664 push @non_fatal_errors, $wmsg unless ($wval);
669 # {{{ Deal with setting up links
671 foreach my $type ( keys %LINKTYPEMAP ) {
672 next unless ( defined $args{$type} );
674 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
676 my ( $wval, $wmsg ) = $self->AddLink(
677 Type => $LINKTYPEMAP{$type}->{'Type'},
678 $LINKTYPEMAP{$type}->{'Mode'} => $link,
682 push @non_fatal_errors, $wmsg unless ($wval);
688 # {{{ Add all the custom fields
690 foreach my $arg ( keys %args ) {
691 next unless ( $arg =~ /^CustomField-(\d+)$/i );
694 my $value ( ref( $args{$arg} ) ? @{ $args{$arg} } : ( $args{$arg} ) )
696 next unless ( length($value) );
697 $self->_AddCustomFieldValue(
700 RecordTransaction => 0
707 if ( $args{'_RecordTransaction'} ) {
709 # {{{ Add a transaction for the create
710 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
712 TimeTaken => $args{'TimeWorked'},
713 MIMEObj => $args{'MIMEObj'}
716 if ( $self->Id && $Trans ) {
718 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
719 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
720 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
723 $RT::Handle->Rollback();
725 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
726 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
730 "Ticket could not be created due to an internal error")
734 $RT::Handle->Commit();
735 return ( $self->Id, $TransObj->Id, $ErrStr );
741 # Not going to record a transaction
742 $RT::Handle->Commit();
743 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
744 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
745 return ( $self->Id, $0, $ErrStr );
753 # {{{ sub CreateFromEmailMessage
756 =head2 CreateFromEmailMessage { Message, Queue, ExtractActorFromHeaders }
758 This code replaces what was once a large part of the email gateway.
759 It takes an email message as a parameter, parses out the sender, subject
760 and a MIME object. It then creates a ticket based on those attributes
764 sub CreateFromEmailMessage {
766 my %args = ( Message => undef,
768 ExtractActorFromSender => undef,
788 CreateTickets uses the template as a template for an ordered set of tickets
789 to create. The basic format is as follows:
792 ===Create-Ticket: identifier
800 =head2 Acceptable fields
802 A complete list of acceptable fields for this beastie:
805 * Queue => Name or id# of a queue
806 Subject => A text string
807 Status => A valid status. defaults to 'new'
809 Due => Dates can be specified in seconds since the epoch
810 to be handled literally or in a semi-free textual
811 format which RT will attempt to parse.
815 Owner => Username or id of an RT user who can and should own
817 + Requestor => Email address
818 + Cc => Email address
819 + AdminCc => Email address
832 Content => content. Can extend to multiple lines. Everything
833 within a template after a Content: header is treated
834 as content until we hit a line containing only
836 ContentType => the content-type of the Content field
837 CustomField-<id#> => custom field value
839 Fields marked with an * are required.
841 Fields marked with a + man have multiple values, simply
842 by repeating the fieldname on a new line with an additional value.
845 When parsed, field names are converted to lowercase and have -s stripped.
846 Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all
847 be treated as the same thing.
865 my %args = $self->_Parse822HeadersForAttributes($content);
867 # Now we have a %args to work with.
868 # Make sure we have at least the minimum set of
869 # reasonable data and do our thang
870 my $ticket = RT::Ticket->new($RT::SystemUser);
873 Queue => $args{'queue'},
874 Subject => $args{'subject'},
875 Status => $args{'status'},
877 Starts => $args{'starts'},
878 Started => $args{'started'},
879 Resolved => $args{'resolved'},
880 Owner => $args{'owner'},
881 Requestor => $args{'requestor'},
883 AdminCc => $args{'admincc'},
884 TimeWorked => $args{'timeworked'},
885 TimeEstimated => $args{'timeestimated'},
886 TimeLeft => $args{'timeleft'},
887 InitialPriority => $args{'initialpriority'},
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 # Add custom field entries to %ticketargs.
900 # TODO: allow named custom fields
902 /^customfield-(\d+)$/
903 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
906 my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs);
908 $RT::Logger->error( "Couldn't create a related ticket for "
909 . $self->TicketObj->Id . " "
920 =head2 UpdateFrom822 $MESSAGE
922 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
923 Returns an um. ask me again when the code exists
928 my $simple_update = <<EOF;
930 AddRequestor: jesse\@example.com
933 my $ticket = RT::Ticket->new($RT::SystemUser);
934 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
935 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
936 $ticket->UpdateFrom822($simple_update);
937 is($ticket->Subject, 'target', "changed the subject");
938 my $jesse = RT::User->new($RT::SystemUser);
939 $jesse->LoadByEmail('jesse@example.com');
940 ok ($jesse->Id, "There's a user for jesse");
941 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
951 my %args = $self->_Parse822HeadersForAttributes($content);
955 Queue => $args{'queue'},
956 Subject => $args{'subject'},
957 Status => $args{'status'},
959 Starts => $args{'starts'},
960 Started => $args{'started'},
961 Resolved => $args{'resolved'},
962 Owner => $args{'owner'},
963 Requestor => $args{'requestor'},
965 AdminCc => $args{'admincc'},
966 TimeWorked => $args{'timeworked'},
967 TimeEstimated => $args{'timeestimated'},
968 TimeLeft => $args{'timeleft'},
969 InitialPriority => $args{'initialpriority'},
970 Priority => $args{'priority'},
971 FinalPriority => $args{'finalpriority'},
972 Type => $args{'type'},
973 DependsOn => $args{'dependson'},
974 DependedOnBy => $args{'dependedonby'},
975 RefersTo => $args{'refersto'},
976 ReferredToBy => $args{'referredtoby'},
977 Members => $args{'members'},
978 MemberOf => $args{'memberof'},
979 MIMEObj => $args{'mimeobj'}
982 foreach my $type qw(Requestor Cc Admincc) {
984 foreach my $action ( 'Add', 'Del', '' ) {
986 my $lctag = lc($action) . lc($type);
987 foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
989 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
990 push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
995 # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
1000 # Add custom field entries to %ticketargs.
1001 # TODO: allow named custom fields
1003 /^customfield-(\d+)$/
1004 && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
1007 # for each ticket we've been told to update, iterate through the set of
1008 # rfc822 headers and perform that update to the ticket.
1011 # {{{ Set basic fields
1025 # Resolve the queue from a name to a numeric id.
1026 if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
1027 my $tempqueue = RT::Queue->new($RT::SystemUser);
1028 $tempqueue->Load( $ticketargs{'Queue'} );
1029 $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
1032 # die "updaterecordobject is a webui thingy";
1035 foreach my $attribute (@attribs) {
1036 my $value = $ticketargs{$attribute};
1038 if ( $value ne $self->$attribute() ) {
1040 my $method = "Set$attribute";
1041 my ( $code, $msg ) = $self->$method($value);
1043 push @results, $self->loc($attribute) . ': ' . $msg;
1048 # We special case owner changing, so we can use ForceOwnerChange
1049 if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
1050 my $ChownType = "Give";
1051 $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
1053 my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
1054 push ( @results, $msg );
1058 # Deal with setting watchers
1061 # Acceptable arguments:
1068 foreach my $type qw(Requestor Cc AdminCc) {
1070 # If we've been given a number of delresses to del, do it.
1071 foreach my $address (@{$ticketargs{'Del'.$type}}) {
1072 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
1073 push (@results, $msg) ;
1076 # If we've been given a number of addresses to add, do it.
1077 foreach my $address (@{$ticketargs{'Add'.$type}}) {
1078 $RT::Logger->debug("Adding $address as a $type");
1079 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
1080 push (@results, $msg) ;
1091 # {{{ _Parse822HeadersForAttributes Content
1093 =head2 _Parse822HeadersForAttributes Content
1095 Takes an RFC822 style message and parses its attributes into a hash.
1099 sub _Parse822HeadersForAttributes {
1101 my $content = shift;
1104 my @lines = ( split ( /\n/, $content ) );
1105 while ( defined( my $line = shift @lines ) ) {
1106 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1111 if ( defined( $args{$tag} ) )
1112 { #if we're about to get a second value, make it an array
1113 $args{$tag} = [ $args{$tag} ];
1115 if ( ref( $args{$tag} ) )
1116 { #If it's an array, we want to push the value
1117 push @{ $args{$tag} }, $value;
1119 else { #if there's nothing there, just set the value
1120 $args{$tag} = $value;
1122 } elsif ($line =~ /^$/) {
1124 #TODO: this won't work, since "" isn't of the form "foo:value"
1126 while ( defined( my $l = shift @lines ) ) {
1127 push @{ $args{'content'} }, $l;
1133 foreach my $date qw(due starts started resolved) {
1134 my $dateobj = RT::Date->new($RT::SystemUser);
1135 if ( $args{$date} =~ /^\d+$/ ) {
1136 $dateobj->Set( Format => 'unix', Value => $args{$date} );
1139 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1141 $args{$date} = $dateobj->ISO;
1143 $args{'mimeobj'} = MIME::Entity->new();
1144 $args{'mimeobj'}->build(
1145 Type => ( $args{'contenttype'} || 'text/plain' ),
1146 Data => ($args{'content'} || '')
1156 =head2 Import PARAMHASH
1159 Doesn\'t create a transaction.
1160 Doesn\'t supply queue defaults, etc.
1168 my ( $ErrStr, $QueueObj, $Owner );
1172 EffectiveId => undef,
1176 Owner => $RT::Nobody->Id,
1177 Subject => '[no subject]',
1178 InitialPriority => undef,
1179 FinalPriority => undef,
1190 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1191 $QueueObj = RT::Queue->new($RT::SystemUser);
1192 $QueueObj->Load( $args{'Queue'} );
1194 #TODO error check this and return 0 if it\'s not loading properly +++
1196 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1197 $QueueObj = RT::Queue->new($RT::SystemUser);
1198 $QueueObj->Load( $args{'Queue'}->Id );
1202 "$self " . $args{'Queue'} . " not a recognised queue object." );
1205 #Can't create a ticket without a queue.
1206 unless ( defined($QueueObj) and $QueueObj->Id ) {
1207 $RT::Logger->debug("$self No queue given for ticket creation.");
1208 return ( 0, $self->loc('Could not create ticket. Queue not set') );
1211 #Now that we have a queue, Check the ACLS
1213 $self->CurrentUser->HasRight(
1214 Right => 'CreateTicket',
1220 $self->loc("No permission to create tickets in the queue '[_1]'"
1221 , $QueueObj->Name));
1224 # {{{ Deal with setting the owner
1226 # Attempt to take user object, user name or user id.
1227 # Assign to nobody if lookup fails.
1228 if ( defined( $args{'Owner'} ) ) {
1229 if ( ref( $args{'Owner'} ) ) {
1230 $Owner = $args{'Owner'};
1233 $Owner = new RT::User( $self->CurrentUser );
1234 $Owner->Load( $args{'Owner'} );
1235 if ( !defined( $Owner->id ) ) {
1236 $Owner->Load( $RT::Nobody->id );
1241 #If we have a proposed owner and they don't have the right
1242 #to own a ticket, scream about it and make them not the owner
1245 and ( $Owner->Id != $RT::Nobody->Id )
1248 Object => $QueueObj,
1249 Right => 'OwnTicket'
1255 $RT::Logger->warning( "$self user "
1256 . $Owner->Name . "("
1259 . "as a ticket owner but has no rights to own "
1261 . $QueueObj->Name . "'\n" );
1266 #If we haven't been handed a valid owner, make it nobody.
1267 unless ( defined($Owner) ) {
1268 $Owner = new RT::User( $self->CurrentUser );
1269 $Owner->Load( $RT::Nobody->UserObj->Id );
1274 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1275 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1278 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
1279 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
1280 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
1281 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
1283 # If we're coming in with an id, set that now.
1284 my $EffectiveId = undef;
1285 if ( $args{'id'} ) {
1286 $EffectiveId = $args{'id'};
1290 my $id = $self->SUPER::Create(
1292 EffectiveId => $EffectiveId,
1293 Queue => $QueueObj->Id,
1294 Owner => $Owner->Id,
1295 Subject => $args{'Subject'}, # loc
1296 InitialPriority => $args{'InitialPriority'}, # loc
1297 FinalPriority => $args{'FinalPriority'}, # loc
1298 Priority => $args{'InitialPriority'}, # loc
1299 Status => $args{'Status'}, # loc
1300 TimeWorked => $args{'TimeWorked'}, # loc
1301 Type => $args{'Type'}, # loc
1302 Created => $args{'Created'}, # loc
1303 Told => $args{'Told'}, # loc
1304 LastUpdated => $args{'Updated'}, # loc
1305 Resolved => $args{'Resolved'}, # loc
1306 Due => $args{'Due'}, # loc
1309 # If the ticket didn't have an id
1310 # Set the ticket's effective ID now that we've created it.
1311 if ( $args{'id'} ) {
1312 $self->Load( $args{'id'} );
1316 $self->__Set( Field => 'EffectiveId', Value => $id );
1320 $self . "->Import couldn't set EffectiveId: $msg\n" );
1325 foreach $watcher ( @{ $args{'Cc'} } ) {
1326 $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 );
1328 foreach $watcher ( @{ $args{'AdminCc'} } ) {
1329 $self->_AddWatcher( Type => 'AdminCc', Person => $watcher,
1332 foreach $watcher ( @{ $args{'Requestor'} } ) {
1333 $self->_AddWatcher( Type => 'Requestor', Person => $watcher,
1337 return ( $self->Id, $ErrStr );
1343 # {{{ Routines dealing with watchers.
1345 # {{{ _CreateTicketGroups
1347 =head2 _CreateTicketGroups
1349 Create the ticket groups and links for this ticket.
1350 This routine expects to be called from Ticket->Create _inside of a transaction_
1352 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1354 It will return true on success and undef on failure.
1358 my $ticket = RT::Ticket->new($RT::SystemUser);
1359 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1360 Owner => $RT::SystemUser->Id,
1362 Requestor => ['jesse@example.com'],
1365 ok ($id, "Ticket $id was created");
1366 ok(my $group = RT::Group->new($RT::SystemUser));
1367 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1368 ok ($group->Id, "Found the requestors object for this ticket");
1370 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1371 $jesse->LoadByEmail('jesse@example.com');
1372 ok($jesse->Id, "Found the jesse rt user");
1375 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1376 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1377 ok ($add_id, "Add succeeded: ($add_msg)");
1378 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1379 $bob->LoadByEmail('bob@fsck.com');
1380 ok($bob->Id, "Found the bob rt user");
1381 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1382 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1383 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1386 $group = RT::Group->new($RT::SystemUser);
1387 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1388 ok ($group->Id, "Found the cc object for this ticket");
1389 $group = RT::Group->new($RT::SystemUser);
1390 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1391 ok ($group->Id, "Found the AdminCc object for this ticket");
1392 $group = RT::Group->new($RT::SystemUser);
1393 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1394 ok ($group->Id, "Found the Owner object for this ticket");
1395 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1402 sub _CreateTicketGroups {
1405 my @types = qw(Requestor Owner Cc AdminCc);
1407 foreach my $type (@types) {
1408 my $type_obj = RT::Group->new($self->CurrentUser);
1409 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1410 Instance => $self->Id,
1413 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1414 $self->Id.": ".$msg);
1424 # {{{ sub OwnerGroup
1428 A constructor which returns an RT::Group object containing the owner of this ticket.
1434 my $owner_obj = RT::Group->new($self->CurrentUser);
1435 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1436 return ($owner_obj);
1442 # {{{ sub AddWatcher
1446 AddWatcher takes a parameter hash. The keys are as follows:
1448 Type One of Requestor, Cc, AdminCc
1450 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1452 Email The email address of the new watcher. If a user with this
1453 email address can't be found, a new nonprivileged user will be created.
1455 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.
1463 PrincipalId => undef,
1469 #If the watcher we're trying to add is for the current user
1470 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) {
1471 # If it's an AdminCc and they don't have
1472 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1473 if ( $args{'Type'} eq 'AdminCc' ) {
1474 unless ( $self->CurrentUserHasRight('ModifyTicket')
1475 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1476 return ( 0, $self->loc('Permission Denied'))
1480 # If it's a Requestor or Cc and they don't have
1481 # 'Watch' or 'ModifyTicket', bail
1482 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1484 unless ( $self->CurrentUserHasRight('ModifyTicket')
1485 or $self->CurrentUserHasRight('Watch') ) {
1486 return ( 0, $self->loc('Permission Denied'))
1490 $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type");
1491 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1495 # If the watcher isn't the current user
1496 # and the current user doesn't have 'ModifyTicket'
1499 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1500 return ( 0, $self->loc("Permission Denied") );
1506 return ( $self->_AddWatcher(%args) );
1509 #This contains the meat of AddWatcher. but can be called from a routine like
1510 # Create, which doesn't need the additional acl check
1516 PrincipalId => undef,
1522 my $principal = RT::Principal->new($self->CurrentUser);
1523 if ($args{'Email'}) {
1524 my $user = RT::User->new($RT::SystemUser);
1525 my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1527 $args{'PrincipalId'} = $pid;
1530 if ($args{'PrincipalId'}) {
1531 $principal->Load($args{'PrincipalId'});
1535 # If we can't find this watcher, we need to bail.
1536 unless ($principal->Id) {
1537 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1538 return(0, $self->loc("Could not find or create that user"));
1542 my $group = RT::Group->new($self->CurrentUser);
1543 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1544 unless ($group->id) {
1545 return(0,$self->loc("Group not found"));
1548 if ( $group->HasMember( $principal)) {
1550 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1554 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1555 InsideTransaction => 1 );
1557 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1559 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1562 unless ( $args{'Silent'} ) {
1563 $self->_NewTransaction(
1564 Type => 'AddWatcher',
1565 NewValue => $principal->Id,
1566 Field => $args{'Type'}
1570 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1576 # {{{ sub DeleteWatcher
1578 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1581 Deletes a Ticket watcher. Takes two arguments:
1583 Type (one of Requestor,Cc,AdminCc)
1587 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1589 Email (the email address of an existing wathcer)
1598 my %args = ( Type => undef,
1599 PrincipalId => undef,
1603 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1604 return ( 0, $self->loc("No principal specified") );
1606 my $principal = RT::Principal->new( $self->CurrentUser );
1607 if ( $args{'PrincipalId'} ) {
1609 $principal->Load( $args{'PrincipalId'} );
1612 my $user = RT::User->new( $self->CurrentUser );
1613 $user->LoadByEmail( $args{'Email'} );
1614 $principal->Load( $user->Id );
1617 # If we can't find this watcher, we need to bail.
1618 unless ( $principal->Id ) {
1619 return ( 0, $self->loc("Could not find that principal") );
1622 my $group = RT::Group->new( $self->CurrentUser );
1623 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1624 unless ( $group->id ) {
1625 return ( 0, $self->loc("Group not found") );
1629 #If the watcher we're trying to add is for the current user
1630 if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) {
1632 # If it's an AdminCc and they don't have
1633 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1634 if ( $args{'Type'} eq 'AdminCc' ) {
1635 unless ( $self->CurrentUserHasRight('ModifyTicket')
1636 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1637 return ( 0, $self->loc('Permission Denied') );
1641 # If it's a Requestor or Cc and they don't have
1642 # 'Watch' or 'ModifyTicket', bail
1643 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1645 unless ( $self->CurrentUserHasRight('ModifyTicket')
1646 or $self->CurrentUserHasRight('Watch') ) {
1647 return ( 0, $self->loc('Permission Denied') );
1651 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1653 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1657 # If the watcher isn't the current user
1658 # and the current user doesn't have 'ModifyTicket' bail
1660 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1661 return ( 0, $self->loc("Permission Denied") );
1667 # see if this user is already a watcher.
1669 unless ( $group->HasMember($principal) ) {
1671 $self->loc( 'That principal is not a [_1] for this ticket',
1675 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1677 $RT::Logger->error( "Failed to delete "
1679 . " as a member of group "
1685 'Could not remove that principal as a [_1] for this ticket',
1689 unless ( $args{'Silent'} ) {
1690 $self->_NewTransaction( Type => 'DelWatcher',
1691 OldValue => $principal->Id,
1692 Field => $args{'Type'} );
1696 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1697 $principal->Object->Name,
1706 =head2 SquelchMailTo [EMAIL]
1708 Takes an optional email address to never email about updates to this ticket.
1711 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1715 my $t = RT::Ticket->new($RT::SystemUser);
1716 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1718 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1720 my @returned = $t->SquelchMailTo('nobody@example.com');
1722 is($#returned, 0, "The ticket has one squelched recipients");
1724 my @names = $t->Attributes->Names;
1725 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1726 @returned = $t->SquelchMailTo('nobody@example.com');
1729 is($#returned, 0, "The ticket has one squelched recipients");
1731 @names = $t->Attributes->Names;
1732 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1735 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1736 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1737 @returned = $t->SquelchMailTo();
1738 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1748 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1752 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1753 unless grep { $_->Content eq $attr }
1754 $self->Attributes->Named('SquelchMailTo');
1757 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1760 my @attributes = $self->Attributes->Named('SquelchMailTo');
1761 return (@attributes);
1765 =head2 UnsquelchMailTo ADDRESS
1767 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1769 Returns a tuple of (status, message)
1773 sub UnsquelchMailTo {
1776 my $address = shift;
1777 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1778 return ( 0, $self->loc("Permission Denied") );
1781 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1782 return ($val, $msg);
1786 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1788 =head2 RequestorAddresses
1790 B<Returns> String: All Ticket Requestor email addresses as a string.
1794 sub RequestorAddresses {
1797 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1801 return ( $self->Requestors->MemberEmailAddressesAsString );
1805 =head2 AdminCcAddresses
1807 returns String: All Ticket AdminCc email addresses as a string
1811 sub AdminCcAddresses {
1814 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1818 return ( $self->AdminCc->MemberEmailAddressesAsString )
1824 returns String: All Ticket Ccs as a string of email addresses
1831 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1835 return ( $self->Cc->MemberEmailAddressesAsString);
1841 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1843 # {{{ sub Requestors
1848 Returns this ticket's Requestors as an RT::Group object
1855 my $group = RT::Group->new($self->CurrentUser);
1856 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1857 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1870 Returns an RT::Group object which contains this ticket's Ccs.
1871 If the user doesn't have "ShowTicket" permission, returns an empty group
1878 my $group = RT::Group->new($self->CurrentUser);
1879 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1880 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1893 Returns an RT::Group object which contains this ticket's AdminCcs.
1894 If the user doesn't have "ShowTicket" permission, returns an empty group
1901 my $group = RT::Group->new($self->CurrentUser);
1902 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1903 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1913 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1916 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1918 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1920 Takes a param hash with the attributes Type and either PrincipalId or Email
1922 Type is one of Requestor, Cc, AdminCc and Owner
1924 PrincipalId is an RT::Principal id, and Email is an email address.
1926 Returns true if the specified principal (or the one corresponding to the
1927 specified address) is a member of the group Type for this ticket.
1929 XX TODO: This should be Memoized.
1936 my %args = ( Type => 'Requestor',
1937 PrincipalId => undef,
1942 # Load the relevant group.
1943 my $group = RT::Group->new($self->CurrentUser);
1944 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1946 # Find the relevant principal.
1947 my $principal = RT::Principal->new($self->CurrentUser);
1948 if (!$args{PrincipalId} && $args{Email}) {
1949 # Look up the specified user.
1950 my $user = RT::User->new($self->CurrentUser);
1951 $user->LoadByEmail($args{Email});
1953 $args{PrincipalId} = $user->PrincipalId;
1956 # A non-existent user can't be a group member.
1960 $principal->Load($args{'PrincipalId'});
1962 # Ask if it has the member in question
1963 return ($group->HasMember($principal));
1968 # {{{ sub IsRequestor
1970 =head2 IsRequestor PRINCIPAL_ID
1972 Takes an RT::Principal id
1973 Returns true if the principal is a requestor of the current ticket.
1982 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1990 =head2 IsCc PRINCIPAL_ID
1992 Takes an RT::Principal id.
1993 Returns true if the principal is a requestor of the current ticket.
2002 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
2010 =head2 IsAdminCc PRINCIPAL_ID
2012 Takes an RT::Principal id.
2013 Returns true if the principal is a requestor of the current ticket.
2021 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
2031 Takes an RT::User object. Returns true if that user is this ticket's owner.
2032 returns undef otherwise
2040 # no ACL check since this is used in acl decisions
2041 # unless ($self->CurrentUserHasRight('ShowTicket')) {
2045 #Tickets won't yet have owners when they're being created.
2046 unless ( $self->OwnerObj->id ) {
2050 if ( $person->id == $self->OwnerObj->id ) {
2064 # {{{ Routines dealing with queues
2066 # {{{ sub ValidateQueue
2073 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
2077 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2078 my $id = $QueueObj->Load($Value);
2094 my $NewQueue = shift;
2096 #Redundant. ACL gets checked in _Set;
2097 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2098 return ( 0, $self->loc("Permission Denied") );
2101 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2102 $NewQueueObj->Load($NewQueue);
2104 unless ( $NewQueueObj->Id() ) {
2105 return ( 0, $self->loc("That queue does not exist") );
2108 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2109 return ( 0, $self->loc('That is the same value') );
2112 $self->CurrentUser->HasRight(
2113 Right => 'CreateTicket',
2114 Object => $NewQueueObj
2118 return ( 0, $self->loc("You may not create requests in that queue.") );
2122 $self->OwnerObj->HasRight(
2123 Right => 'OwnTicket',
2124 Object => $NewQueueObj
2131 return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2141 Takes nothing. returns this ticket's queue object
2148 my $queue_obj = RT::Queue->new( $self->CurrentUser );
2150 #We call __Value so that we can avoid the ACL decision and some deep recursion
2151 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2152 return ($queue_obj);
2159 # {{{ Date printing routines
2165 Returns an RT::Date object containing this ticket's due date
2172 my $time = new RT::Date( $self->CurrentUser );
2174 # -1 is RT::Date slang for never
2176 $time->Set( Format => 'sql', Value => $self->Due );
2179 $time->Set( Format => 'unix', Value => -1 );
2187 # {{{ sub DueAsString
2191 Returns this ticket's due date as a human readable string
2197 return $self->DueObj->AsString();
2202 # {{{ sub ResolvedObj
2206 Returns an RT::Date object of this ticket's 'resolved' time.
2213 my $time = new RT::Date( $self->CurrentUser );
2214 $time->Set( Format => 'sql', Value => $self->Resolved );
2220 # {{{ sub SetStarted
2224 Takes a date in ISO format or undef
2225 Returns a transaction id and a message
2226 The client calls "Start" to note that the project was started on the date in $date.
2227 A null date means "now"
2233 my $time = shift || 0;
2235 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2236 return ( 0, self->loc("Permission Denied") );
2239 #We create a date object to catch date weirdness
2240 my $time_obj = new RT::Date( $self->CurrentUser() );
2242 $time_obj->Set( Format => 'ISO', Value => $time );
2245 $time_obj->SetToNow();
2248 #Now that we're starting, open this ticket
2249 #TODO do we really want to force this as policy? it should be a scrip
2251 #We need $TicketAsSystem, in case the current user doesn't have
2254 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2255 $TicketAsSystem->Load( $self->Id );
2256 if ( $TicketAsSystem->Status eq 'new' ) {
2257 $TicketAsSystem->Open();
2260 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2266 # {{{ sub StartedObj
2270 Returns an RT::Date object which contains this ticket's
2278 my $time = new RT::Date( $self->CurrentUser );
2279 $time->Set( Format => 'sql', Value => $self->Started );
2289 Returns an RT::Date object which contains this ticket's
2297 my $time = new RT::Date( $self->CurrentUser );
2298 $time->Set( Format => 'sql', Value => $self->Starts );
2308 Returns an RT::Date object which contains this ticket's
2316 my $time = new RT::Date( $self->CurrentUser );
2317 $time->Set( Format => 'sql', Value => $self->Told );
2323 # {{{ sub ToldAsString
2327 A convenience method that returns ToldObj->AsString
2329 TODO: This should be deprecated
2335 if ( $self->Told ) {
2336 return $self->ToldObj->AsString();
2345 # {{{ sub TimeWorkedAsString
2347 =head2 TimeWorkedAsString
2349 Returns the amount of time worked on this ticket as a Text String
2353 sub TimeWorkedAsString {
2355 return "0" unless $self->TimeWorked;
2357 #This is not really a date object, but if we diff a number of seconds
2358 #vs the epoch, we'll get a nice description of time worked.
2360 my $worked = new RT::Date( $self->CurrentUser );
2362 #return the #of minutes worked turned into seconds and written as
2363 # a simple text string
2365 return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2372 # {{{ Routines dealing with correspondence/comments
2378 Comment on this ticket.
2379 Takes a hashref with the following attributes:
2380 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2383 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2385 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2386 They will, however, be prepared and you'll be able to access them through the TransactionObj
2394 my %args = ( CcMessageTo => undef,
2395 BccMessageTo => undef,
2402 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2403 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2404 return ( 0, $self->loc("Permission Denied"), undef );
2406 $args{'NoteType'} = 'Comment';
2408 if ($args{'DryRun'}) {
2409 $RT::Handle->BeginTransaction();
2410 $args{'CommitScrips'} = 0;
2413 my @results = $self->_RecordNote(%args);
2414 if ($args{'DryRun'}) {
2415 $RT::Handle->Rollback();
2422 # {{{ sub Correspond
2426 Correspond on this ticket.
2427 Takes a hashref with the following attributes:
2430 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2432 if there's no MIMEObj, Content is used to build a MIME::Entity object
2434 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2435 They will, however, be prepared and you'll be able to access them through the TransactionObj
2442 my %args = ( CcMessageTo => undef,
2443 BccMessageTo => undef,
2449 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2450 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2451 return ( 0, $self->loc("Permission Denied"), undef );
2454 $args{'NoteType'} = 'Correspond';
2455 if ($args{'DryRun'}) {
2456 $RT::Handle->BeginTransaction();
2457 $args{'CommitScrips'} = 0;
2460 my @results = $self->_RecordNote(%args);
2462 #Set the last told date to now if this isn't mail from the requestor.
2463 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2464 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2466 if ($args{'DryRun'}) {
2467 $RT::Handle->Rollback();
2476 # {{{ sub _RecordNote
2480 the meat of both comment and correspond.
2482 Performs no access control checks. hence, dangerous.
2489 my %args = ( CcMessageTo => undef,
2490 BccMessageTo => undef,
2497 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2498 return ( 0, $self->loc("No message attached"), undef );
2500 unless ( $args{'MIMEObj'} ) {
2501 $args{'MIMEObj'} = MIME::Entity->build( Data => (
2502 ref $args{'Content'}
2504 : [ $args{'Content'} ]
2508 # convert text parts into utf-8
2509 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2511 # If we've been passed in CcMessageTo and BccMessageTo fields,
2512 # add them to the mime object for passing on to the transaction handler
2513 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2516 $args{'MIMEObj'}->head->add( 'RT-Send-Cc', RT::User::CanonicalizeEmailAddress(
2517 undef, $args{'CcMessageTo'}
2519 if defined $args{'CcMessageTo'};
2520 $args{'MIMEObj'}->head->add( 'RT-Send-Bcc',
2521 RT::User::CanonicalizeEmailAddress(
2522 undef, $args{'BccMessageTo'}
2524 if defined $args{'BccMessageTo'};
2526 #Record the correspondence (write the transaction)
2527 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2528 Type => $args{'NoteType'},
2529 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2530 TimeTaken => $args{'TimeTaken'},
2531 MIMEObj => $args{'MIMEObj'},
2532 CommitScrips => $args{'CommitScrips'},
2536 $RT::Logger->err("$self couldn't init a transaction $msg");
2537 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2540 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2552 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2555 my $type = shift || "";
2557 unless ( $self->{"$field$type"} ) {
2558 $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2559 if ( $self->CurrentUserHasRight('ShowTicket') ) {
2560 # Maybe this ticket is a merged ticket
2561 my $Tickets = new RT::Tickets( $self->CurrentUser );
2562 # at least to myself
2563 $self->{"$field$type"}->Limit( FIELD => $field,
2564 VALUE => $self->URI,
2565 ENTRYAGGREGATOR => 'OR' );
2566 $Tickets->Limit( FIELD => 'EffectiveId',
2567 VALUE => $self->EffectiveId );
2568 while (my $Ticket = $Tickets->Next) {
2569 $self->{"$field$type"}->Limit( FIELD => $field,
2570 VALUE => $Ticket->URI,
2571 ENTRYAGGREGATOR => 'OR' );
2573 $self->{"$field$type"}->Limit( FIELD => 'Type',
2578 return ( $self->{"$field$type"} );
2583 # {{{ sub DeleteLink
2587 Delete a link. takes a paramhash of Base, Target and Type.
2588 Either Base or Target must be null. The null value will
2589 be replaced with this ticket\'s id
2603 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2604 $RT::Logger->debug("No permission to delete links\n");
2605 return ( 0, $self->loc('Permission Denied'))
2609 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2612 $RT::Logger->debug("Couldn't find that link\n");
2616 my ($direction, $remote_link);
2618 if ( $args{'Base'} ) {
2619 $remote_link = $args{'Base'};
2620 $direction = 'Target';
2622 elsif ( $args{'Target'} ) {
2623 $remote_link = $args{'Target'};
2628 my $remote_uri = RT::URI->new( $RT::SystemUser );
2629 $remote_uri->FromURI( $remote_link );
2631 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2632 Type => 'DeleteLink',
2633 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2634 OldValue => $remote_uri->URI || $remote_link,
2638 return ( $Trans, $Msg );
2648 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2655 my %args = ( Target => '',
2662 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2663 return ( 0, $self->loc("Permission Denied") );
2666 my ($val, $Msg) = $self->SUPER::_AddLink(%args);
2669 return ($val, $Msg);
2672 my ($direction, $remote_link);
2673 if ( $args{'Target'} ) {
2674 $remote_link = $args{'Target'};
2675 $direction = 'Base';
2676 } elsif ( $args{'Base'} ) {
2677 $remote_link = $args{'Base'};
2678 $direction = 'Target';
2681 # Don't write the transaction if we're doing this on create
2682 if ( $args{'Silent'} ) {
2686 my $remote_uri = RT::URI->new( $RT::SystemUser );
2687 $remote_uri->FromURI( $remote_link );
2689 #Write the transaction
2690 my ( $Trans, $Msg, $TransObj ) =
2691 $self->_NewTransaction(Type => 'AddLink',
2692 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2693 NewValue => $remote_uri->URI || $remote_link,
2695 return ( $Trans, $Msg );
2705 MergeInto take the id of the ticket to merge this ticket into.
2711 my $MergeInto = shift;
2713 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2714 return ( 0, $self->loc("Permission Denied") );
2717 # Load up the new ticket.
2718 my $NewTicket = RT::Ticket->new($RT::SystemUser);
2719 $NewTicket->Load($MergeInto);
2721 # make sure it exists.
2722 unless ( defined $NewTicket->Id ) {
2723 return ( 0, $self->loc("New ticket doesn't exist") );
2726 # Make sure the current user can modify the new ticket.
2727 unless ( $NewTicket->CurrentUserHasRight('ModifyTicket') ) {
2728 $RT::Logger->debug("failed...");
2729 return ( 0, $self->loc("Permission Denied") );
2733 "checking if the new ticket has the same id and effective id...");
2734 unless ( $NewTicket->id == $NewTicket->EffectiveId ) {
2735 $RT::Logger->err( "$self trying to merge into "
2737 . " which is itself merged.\n" );
2739 $self->loc("Can't merge into a merged ticket. You should never get this error") );
2742 # We use EffectiveId here even though it duplicates information from
2743 # the links table becasue of the massive performance hit we'd take
2744 # by trying to do a separate database query for merge info everytime
2747 #update this ticket's effective id to the new ticket's id.
2748 my ( $id_val, $id_msg ) = $self->__Set(
2749 Field => 'EffectiveId',
2750 Value => $NewTicket->Id()
2755 "Couldn't set effective ID for " . $self->Id . ": $id_msg" );
2756 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2759 my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved');
2761 unless ($status_val) {
2762 $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) );
2766 # update all the links that point to that old ticket
2767 my $old_links_to = RT::Links->new($self->CurrentUser);
2768 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2770 while (my $link = $old_links_to->Next) {
2771 if ($link->Base eq $NewTicket->URI) {
2774 $link->SetTarget($NewTicket->URI);
2779 my $old_links_from = RT::Links->new($self->CurrentUser);
2780 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2782 while (my $link = $old_links_from->Next) {
2783 if ($link->Target eq $NewTicket->URI) {
2786 $link->SetBase($NewTicket->URI);
2791 # Update time fields
2792 $NewTicket->SetTimeEstimated(($NewTicket->TimeEstimated || 0) + ($self->TimeEstimated || 0));
2793 $NewTicket->SetTimeWorked( ($NewTicket->TimeWorked || 0) + ($self->TimeWorked || 0));
2794 $NewTicket->SetTimeLeft( ($NewTicket->TimeLeft || 0) + ($self->TimeLeft || 0));
2796 #add all of this ticket's watchers to that ticket.
2797 my $requestors = $self->Requestors->MembersObj;
2798 while (my $watcher = $requestors->Next) {
2799 $NewTicket->_AddWatcher( Type => 'Requestor',
2801 PrincipalId => $watcher->MemberId);
2804 my $Ccs = $self->Cc->MembersObj;
2805 while (my $watcher = $Ccs->Next) {
2806 $NewTicket->_AddWatcher( Type => 'Cc',
2808 PrincipalId => $watcher->MemberId);
2811 my $AdminCcs = $self->AdminCc->MembersObj;
2812 while (my $watcher = $AdminCcs->Next) {
2813 $NewTicket->_AddWatcher( Type => 'AdminCc',
2815 PrincipalId => $watcher->MemberId);
2819 #find all of the tickets that were merged into this ticket.
2820 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2821 $old_mergees->Limit(
2822 FIELD => 'EffectiveId',
2827 # update their EffectiveId fields to the new ticket's id
2828 while ( my $ticket = $old_mergees->Next() ) {
2829 my ( $val, $msg ) = $ticket->__Set(
2830 Field => 'EffectiveId',
2831 Value => $NewTicket->Id()
2835 #make a new link: this ticket is merged into that other ticket.
2836 $self->AddLink( Type => 'MergedInto', Target => $NewTicket->Id());
2838 $NewTicket->_SetLastUpdated;
2840 return ( 1, $self->loc("Merge Successful") );
2847 # {{{ Routines dealing with ownership
2853 Takes nothing and returns an RT::User object of
2861 #If this gets ACLed, we lose on a rights check in User.pm and
2862 #get deep recursion. if we need ACLs here, we need
2863 #an equiv without ACLs
2865 my $owner = new RT::User( $self->CurrentUser );
2866 $owner->Load( $self->__Value('Owner') );
2868 #Return the owner object
2874 # {{{ sub OwnerAsString
2876 =head2 OwnerAsString
2878 Returns the owner's email address
2884 return ( $self->OwnerObj->EmailAddress );
2894 Takes two arguments:
2895 the Id or Name of the owner
2896 and (optionally) the type of the SetOwner Transaction. It defaults
2897 to 'Give'. 'Steal' is also a valid option.
2901 my $root = RT::User->new($RT::SystemUser);
2902 $root->Load('root');
2903 ok ($root->Id, "Loaded the root user");
2904 my $t = RT::Ticket->new($RT::SystemUser);
2906 $t->SetOwner('root');
2907 ok ($t->OwnerObj->Name eq 'root' , "Root owns the ticket");
2909 ok ($t->OwnerObj->id eq $RT::SystemUser->id , "SystemUser owns the ticket");
2910 my $txns = RT::Transactions->new($RT::SystemUser);
2911 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
2912 $txns->Limit(FIELD => 'Ticket', VALUE => '1');
2913 my $steal = $txns->First;
2914 ok($steal->OldValue == $root->Id , "Stolen from root");
2915 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
2923 my $NewOwner = shift;
2924 my $Type = shift || "Give";
2926 # must have ModifyTicket rights
2927 # or TakeTicket/StealTicket and $NewOwner is self
2928 # see if it's a take
2929 if ( $self->OwnerObj->Id == $RT::Nobody->Id ) {
2930 unless ( $self->CurrentUserHasRight('ModifyTicket')
2931 || $self->CurrentUserHasRight('TakeTicket') ) {
2932 return ( 0, $self->loc("Permission Denied") );
2936 # see if it's a steal
2937 elsif ( $self->OwnerObj->Id != $RT::Nobody->Id
2938 && $self->OwnerObj->Id != $self->CurrentUser->id ) {
2940 unless ( $self->CurrentUserHasRight('ModifyTicket')
2941 || $self->CurrentUserHasRight('StealTicket') ) {
2942 return ( 0, $self->loc("Permission Denied") );
2946 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2947 return ( 0, $self->loc("Permission Denied") );
2950 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2951 my $OldOwnerObj = $self->OwnerObj;
2953 $NewOwnerObj->Load($NewOwner);
2954 if ( !$NewOwnerObj->Id ) {
2955 return ( 0, $self->loc("That user does not exist") );
2958 #If thie ticket has an owner and it's not the current user
2960 if ( ( $Type ne 'Steal' )
2961 and ( $Type ne 'Force' )
2962 and #If we're not stealing
2963 ( $self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
2964 ( $self->CurrentUser->Id ne $self->OwnerObj->Id() )
2965 ) { #and it's not us
2968 "You can only reassign tickets that you own or that are unowned" ) );
2971 #If we've specified a new owner and that user can't modify the ticket
2972 elsif ( ( $NewOwnerObj->Id )
2973 and ( !$NewOwnerObj->HasRight( Right => 'OwnTicket',
2976 return ( 0, $self->loc("That user may not own tickets in that queue") );
2979 #If the ticket has an owner and it's the new owner, we don't need
2981 elsif ( ( $self->OwnerObj )
2982 and ( $NewOwnerObj->Id eq $self->OwnerObj->Id ) ) {
2983 return ( 0, $self->loc("That user already owns that ticket") );
2986 $RT::Handle->BeginTransaction();
2988 # Delete the owner in the owner group, then add a new one
2989 # TODO: is this safe? it's not how we really want the API to work
2990 # for most things, but it's fast.
2991 my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
2993 $RT::Handle->Rollback();
2994 return ( 0, $self->loc("Could not change owner. ") . $del_msg );
2997 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2998 PrincipalId => $NewOwnerObj->PrincipalId,
2999 InsideTransaction => 1 );
3001 $RT::Handle->Rollback();
3002 return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3005 # We call set twice with slightly different arguments, so
3006 # as to not have an SQL transaction span two RT transactions
3008 my ( $val, $msg ) = $self->_Set(
3010 RecordTransaction => 0,
3011 Value => $NewOwnerObj->Id,
3013 TransactionType => $Type,
3014 CheckACL => 0, # don't check acl
3018 $RT::Handle->Rollback;
3019 return ( 0, $self->loc("Could not change owner. ") . $msg );
3022 $RT::Handle->Commit();
3024 my ( $trans, $msg, undef ) = $self->_NewTransaction(
3027 NewValue => $NewOwnerObj->Id,
3028 OldValue => $OldOwnerObj->Id,
3032 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3033 $OldOwnerObj->Name, $NewOwnerObj->Name );
3035 # TODO: make sure the trans committed properly
3037 return ( $trans, $msg );
3047 A convenince method to set the ticket's owner to the current user
3053 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3062 Convenience method to set the owner to 'nobody' if the current user is the owner.
3068 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3077 A convenience method to change the owner of the current ticket to the
3078 current user. Even if it's owned by another user.
3085 if ( $self->IsOwner( $self->CurrentUser ) ) {
3086 return ( 0, $self->loc("You already own this ticket") );
3089 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3099 # {{{ Routines dealing with status
3101 # {{{ sub ValidateStatus
3103 =head2 ValidateStatus STATUS
3105 Takes a string. Returns true if that status is a valid status for this ticket.
3106 Returns false otherwise.
3110 sub ValidateStatus {
3114 #Make sure the status passed in is valid
3115 unless ( $self->QueueObj->IsValidStatus($status) ) {
3127 =head2 SetStatus STATUS
3129 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3131 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.
3135 my $tt = RT::Ticket->new($RT::SystemUser);
3136 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3139 ok($tt->Status eq 'new', "New ticket is created as new");
3141 ($id, $msg) = $tt->SetStatus('open');
3143 ok ($msg =~ /open/i, "Status message is correct");
3144 ($id, $msg) = $tt->SetStatus('resolved');
3146 ok ($msg =~ /resolved/i, "Status message is correct");
3147 ($id, $msg) = $tt->SetStatus('resolved');
3161 $args{Status} = shift;
3168 if ( $args{Status} eq 'deleted') {
3169 unless ($self->CurrentUserHasRight('DeleteTicket')) {
3170 return ( 0, $self->loc('Permission Denied') );
3173 unless ($self->CurrentUserHasRight('ModifyTicket')) {
3174 return ( 0, $self->loc('Permission Denied') );
3178 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3179 return (0, $self->loc('That ticket has unresolved dependencies'));
3182 my $now = RT::Date->new( $self->CurrentUser );
3185 #If we're changing the status from new, record that we've started
3186 if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3188 #Set the Started time to "now"
3189 $self->_Set( Field => 'Started',
3191 RecordTransaction => 0 );
3194 if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) {
3196 #When we resolve a ticket, set the 'Resolved' attribute to now.
3197 $self->_Set( Field => 'Resolved',
3199 RecordTransaction => 0 );
3202 #Actually update the status
3203 my ($val, $msg)= $self->_Set( Field => 'Status',
3204 Value => $args{Status},
3206 TransactionType => 'Status' );
3217 Takes no arguments. Marks this ticket for garbage collection
3223 $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead.");
3224 return $self->Delete;
3229 return ( $self->SetStatus('deleted') );
3231 # TODO: garbage collection
3240 Sets this ticket's status to stalled
3246 return ( $self->SetStatus('stalled') );
3255 Sets this ticket's status to rejected
3261 return ( $self->SetStatus('rejected') );
3270 Sets this ticket\'s status to Open
3276 return ( $self->SetStatus('open') );
3285 Sets this ticket\'s status to Resolved
3291 return ( $self->SetStatus('resolved') );
3298 # {{{ Routines dealing with custom fields
3301 # {{{ FirstCustomFieldValue
3303 =item FirstCustomFieldValue FIELD
3305 Return the content of the first value of CustomField FIELD for this ticket
3306 Takes a field id or name
3310 sub FirstCustomFieldValue {
3313 my $values = $self->CustomFieldValues($field);
3314 if ($values->First) {
3315 return $values->First->Content;
3324 # {{{ CustomFieldValues
3326 =item CustomFieldValues FIELD
3328 Return a TicketCustomFieldValues object of all values of CustomField FIELD for this ticket.
3329 Takes a field id or name.
3334 sub CustomFieldValues {
3338 my $cf = RT::CustomField->new($self->CurrentUser);
3340 if ($field =~ /^\d+$/) {
3341 $cf->LoadById($field);
3343 $cf->LoadByNameAndQueue(Name => $field, Queue => $self->QueueObj->Id);
3345 $cf->LoadByNameAndQueue(Name => $field, Queue => '0');
3348 my $cf_values = RT::TicketCustomFieldValues->new( $self->CurrentUser );
3349 $cf_values->LimitToCustomField($cf->id) if $cf->id;
3350 $cf_values->LimitToTicket($self->Id());
3351 $cf_values->OrderBy( FIELD => 'id' );
3353 # @values is a CustomFieldValues object;
3354 return ($cf_values);
3359 # {{{ AddCustomFieldValue
3361 =item AddCustomFieldValue { Field => FIELD, Value => VALUE }
3363 VALUE should be a string.
3364 FIELD can be a CustomField object, a CustomField ID, or a CustomField Name.
3367 Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field,
3368 deletes the old value.
3369 If VALUE isn't a valid value for the custom field, returns
3370 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3374 sub AddCustomFieldValue {
3376 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3377 return ( 0, $self->loc("Permission Denied") );
3379 $self->_AddCustomFieldValue(@_);
3382 sub _AddCustomFieldValue {
3387 RecordTransaction => 1,
3391 my $cf = RT::CustomField->new( $self->CurrentUser );
3392 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3393 $cf->Load( $args{'Field'}->id );
3395 elsif ($args{'Field'} =~ /\D/) {
3396 $cf->LoadByNameAndQueue( Name => $args{'Field'}, Queue => $self->QueueObj->Id );
3399 $cf->Load( $args{'Field'} );
3402 unless ( $cf->Id ) {
3403 return ( 0, $self->loc("Custom field [_1] not found", $args{'Field'}) );
3406 # Load up a TicketCustomFieldValues object for this custom field and this ticket
3407 my $values = $cf->ValuesForTicket( $self->id );
3409 unless ( $cf->ValidateValue( $args{'Value'} ) ) {
3410 return ( 0, $self->loc("Invalid value for custom field") );
3413 # If the custom field only accepts a single value, delete the existing
3414 # value and record a "changed from foo to bar" transaction
3415 if ( $cf->SingleValue ) {
3417 # We need to whack any old values here. In most cases, the custom field should
3418 # only have one value to delete. In the pathalogical case, this custom field
3419 # used to be a multiple and we have many values to whack....
3420 my $cf_values = $values->Count;
3422 if ( $cf_values > 1 ) {
3423 my $i = 0; #We want to delete all but the last one, so we can then
3424 # execute the same code to "change" the value from old to new
3425 while ( my $value = $values->Next ) {
3427 if ( $i < $cf_values ) {
3428 my $old_value = $value->Content;
3429 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $value->Content);
3433 my ( $TransactionId, $Msg, $TransactionObj ) =
3434 $self->_NewTransaction(
3435 Type => 'CustomField',
3437 OldValue => $old_value
3444 if (my $value = $cf->ValuesForTicket( $self->Id )->First) {
3445 $old_value = $value->Content();
3446 return (1) if $old_value eq $args{'Value'};
3449 my ( $new_value_id, $value_msg ) = $cf->AddValueForTicket(
3450 Ticket => $self->Id,
3451 Content => $args{'Value'}
3454 unless ($new_value_id) {
3456 $self->loc("Could not add new custom field value for ticket. [_1] ",
3460 my $new_value = RT::TicketCustomFieldValue->new( $self->CurrentUser );
3461 $new_value->Load($new_value_id);
3463 # now that adding the new value was successful, delete the old one
3464 if (defined $old_value) {
3465 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $old_value);
3471 if ($args{'RecordTransaction'}) {
3472 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3473 Type => 'CustomField',
3475 OldValue => $old_value,
3476 NewValue => $new_value->Content
3480 if ( $old_value eq '' ) {
3481 return ( 1, $self->loc("[_1] [_2] added", $cf->Name, $new_value->Content) );
3483 elsif ( $new_value->Content eq '' ) {
3484 return ( 1, $self->loc("[_1] [_2] deleted", $cf->Name, $old_value) );
3487 return ( 1, $self->loc("[_1] [_2] changed to [_3]", $cf->Name, $old_value, $new_value->Content ) );
3492 # otherwise, just add a new value and record "new value added"
3494 my ( $new_value_id ) = $cf->AddValueForTicket(
3495 Ticket => $self->Id,
3496 Content => $args{'Value'}
3499 unless ($new_value_id) {
3501 $self->loc("Could not add new custom field value for ticket. "));
3503 if ( $args{'RecordTransaction'} ) {
3504 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3505 Type => 'CustomField',
3507 NewValue => $args{'Value'}
3509 unless ($TransactionId) {
3511 $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
3514 return ( 1, $self->loc("[_1] added as a value for [_2]",$args{'Value'}, $cf->Name));
3521 # {{{ DeleteCustomFieldValue
3523 =item DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
3525 Deletes VALUE as a value of CustomField FIELD.
3527 VALUE can be a string, a CustomFieldValue or a TicketCustomFieldValue.
3529 If VALUE isn't a valid value for the custom field, returns
3530 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
3534 sub DeleteCustomFieldValue {
3541 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3542 return ( 0, $self->loc("Permission Denied") );
3544 my $cf = RT::CustomField->new( $self->CurrentUser );
3545 if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) {
3546 $cf->LoadById( $args{'Field'}->id );
3549 $cf->LoadById( $args{'Field'} );
3552 unless ( $cf->Id ) {
3553 return ( 0, $self->loc("Custom field not found") );
3557 my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $args{'Value'});
3561 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
3562 Type => 'CustomField',
3564 OldValue => $args{'Value'}
3566 unless($TransactionId) {
3567 return(0, $self->loc("Couldn't create a transaction: [_1]", $Msg));
3570 return($TransactionId, $self->loc("[_1] is no longer a value for custom field [_2]", $args{'Value'}, $cf->Name));
3577 # {{{ Actions + Routines dealing with transactions
3579 # {{{ sub SetTold and _SetTold
3581 =head2 SetTold ISO [TIMETAKEN]
3583 Updates the told and records a transaction
3590 $told = shift if (@_);
3591 my $timetaken = shift || 0;
3593 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3594 return ( 0, $self->loc("Permission Denied") );
3597 my $datetold = new RT::Date( $self->CurrentUser );
3599 $datetold->Set( Format => 'iso',
3603 $datetold->SetToNow();
3606 return ( $self->_Set( Field => 'Told',
3607 Value => $datetold->ISO,
3608 TimeTaken => $timetaken,
3609 TransactionType => 'Told' ) );
3614 Updates the told without a transaction or acl check. Useful when we're sending replies.
3621 my $now = new RT::Date( $self->CurrentUser );
3624 #use __Set to get no ACLs ;)
3625 return ( $self->__Set( Field => 'Told',
3626 Value => $now->ISO ) );
3631 # {{{ sub Transactions
3635 Returns an RT::Transactions object of all transactions on this ticket
3642 use RT::Transactions;
3643 my $transactions = RT::Transactions->new( $self->CurrentUser );
3645 #If the user has no rights, return an empty object
3646 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3647 my $tickets = $transactions->NewAlias('Tickets');
3648 $transactions->Join(
3654 $transactions->Limit(
3656 FIELD => 'EffectiveId',
3657 VALUE => $self->id()
3660 # if the user may not see comments do not return them
3661 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3662 $transactions->Limit(
3666 ENTRYAGGREGATOR => 'AND'
3668 $transactions->Limit(
3671 VALUE => "CommentEmailRecord",
3672 ENTRYAGGREGATOR => 'AND'
3677 return ($transactions);
3682 # {{{ sub _NewTransaction
3684 =head2 _NewTransaction PARAMHASH
3686 Private function to create a new RT::Transaction object for this ticket update
3690 sub _NewTransaction {
3700 ActivateScrips => 1,
3705 require RT::Transaction;
3706 my $trans = new RT::Transaction( $self->CurrentUser );
3707 my ( $transaction, $msg ) = $trans->Create(
3708 Ticket => $self->Id,
3709 TimeTaken => $args{'TimeTaken'},
3710 Type => $args{'Type'},
3711 Data => $args{'Data'},
3712 Field => $args{'Field'},
3713 NewValue => $args{'NewValue'},
3714 OldValue => $args{'OldValue'},
3715 MIMEObj => $args{'MIMEObj'},
3716 ActivateScrips => $args{'ActivateScrips'},
3717 CommitScrips => $args{'CommitScrips'},
3720 # Rationalize the object since we may have done things to it during the caching.
3721 $self->Load($self->Id);
3723 $RT::Logger->warning($msg) unless $transaction;
3725 $self->_SetLastUpdated;
3727 if ( defined $args{'TimeTaken'} ) {
3728 $self->_UpdateTimeTaken( $args{'TimeTaken'} );
3730 if ( $RT::UseTransactionBatch and $transaction ) {
3731 push @{$self->{_TransactionBatch}}, $trans;
3733 return ( $transaction, $msg, $trans );
3738 =head2 TransactionBatch
3740 Returns an array reference of all transactions created on this ticket during
3741 this ticket object's lifetime, or undef if there were none.
3743 Only works when the $RT::UseTransactionBatch config variable is set to true.
3747 sub TransactionBatch {
3749 return $self->{_TransactionBatch};
3755 # The following line eliminates reentrancy.
3756 # It protects against the fact that perl doesn't deal gracefully
3757 # when an object's refcount is changed in its destructor.
3758 return if $self->{_Destroyed}++;
3760 my $batch = $self->TransactionBatch or return;
3762 RT::Scrips->new($RT::SystemUser)->Apply(
3763 Stage => 'TransactionBatch',
3765 TransactionObj => $batch->[0],
3771 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3773 # {{{ sub _ClassAccessible
3775 sub _ClassAccessible {
3777 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3778 Queue => { 'read' => 1, 'write' => 1 },
3779 Requestors => { 'read' => 1, 'write' => 1 },
3780 Owner => { 'read' => 1, 'write' => 1 },
3781 Subject => { 'read' => 1, 'write' => 1 },
3782 InitialPriority => { 'read' => 1, 'write' => 1 },
3783 FinalPriority => { 'read' => 1, 'write' => 1 },
3784 Priority => { 'read' => 1, 'write' => 1 },
3785 Status => { 'read' => 1, 'write' => 1 },
3786 TimeEstimated => { 'read' => 1, 'write' => 1 },
3787 TimeWorked => { 'read' => 1, 'write' => 1 },
3788 TimeLeft => { 'read' => 1, 'write' => 1 },
3789 Told => { 'read' => 1, 'write' => 1 },
3790 Resolved => { 'read' => 1 },
3791 Type => { 'read' => 1 },
3792 Starts => { 'read' => 1, 'write' => 1 },
3793 Started => { 'read' => 1, 'write' => 1 },
3794 Due => { 'read' => 1, 'write' => 1 },
3795 Creator => { 'read' => 1, 'auto' => 1 },
3796 Created => { 'read' => 1, 'auto' => 1 },
3797 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3798 LastUpdated => { 'read' => 1, 'auto' => 1 }
3810 my %args = ( Field => undef,
3813 RecordTransaction => 1,
3816 TransactionType => 'Set',
3819 if ($args{'CheckACL'}) {
3820 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3821 return ( 0, $self->loc("Permission Denied"));
3825 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3826 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3827 return(0, $self->loc("Internal Error"));
3830 #if the user is trying to modify the record
3832 #Take care of the old value we really don't want to get in an ACL loop.
3833 # so ask the super::_Value
3834 my $Old = $self->SUPER::_Value("$args{'Field'}");
3837 if ( $args{'UpdateTicket'} ) {
3840 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3841 Value => $args{'Value'} );
3843 #If we can't actually set the field to the value, don't record
3844 # a transaction. instead, get out of here.
3845 if ( $ret == 0 ) { return ( 0, $msg ); }
3848 if ( $args{'RecordTransaction'} == 1 ) {
3850 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3851 Type => $args{'TransactionType'},
3852 Field => $args{'Field'},
3853 NewValue => $args{'Value'},
3855 TimeTaken => $args{'TimeTaken'},
3857 return ( $Trans, scalar $TransObj->Description );
3860 return ( $ret, $msg );
3870 Takes the name of a table column.
3871 Returns its value as a string, if the user passes an ACL check
3880 #if the field is public, return it.
3881 if ( $self->_Accessible( $field, 'public' ) ) {
3883 #$RT::Logger->debug("Skipping ACL check for $field\n");
3884 return ( $self->SUPER::_Value($field) );
3888 #If the current user doesn't have ACLs, don't let em at it.
3890 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3893 return ( $self->SUPER::_Value($field) );
3899 # {{{ sub _UpdateTimeTaken
3901 =head2 _UpdateTimeTaken
3903 This routine will increment the timeworked counter. it should
3904 only be called from _NewTransaction
3908 sub _UpdateTimeTaken {
3910 my $Minutes = shift;
3913 $Total = $self->SUPER::_Value("TimeWorked");
3914 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3916 Field => "TimeWorked",
3927 # {{{ Routines dealing with ACCESS CONTROL
3929 # {{{ sub CurrentUserHasRight
3931 =head2 CurrentUserHasRight
3933 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3934 1 if the user has that right. It returns 0 if the user doesn't have that right.
3938 sub CurrentUserHasRight {
3944 Principal => $self->CurrentUser->UserObj(),
3957 Takes a paramhash with the attributes 'Right' and 'Principal'
3958 'Right' is a ticket-scoped textual right from RT::ACE
3959 'Principal' is an RT::User object
3961 Returns 1 if the principal has the right. Returns undef if not.
3973 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3975 $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
3979 $args{'Principal'}->HasRight(
3981 Right => $args{'Right'}
3994 Jesse Vincent, jesse@bestpractical.com