1 # $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
2 # (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
3 # This software is redistributable under the terms of the GNU GPL
8 RT::Ticket - RT ticket object
13 my $ticket = new RT::Ticket($CurrentUser);
14 $ticket->Load($ticket_id);
18 This module lets you manipulate RT\'s ticket object.
44 ok(require RT::Ticket, "Loading the RT::Ticket library");
54 $self->{'table'} = "Tickets";
55 return ($self->SUPER::_Init(@_));
64 Takes a single argument. This can be a ticket id, ticket alias or
65 local ticket uri. If the ticket can't be loaded, returns undef.
66 Otherwise, returns the ticket id.
74 #TODO modify this routine to look at EffectiveId and do the recursive load
75 # thing. be careful to cache all the interim tickets we try so we don't loop forever.
77 #If it's a local URI, turn it into a ticket id
78 if ($id =~ /^$RT::TicketBaseURI(\d+)$/) {
81 #If it's a remote URI, we're going to punt for now
82 elsif ($id =~ '://' ) {
86 #If we have an integer URI, load the ticket
87 if ( $id =~ /^\d+$/ ) {
88 my $ticketid = $self->LoadById($id);
91 $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
96 #It's not a URI. It's not a numerical ticket ID. Punt!
101 #If we're merged, resolve the merge.
102 if (($self->EffectiveId) and
103 ($self->EffectiveId != $self->Id)) {
104 return ($self->Load($self->EffectiveId));
107 #Ok. we're loaded. lets get outa here.
118 Given a local ticket URI, loads the specified ticket.
126 if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) {
128 return ($self->Load($id));
141 Arguments: ARGS is a hash of named parameters. Valid parameters are:
143 Queue - Either a Queue object or a Queue Name
144 Requestor - A reference to a list of RT::User objects, email addresses or RT user Names
145 Cc - A reference to a list of RT::User objects, email addresses or Names
146 AdminCc - A reference to a list of RT::User objects, email addresses or Names
147 Type -- The ticket\'s type. ignore this for now
148 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
149 Subject -- A string describing the subject of the ticket
150 InitialPriority -- an integer from 0 to 99
151 FinalPriority -- an integer from 0 to 99
152 Status -- any valid status (Defined in RT::Queue)
153 TimeWorked -- an integer
154 TimeLeft -- an integer
155 Starts -- an ISO date describing the ticket\'s start date and time in GMT
156 Due -- an ISO date describing the ticket\'s due date and time in GMT
157 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
159 KeywordSelect-<id> -- an array of keyword ids for that keyword select
162 Returns: TICKETID, Transaction Object, Error Message
167 my $t = RT::Ticket->new($RT::SystemUser);
169 ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created");
171 ok ( my $id = $t->Id, "Got ticket id");
186 Owner => $RT::Nobody->UserObj,
187 Subject => '[no subject]',
188 InitialPriority => undef,
189 FinalPriority => undef,
198 my ($ErrStr, $QueueObj, $Owner, $resolved);
199 my (@non_fatal_errors);
201 my $now = RT::Date->new($self->CurrentUser);
204 if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
205 $QueueObj=RT::Queue->new($RT::SystemUser);
206 $QueueObj->Load($args{'Queue'});
208 elsif (ref($args{'Queue'}) eq 'RT::Queue') {
209 $QueueObj=RT::Queue->new($RT::SystemUser);
210 $QueueObj->Load($args{'Queue'}->Id);
213 $RT::Logger->debug("$self ". $args{'Queue'} .
214 " not a recognised queue object.");
217 #Can't create a ticket without a queue.
218 unless (defined ($QueueObj)) {
219 $RT::Logger->debug( "$self No queue given for ticket creation.");
220 return (0, 0,'Could not create ticket. Queue not set');
223 #Now that we have a queue, Check the ACLS
224 unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
225 QueueObj => $QueueObj )) {
226 return (0,0,"No permission to create tickets in the queue '".
227 $QueueObj->Name."'.");
230 #Since we have a queue, we can set queue defaults
233 # If there's no queue default initial priority and it's not set, set it to 0
234 $args{'InitialPriority'} = ($QueueObj->InitialPriority || 0)
235 unless (defined $args{'InitialPriority'});
239 # If there's no queue default final priority and it's not set, set it to 0
240 $args{'FinalPriority'} = ($QueueObj->FinalPriority || 0)
241 unless (defined $args{'FinalPriority'});
244 #TODO we should see what sort of due date we're getting, rather +
245 # than assuming it's in ISO format.
247 #Set the due date. if we didn't get fed one, use the queue default due in
248 my $due = new RT::Date($self->CurrentUser);
249 if (defined $args{'Due'}) {
250 $due->Set (Format => 'ISO',
251 Value => $args{'Due'});
253 elsif (defined ($QueueObj->DefaultDueIn)) {
255 $due->AddDays($QueueObj->DefaultDueIn);
258 my $starts = new RT::Date($self->CurrentUser);
259 if (defined $args{'Starts'}) {
260 $starts->Set (Format => 'ISO',
261 Value => $args{'Starts'});
265 # {{{ Deal with setting the owner
267 if (ref($args{'Owner'}) eq 'RT::User') {
268 $Owner = $args{'Owner'};
270 #If we've been handed something else, try to load the user.
271 elsif ($args{'Owner'}) {
272 $Owner = new RT::User($self->CurrentUser);
273 $Owner->Load($args{'Owner'});
276 #If we can't handle it, call it nobody
278 if (ref($args{'Owner'})) {
279 $RT::Logger->warning("$ticket ->Create called with an Owner of ".
280 "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n");
282 push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
285 $RT::Logger->warning("$self ->Create called with an ".
286 "unknown datatype for Owner: ".$args{'Owner'} .
287 ". Defaulting to Nobody.\n");
291 #If we have a proposed owner and they don't have the right
292 #to own a ticket, scream about it and make them not the owner
293 if ((defined ($Owner)) and
294 ($Owner->Id != $RT::Nobody->Id) and
295 (!$Owner->HasQueueRight( QueueObj => $QueueObj,
296 Right => 'OwnTicket'))) {
298 $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
300 "as a ticket owner but has no rights to own ".
301 "tickets in this queue\n");
303 push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
308 #If we haven't been handed a valid owner, make it nobody.
309 unless (defined ($Owner)) {
310 $Owner = new RT::User($self->CurrentUser);
311 $Owner->Load($RT::Nobody->UserObj->Id);
316 unless ($self->ValidateStatus($args{'Status'})) {
317 return (0,0,'Invalid value for status');
320 if ($args{'Status'} eq 'resolved') {
321 $resolved = $now->ISO;
326 my $id = $self->SUPER::Create(
327 Queue => $QueueObj->Id,
329 Subject => $args{'Subject'},
330 InitialPriority => $args{'InitialPriority'},
331 FinalPriority => $args{'FinalPriority'},
332 Priority => $args{'InitialPriority'},
333 Status => $args{'Status'},
334 TimeWorked => $args{'TimeWorked'},
335 TimeLeft => $args{'TimeLeft'},
336 Type => $args{'Type'},
337 Starts => $starts->ISO,
338 Resolved => $resolved,
341 #Set the ticket's effective ID now that we've created it.
342 my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
345 $RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n");
350 foreach $watcher (@{$args{'Cc'}}) {
352 $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
353 push @non_fatal_errors, $wmsg unless ($wval);
356 foreach $watcher (@{$args{'Requestor'}}) {
358 $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
359 push @non_fatal_errors, $wmsg unless ($wval);
362 foreach $watcher (@{$args{'AdminCc'}}) {
363 # Note that we're using AddWatcher, rather than _AddWatcher, as we
364 # actually _want_ that ACL check. Otherwise, random ticket creators
365 # could make themselves adminccs and maybe get ticket rights. that would
368 $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
369 push @non_fatal_errors, $wmsg unless ($wval);
372 # Iterate through all the KeywordSelect-<int> params passed in, calling _AddKeyword
376 foreach my $key (keys %args) {
378 next unless ($key =~ /^KeywordSelect-(.*)$/);
383 my @keywords = ref($args{$key}) eq 'ARRAY' ?
384 @{$args{$key}} : ($args{$key});
386 foreach my $keyword (@keywords) {
387 my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks,
391 push @non_fatal_errors, $kmsg unless ($kval);
396 #Add a transaction for the create
397 my ($Trans, $Msg, $TransObj) =
398 $self->_NewTransaction( Type => "Create",
400 MIMEObj=>$args{'MIMEObj'});
403 if ($self->Id && $Trans) {
404 $ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name.
405 "'.\n" . join("\n", @non_fatal_errors);
407 $RT::Logger->info($ErrStr);
410 # TODO where does this get errstr from?
411 $RT::Logger->warning("Ticket couldn't be created: $ErrStr");
414 return($self->Id, $TransObj->Id, $ErrStr);
421 =head2 Import PARAMHASH
424 Doesn\'t create a transaction.
425 Doesn\'t supply queue defaults, etc.
427 Arguments are identical to Create(), with the addition of
437 my ( $ErrStr, $QueueObj, $Owner);
439 my %args = (id => undef,
440 EffectiveId => undef,
444 Owner => $RT::Nobody->Id,
445 Subject => '[no subject]',
446 InitialPriority => undef,
447 FinalPriority => undef,
457 if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
458 $QueueObj=RT::Queue->new($RT::SystemUser);
459 $QueueObj->Load($args{'Queue'});
460 #TODO error check this and return 0 if it\'s not loading properly +++
462 elsif (ref($args{'Queue'}) eq 'RT::Queue') {
463 $QueueObj=RT::Queue->new($RT::SystemUser);
464 $QueueObj->Load($args{'Queue'}->Id);
467 $RT::Logger->debug("$self ". $args{'Queue'} .
468 " not a recognised queue object.");
471 #Can't create a ticket without a queue.
472 unless (defined ($QueueObj) and $QueueObj->Id) {
473 $RT::Logger->debug( "$self No queue given for ticket creation.");
474 return (0,'Could not create ticket. Queue not set');
477 #Now that we have a queue, Check the ACLS
478 unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
479 QueueObj => $QueueObj )) {
480 return (0,"No permission to create tickets in the queue '".
481 $QueueObj->Name."'.");
487 # {{{ Deal with setting the owner
489 # Attempt to take user object, user name or user id.
490 # Assign to nobody if lookup fails.
491 if (defined ($args{'Owner'})) {
492 if ( ref($args{'Owner'}) ) {
493 $Owner = $args{'Owner'};
496 $Owner = new RT::User($self->CurrentUser);
497 $Owner->Load($args{'Owner'});
498 if ( ! defined($Owner->id) ) {
499 $Owner->Load($RT::Nobody->id);
505 #If we have a proposed owner and they don't have the right
506 #to own a ticket, scream about it and make them not the owner
507 if ((defined ($Owner)) and
508 ($Owner->Id != $RT::Nobody->Id) and
509 (!$Owner->HasQueueRight( QueueObj => $QueueObj,
510 Right => 'OwnTicket'))) {
512 $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
514 "as a ticket owner but has no rights to own ".
515 "tickets in '".$QueueObj->Name."'\n");
520 #If we haven't been handed a valid owner, make it nobody.
521 unless (defined ($Owner)) {
522 $Owner = new RT::User($self->CurrentUser);
523 $Owner->Load($RT::Nobody->UserObj->Id);
528 unless ($self->ValidateStatus($args{'Status'})) {
529 return (0,"'$args{'Status'}' is an invalid value for status");
532 $self->{'_AccessibleCache'}{Created} = { 'read'=>1, 'write'=>1 };
533 $self->{'_AccessibleCache'}{Creator} = { 'read'=>1, 'auto'=>1 };
534 $self->{'_AccessibleCache'}{LastUpdated} = { 'read'=>1, 'write'=>1 };
535 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read'=>1, 'auto'=>1 };
538 # If we're coming in with an id, set that now.
539 my $EffectiveId = undef;
541 $EffectiveId = $args{'id'};
546 my $id = $self->SUPER::Create(
548 EffectiveId => $EffectiveId,
549 Queue => $QueueObj->Id,
551 Subject => $args{'Subject'},
552 InitialPriority => $args{'InitialPriority'},
553 FinalPriority => $args{'FinalPriority'},
554 Priority => $args{'InitialPriority'},
555 Status => $args{'Status'},
556 TimeWorked => $args{'TimeWorked'},
557 Type => $args{'Type'},
558 Created => $args{'Created'},
559 Told => $args{'Told'},
560 LastUpdated => $args{'Updated'},
561 Resolved => $args{Resolved},
567 # If the ticket didn't have an id
568 # Set the ticket's effective ID now that we've created it.
570 $self->Load($args{'id'});
573 my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
576 $RT::Logger->err($self."->Import couldn't set EffectiveId: $msg\n");
581 foreach $watcher (@{$args{'Cc'}}) {
582 $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
584 foreach $watcher (@{$args{'AdminCc'}}) {
585 $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
587 foreach $watcher (@{$args{'Requestor'}}) {
588 $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
591 return($self->Id, $ErrStr);
600 return (0, 'Deleting this object would violate referential integrity.'.
605 # {{{ Routines dealing with watchers.
607 # {{{ Routines dealing with adding new watchers
613 AddWatcher takes a parameter hash. The keys are as follows:
619 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.
625 my %args = ( Email => undef,
632 #If the watcher we're trying to add is for the current user
633 if ( ( $self->CurrentUser->EmailAddress &&
634 ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or
635 ($args{'Owner'} eq $self->CurrentUser->Id)
639 # If it's an AdminCc and they don't have
640 # 'WatchAsAdminCc' or 'ModifyTicket', bail
641 if ($args{'Type'} eq 'AdminCc') {
642 unless ($self->CurrentUserHasRight('ModifyTicket') or
643 $self->CurrentUserHasRight('WatchAsAdminCc')) {
644 return(0, 'Permission Denied');
648 # If it's a Requestor or Cc and they don't have
649 # 'Watch' or 'ModifyTicket', bail
650 elsif (($args{'Type'} eq 'Cc') or
651 ($args{'Type'} eq 'Requestor')) {
653 unless ($self->CurrentUserHasRight('ModifyTicket') or
654 $self->CurrentUserHasRight('Watch')) {
655 return(0, 'Permission Denied');
659 $RT::Logger->warn("$self -> AddWatcher hit code".
660 " it never should. We got passed ".
661 " a type of ". $args{'Type'});
662 return (0,'Error in parameters to TicketAddWatcher');
665 # If the watcher isn't the current user
666 # and the current user doesn't have 'ModifyTicket'
669 unless ($self->CurrentUserHasRight('ModifyTicket')) {
670 return (0, "Permission Denied");
675 return ($self->_AddWatcher(%args));
679 #This contains the meat of AddWatcher. but can be called from a routine like
680 # Create, which doesn't need the additional acl check
693 #clear the watchers cache
694 $self->{'watchers_cache'} = undef;
696 if (defined $args{'Person'}) {
697 #if it's an RT::User object, pull out the id and shove it in Owner
698 if (ref ($args{'Person'}) =~ /RT::User/) {
699 $args{'Owner'} = $args{'Person'}->id;
701 #if it's an int, shove it in Owner
702 elsif ($args{'Person'} =~ /^\d+$/) {
703 $args{'Owner'} = $args{'Person'};
705 #if it's an email address, shove it in Email
707 $args{'Email'} = $args{'Person'};
711 # Turn an email address int a watcher if we possibly can.
712 if ($args{'Email'}) {
713 my $watcher = new RT::User($self->CurrentUser);
714 $watcher->LoadByEmail($args{'Email'});
716 $args{'Owner'} = $watcher->Id;
717 delete $args{'Email'};
722 # see if this user is already a watcher. if we have an owner, check it
723 # otherwise, we've got an email-address watcher. use that.
725 if ($self->IsWatcher(Type => $args{'Type'},
726 Id => ($args{'Owner'} || $args{'Email'}) ) ) {
729 return(0, 'That user is already that sort of watcher for this ticket');
734 my $Watcher = new RT::Watcher ($self->CurrentUser);
735 my ($retval, $msg) = ($Watcher->Create( Value => $self->Id,
737 Email => $args{'Email'},
738 Type => $args{'Type'},
739 Owner => $args{'Owner'},
742 unless ($args{'Silent'}) {
743 $self->_NewTransaction( Type => 'AddWatcher',
744 NewValue => $Watcher->Email,
745 Field => $Watcher->Type);
748 return ($retval, $msg);
753 # {{{ sub AddRequestor
757 AddRequestor takes what AddWatcher does, except it presets
758 the "Type" parameter to \'Requestor\'
764 return ($self->AddWatcher ( Type => 'Requestor', @_));
773 AddCc takes what AddWatcher does, except it presets
774 the "Type" parameter to \'Cc\'
780 return ($self->AddWatcher ( Type => 'Cc', @_));
788 AddAdminCc takes what AddWatcher does, except it presets
789 the "Type" parameter to \'AdminCc\'
795 return ($self->AddWatcher ( Type => 'AdminCc', @_));
802 # {{{ sub DeleteWatcher
804 =head2 DeleteWatcher id [type]
806 DeleteWatcher takes a single argument which is either an email address
808 If the first argument is an email address, you need to specify the watcher type you're talking
809 about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'.
810 It removes that watcher from this Ticket\'s list of watchers.
815 #TODO It is lame that you can't call this the same way you can call AddWatcher
823 $type = shift if (@_);
825 my $Watcher = new RT::Watcher($self->CurrentUser);
827 #If it\'s a numeric watcherid
828 if ($id =~ /^(\d*)$/) {
832 #Otherwise, we'll assume it's an email address
835 $Watcher->LoadByValue( Email => $id,
839 return (0,$msg) unless ($result);
843 return(0,"Can\'t delete a watcher by email address without specifying a type");
848 #If the watcher we're trying to delete is for the current user
849 if ($Watcher->Email eq $self->CurrentUser->EmailAddress) {
851 # If it's an AdminCc and they don't have
852 # 'WatchAsAdminCc' or 'ModifyTicket', bail
853 if ($Watcher->Type eq 'AdminCc') {
854 unless ($self->CurrentUserHasRight('ModifyTicket') or
855 $self->CurrentUserHasRight('WatchAsAdminCc')) {
856 return(0, 'Permission Denied');
860 # If it's a Requestor or Cc and they don't have
861 # 'Watch' or 'ModifyTicket', bail
862 elsif (($Watcher->Type eq 'Cc') or
863 ($Watcher->Type eq 'Requestor')) {
865 unless ($self->CurrentUserHasRight('ModifyTicket') or
866 $self->CurrentUserHasRight('Watch')) {
867 return(0, 'Permission Denied');
871 $RT::Logger->warn("$self -> DeleteWatcher hit code".
872 " it never should. We got passed ".
873 " a type of ". $args{'Type'});
874 return (0,'Error in parameters to $self DeleteWatcher');
877 # If the watcher isn't the current user
878 # and the current user doesn't have 'ModifyTicket'
881 unless ($self->CurrentUserHasRight('ModifyTicket')) {
882 return (0, "Permission Denied");
888 unless (($Watcher->Scope eq 'Ticket') and
889 ($Watcher->Value == $self->id) ) {
890 return (0, "Not a watcher for this ticket");
894 #Clear out the watchers hash.
895 $self->{'watchers'} = undef;
897 #If we\'ve validated that it is a watcher for this ticket
898 $self->_NewTransaction ( Type => 'DelWatcher',
899 OldValue => $Watcher->Email,
900 Field => $Watcher->Type,
903 my $retval = $Watcher->Delete();
906 return(0,"Watcher could not be deleted. Database inconsistency possible.");
909 return(1, "Watcher deleted");
912 # {{{ sub DeleteRequestor
914 =head2 DeleteRequestor EMAIL
916 Takes an email address. It calls DeleteWatcher with a preset
922 sub DeleteRequestor {
925 return ($self->DeleteWatcher ($id, 'Requestor'))
932 =head2 DeleteCc EMAIL
934 Takes an email address. It calls DeleteWatcher with a preset
943 return ($self->DeleteWatcher ($id, 'Cc'))
948 # {{{ sub DeleteAdminCc
950 =head2 DeleteAdminCc EMAIL
952 Takes an email address. It calls DeleteWatcher with a preset
961 return ($self->DeleteWatcher ($id, 'AdminCc'))
973 Watchers returns a Watchers object preloaded with this ticket\'s watchers.
975 # It should return only the ticket watchers. the actual FooAsString
976 # methods capture the queue watchers too. I don't feel thrilled about this,
977 # but we don't want the Cc Requestors and AdminCc objects to get filled up
978 # with all the queue watchers too. we've got seperate objects for that.
979 # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch?
986 require RT::Watchers;
987 my $watchers=RT::Watchers->new($self->CurrentUser);
988 if ($self->CurrentUserHasRight('ShowTicket')) {
989 $watchers->LimitToTicket($self->id);
998 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1000 =head2 RequestorsAsString
1002 B<Returns> String: All Ticket Requestor email addresses as a string.
1006 sub RequestorsAsString {
1009 unless ($self->CurrentUserHasRight('ShowTicket')) {
1013 return ($self->Requestors->EmailsAsString() );
1016 =head2 WatchersAsString
1018 B<Returns> String: All Ticket Watchers email addresses as a string
1022 sub WatchersAsString {
1025 unless ($self->CurrentUserHasRight('ShowTicket')) {
1026 return (0, "Permission Denied");
1029 return ($self->Watchers->EmailsAsString());
1033 =head2 AdminCcAsString
1035 returns String: All Ticket AdminCc email addresses as a string
1040 sub AdminCcAsString {
1043 unless ($self->CurrentUserHasRight('ShowTicket')) {
1047 return ($self->AdminCc->EmailsAsString());
1053 returns String: All Ticket Ccs as a string of email addresses
1060 unless ($self->CurrentUserHasRight('ShowTicket')) {
1064 return ($self->Cc->EmailsAsString());
1070 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1072 # {{{ sub Requestors
1077 Returns this ticket's Requestors as an RT::Watchers object
1084 my $requestors = $self->Watchers();
1085 if ($self->CurrentUserHasRight('ShowTicket')) {
1086 $requestors->LimitToRequestors();
1089 return($requestors);
1100 Returns a watchers object which contains this ticket's Cc watchers
1107 my $cc = $self->Watchers();
1109 if ($self->CurrentUserHasRight('ShowTicket')) {
1124 Returns this ticket\'s administrative Ccs as an RT::Watchers object
1131 my $admincc = $self->Watchers();
1132 if ($self->CurrentUserHasRight('ShowTicket')) {
1133 $admincc->LimitToAdminCc();
1142 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1145 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1149 Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string
1150 is a ticket watcher. Returns undef otherwise
1157 my %args = ( Type => 'Requestor',
1163 my %cols = ('Type' => $args{'Type'},
1164 'Scope' => 'Ticket',
1165 'Value' => $self->Id,
1170 if (ref($args{'Id'})){
1171 #If it's a ref, it's an RT::User object;
1172 $cols{'Owner'} = $args{'Id'}->Id;
1174 elsif ($args{'Id'} =~ /^\d+$/) {
1175 # if it's an integer, it's a reference to an RT::User obj
1176 $cols{'Owner'} = $args{'Id'};
1179 $cols{'Email'} = $args{'Id'};
1182 if ($args{'Email'}) {
1183 $cols{'Email'} = $args{'Email'};
1186 my $description = join(":",%cols);
1188 #If we've cached a positive match...
1189 if (defined $self->{'watchers_cache'}->{"$description"}) {
1190 if ($self->{'watchers_cache'}->{"$description"} == 1) {
1193 else { #If we've cached a negative match...
1199 my $watcher = new RT::Watcher($self->CurrentUser);
1200 $watcher->LoadByCols(%cols);
1204 $self->{'watchers_cache'}->{"$description"} = 1;
1208 $self->{'watchers_cache'}->{"$description"} = 0;
1215 # {{{ sub IsRequestor
1219 Takes an email address, RT::User object or integer (RT user id)
1220 Returns true if the string is a requestor of the current ticket.
1229 return ($self->IsWatcher(Type => 'Requestor', Id => $person));
1239 Takes a string. Returns true if the string is a Cc watcher of the current ticket.
1247 return ($self->IsWatcher( Type => 'Cc', Id => $cc ));
1257 Takes a string. Returns true if the string is an AdminCc watcher of the current ticket.
1265 return ($self->IsWatcher( Type => 'AdminCc', Id => $person ));
1275 Takes an RT::User object. Returns true if that user is this ticket's owner.
1276 returns undef otherwise
1285 # no ACL check since this is used in acl decisions
1286 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1291 #Tickets won't yet have owners when they're being created.
1292 unless ($self->OwnerObj->id) {
1296 if ($person->id == $self->OwnerObj->id) {
1311 # {{{ Routines dealing with queues
1313 # {{{ sub ValidateQueue
1319 #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue,
1321 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1325 my $QueueObj = RT::Queue->new($self->CurrentUser);
1326 my $id = $QueueObj->Load($Value);
1342 my $NewQueue = shift;
1344 #Redundant. ACL gets checked in _Set;
1345 unless ($self->CurrentUserHasRight('ModifyTicket')) {
1346 return (0, "Permission Denied");
1350 my $NewQueueObj = RT::Queue->new($self->CurrentUser);
1351 $NewQueueObj->Load($NewQueue);
1353 unless ($NewQueueObj->Id()) {
1354 return (0, "That queue does not exist");
1357 if ($NewQueueObj->Id == $self->QueueObj->Id) {
1358 return (0, 'That is the same value');
1360 unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket',
1361 QueueObj => $NewQueueObj )) {
1362 return (0, "You may not create requests in that queue.");
1365 unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket',
1366 QueueObj => $NewQueueObj)) {
1370 return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id()));
1380 Takes nothing. returns this ticket's queue object
1387 my $queue_obj = RT::Queue->new($self->CurrentUser);
1388 #We call __Value so that we can avoid the ACL decision and some deep recursion
1389 my ($result) = $queue_obj->Load($self->__Value('Queue'));
1390 return ($queue_obj);
1398 # {{{ Date printing routines
1404 Returns an RT::Date object containing this ticket's due date
1410 my $time = new RT::Date($self->CurrentUser);
1412 # -1 is RT::Date slang for never
1414 $time->Set(Format => 'sql', Value => $self->Due );
1417 $time->Set(Format => 'unix', Value => -1);
1424 # {{{ sub DueAsString
1428 Returns this ticket's due date as a human readable string
1434 return $self->DueObj->AsString();
1439 # {{{ sub GraceTimeAsString
1441 =head2 GraceTimeAsString
1443 Return the time until this ticket is due as a string
1447 # TODO This should be deprecated
1449 sub GraceTimeAsString {
1453 return ($self->DueObj->AgeAsString());
1462 # {{{ sub ResolvedObj
1466 Returns an RT::Date object of this ticket's 'resolved' time.
1473 my $time = new RT::Date($self->CurrentUser);
1474 $time->Set(Format => 'sql', Value => $self->Resolved);
1479 # {{{ sub SetStarted
1483 Takes a date in ISO format or undef
1484 Returns a transaction id and a message
1485 The client calls "Start" to note that the project was started on the date in $date.
1486 A null date means "now"
1492 my $time = shift || 0;
1495 unless ($self->CurrentUserHasRight('ModifyTicket')) {
1496 return (0, "Permission Denied");
1499 #We create a date object to catch date weirdness
1500 my $time_obj = new RT::Date($self->CurrentUser());
1502 $time_obj->Set(Format => 'ISO', Value => $time);
1505 $time_obj->SetToNow();
1508 #Now that we're starting, open this ticket
1509 #TODO do we really want to force this as policy? it should be a scrip
1511 #We need $TicketAsSystem, in case the current user doesn't have
1514 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1515 $TicketAsSystem->Load($self->Id);
1516 if ($TicketAsSystem->Status eq 'new') {
1517 $TicketAsSystem->Open();
1520 return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO));
1526 # {{{ sub StartedObj
1530 Returns an RT::Date object which contains this ticket's
1539 my $time = new RT::Date($self->CurrentUser);
1540 $time->Set(Format => 'sql', Value => $self->Started);
1549 Returns an RT::Date object which contains this ticket's
1557 my $time = new RT::Date($self->CurrentUser);
1558 $time->Set(Format => 'sql', Value => $self->Starts);
1567 Returns an RT::Date object which contains this ticket's
1576 my $time = new RT::Date($self->CurrentUser);
1577 $time->Set(Format => 'sql', Value => $self->Told);
1583 # {{{ sub LongSinceToldAsString
1585 # TODO this should be deprecated
1588 sub LongSinceToldAsString {
1592 return $self->ToldObj->AgeAsString();
1599 # {{{ sub ToldAsString
1603 A convenience method that returns ToldObj->AsString
1605 TODO: This should be deprecated
1613 return $self->ToldObj->AsString();
1621 # {{{ sub TimeWorkedAsString
1623 =head2 TimeWorkedAsString
1625 Returns the amount of time worked on this ticket as a Text String
1629 sub TimeWorkedAsString {
1631 return "0" unless $self->TimeWorked;
1633 #This is not really a date object, but if we diff a number of seconds
1634 #vs the epoch, we'll get a nice description of time worked.
1636 my $worked = new RT::Date($self->CurrentUser);
1637 #return the #of minutes worked turned into seconds and written as
1638 # a simple text string
1640 return($worked->DurationAsString($self->TimeWorked*60));
1648 # {{{ Routines dealing with correspondence/comments
1654 Comment on this ticket.
1655 Takes a hashref with the follwoing attributes:
1657 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
1665 CcMessageTo => undef,
1666 BccMessageTo => undef,
1671 unless (($self->CurrentUserHasRight('CommentOnTicket')) or
1672 ($self->CurrentUserHasRight('ModifyTicket'))) {
1673 return (0, "Permission Denied");
1676 unless ($args{'MIMEObj'}) {
1677 return(0,"No correspondence attached");
1680 # If we've been passed in CcMessageTo and BccMessageTo fields,
1681 # add them to the mime object for passing on to the transaction handler
1682 # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
1683 # RT-Send-Bcc: headers
1685 $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
1686 $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
1688 #Record the correspondence (write the transaction)
1689 my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment',
1690 Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
1691 TimeTaken => $args{'TimeTaken'},
1692 MIMEObj => $args{'MIMEObj'}
1696 return ($Trans, "The comment has been recorded");
1701 # {{{ sub Correspond
1705 Correspond on this ticket.
1706 Takes a hashref with the following attributes:
1709 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
1716 CcMessageTo => undef,
1717 BccMessageTo => undef,
1722 unless (($self->CurrentUserHasRight('ReplyToTicket')) or
1723 ($self->CurrentUserHasRight('ModifyTicket'))) {
1724 return (0, "Permission Denied");
1727 unless ($args{'MIMEObj'}) {
1728 return(0,"No correspondence attached");
1731 # If we've been passed in CcMessageTo and BccMessageTo fields,
1732 # add them to the mime object for passing on to the transaction handler
1733 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
1736 $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
1737 $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
1739 #Record the correspondence (write the transaction)
1740 my ($Trans,$msg, $TransObj) = $self->_NewTransaction
1741 (Type => 'Correspond',
1742 Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
1743 TimeTaken => $args{'TimeTaken'},
1744 MIMEObj=> $args{'MIMEObj'}
1747 # TODO this bit of logic should really become a scrip for 2.2
1748 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1749 $TicketAsSystem->Load($self->Id);
1752 ($TicketAsSystem->Status ne 'open') and
1753 ($TicketAsSystem->Status ne 'new')
1756 my $oldstatus = $TicketAsSystem->Status();
1757 $TicketAsSystem->__Set(Field => 'Status', Value => 'open');
1758 $TicketAsSystem->_NewTransaction
1761 OldValue => $oldstatus,
1763 Data => 'Ticket auto-opened on incoming correspondence'
1768 $RT::Logger->err("$self couldn't init a transaction ($msg)\n");
1769 return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'});
1772 #Set the last told date to now if this isn't mail from the requestor.
1773 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
1775 unless ($TransObj->IsInbound) {
1779 return ($Trans, "correspondence sent");
1786 # {{{ Routines dealing with Links and Relations between tickets
1788 # {{{ Link Collections
1794 This returns an RT::Links object which references all the tickets
1795 which are 'MembersOf' this ticket
1801 return ($self->_Links('Target', 'MemberOf'));
1810 This returns an RT::Links object which references all the tickets that this
1811 ticket is a 'MemberOf'
1817 return ($self->_Links('Base', 'MemberOf'));
1826 This returns an RT::Links object which shows all references for which this ticket is a base
1832 return ($self->_Links('Base', 'RefersTo'));
1841 This returns an RT::Links object which shows all references for which this ticket is a target
1847 return ($self->_Links('Target', 'RefersTo'));
1856 This returns an RT::Links object which references all the tickets that depend on this one
1861 return ($self->_Links('Target','DependsOn'));
1870 This returns an RT::Links object which references all the tickets that this ticket depends on
1875 return ($self->_Links('Base','DependsOn'));
1885 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1888 my $type =shift || "";
1890 unless ($self->{"$field$type"}) {
1891 $self->{"$field$type"} = new RT::Links($self->CurrentUser);
1892 if ($self->CurrentUserHasRight('ShowTicket')) {
1894 $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI);
1895 $self->{"$field$type"}->Limit(FIELD=>'Type',
1896 VALUE=>$type) if ($type);
1899 return ($self->{"$field$type"});
1907 # {{{ sub DeleteLink
1911 Delete a link. takes a paramhash of Base, Target and Type.
1912 Either Base or Target must be null. The null value will
1913 be replaced with this ticket\'s id
1919 my %args = ( Base => undef,
1925 unless ($self->CurrentUserHasRight('ModifyTicket')) {
1926 $RT::Logger->debug("No permission to delete links\n");
1927 return (0, 'Permission Denied');
1932 #we want one of base and target. we don't care which
1933 #but we only want _one_
1935 if ($args{'Base'} and $args{'Target'}) {
1936 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1937 return (0, 'Can\'t specifiy both base and target');
1939 elsif ($args{'Base'}) {
1940 $args{'Target'} = $self->Id();
1942 elsif ($args{'Target'}) {
1943 $args{'Base'} = $self->Id();
1946 $RT::Logger->debug("$self: Base or Target must be specified\n");
1947 return (0, 'Either base or target must be specified');
1950 my $link = new RT::Link($self->CurrentUser);
1951 $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n");
1953 $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
1959 $RT::Logger->debug("We're going to delete link ".$link->id."\n");
1963 "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
1964 my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
1965 (Type => 'DeleteLink',
1966 Field => $args{'Type'},
1967 Data => $TransString,
1971 return ($linkid, "Link deleted ($TransString)", $transactionid);
1973 #if it's not a link we can find
1975 $RT::Logger->debug("Couldn't find that link\n");
1976 return (0, "Link not found");
1986 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
1993 my %args = ( Target => '',
1998 unless ($self->CurrentUserHasRight('ModifyTicket')) {
1999 return (0, "Permission Denied");
2002 if ($args{'Base'} and $args{'Target'}) {
2003 $RT::Logger->debug("$self tried to delete a link. both base and target were specified\n");
2004 return (0, 'Can\'t specifiy both base and target');
2006 elsif ($args{'Base'}) {
2007 $args{'Target'} = $self->Id();
2009 elsif ($args{'Target'}) {
2010 $args{'Base'} = $self->Id();
2013 return (0, 'Either base or target must be specified');
2016 # {{{ We don't want references to ourself
2017 if ($args{Base} eq $args{Target}) {
2018 return (0, "Can\'t link a ticket to itself");
2023 # If the base isn't a URI, make it a URI.
2024 # If the target isn't a URI, make it a URI.
2026 # {{{ Check if the link already exists - we don't want duplicates
2027 my $old_link= new RT::Link ($self->CurrentUser);
2028 $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
2029 if ($old_link->Id) {
2030 $RT::Logger->debug("$self Somebody tried to duplicate a link");
2031 return ($old_link->id, "Link already exists",0);
2035 # Storing the link in the DB.
2036 my $link = RT::Link->new($self->CurrentUser);
2037 my ($linkid) = $link->Create(Target => $args{Target},
2038 Base => $args{Base},
2039 Type => $args{Type});
2042 return (0,"Link could not be created");
2044 #Write the transaction
2046 my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
2048 my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
2050 Field => $args{'Type'},
2051 Data => $TransString,
2055 return ($Trans, "Link created ($TransString)");
2065 Returns this ticket's URI
2071 return $RT::TicketBaseURI.$self->id;
2079 MergeInto take the id of the ticket to merge this ticket into.
2085 my $MergeInto = shift;
2087 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2088 return (0, "Permission Denied");
2091 # Load up the new ticket.
2092 my $NewTicket = RT::Ticket->new($RT::SystemUser);
2093 $NewTicket->Load($MergeInto);
2095 # make sure it exists.
2096 unless (defined $NewTicket->Id) {
2097 return (0, 'New ticket doesn\'t exist');
2101 # Make sure the current user can modify the new ticket.
2102 unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) {
2103 $RT::Logger->debug("failed...");
2104 return (0, "Permission Denied");
2107 $RT::Logger->debug("checking if the new ticket has the same id and effective id...");
2108 unless ($NewTicket->id == $NewTicket->EffectiveId) {
2109 $RT::Logger->err('$self trying to merge into '.$NewTicket->Id .
2110 ' which is itself merged.\n');
2111 return (0, "Can't merge into a merged ticket. ".
2112 "You should never get this error");
2116 # We use EffectiveId here even though it duplicates information from
2117 # the links table becasue of the massive performance hit we'd take
2118 # by trying to do a seperate database query for merge info everytime
2122 #update this ticket's effective id to the new ticket's id.
2123 my ($id_val, $id_msg) = $self->__Set(Field => 'EffectiveId',
2124 Value => $NewTicket->Id());
2127 $RT::Logger->error("Couldn't set effective ID for ".$self->Id.
2129 return(0,"Merge failed. Couldn't set EffectiveId");
2132 my ($status_val, $status_msg) = $self->__Set(Field => 'Status',
2133 Value => 'resolved');
2135 unless ($status_val) {
2136 $RT::Logger->error("$self couldn't set status to resolved.".
2137 "RT's Database may be inconsistent.");
2140 #make a new link: this ticket is merged into that other ticket.
2141 $self->AddLink( Type =>'MergedInto',
2142 Target => $NewTicket->Id() );
2144 #add all of this ticket's watchers to that ticket.
2145 my $watchers = $self->Watchers();
2147 while (my $watcher = $watchers->Next()) {
2150 $NewTicket->IsWatcher (Type => $watcher->Type,
2151 Id => $watcher->Owner)) or
2153 $NewTicket->IsWatcher (Type => $watcher->Type,
2154 Id => $watcher->Email))
2159 $NewTicket->_AddWatcher(Silent => 1,
2160 Type => $watcher->Type,
2161 Email => $watcher->Email,
2162 Owner => $watcher->Owner);
2167 #find all of the tickets that were merged into this ticket.
2168 my $old_mergees = new RT::Tickets($self->CurrentUser);
2169 $old_mergees->Limit( FIELD => 'EffectiveId',
2171 VALUE => $self->Id );
2173 # update their EffectiveId fields to the new ticket's id
2174 while (my $ticket = $old_mergees->Next()) {
2175 my ($val, $msg) = $ticket->__Set(Field => 'EffectiveId',
2176 Value => $NewTicket->Id());
2178 $NewTicket->_SetLastUpdated;
2180 return ($TransactionObj, "Merge Successful");
2187 # {{{ Routines dealing with keywords
2189 # {{{ sub KeywordsObj
2191 =head2 KeywordsObj [KEYWORD_SELECT_ID]
2193 Returns an B<RT::ObjectKeywords> object preloaded with this ticket's ObjectKeywords.
2194 If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword
2203 $keyword_select = shift if (@_);
2205 use RT::ObjectKeywords;
2206 my $Keywords = new RT::ObjectKeywords($self->CurrentUser);
2209 if ($self->CurrentUserHasRight('ShowTicket')) {
2210 $Keywords->LimitToTicket($self->id);
2211 if ($keyword_select) {
2212 $Keywords->LimitToKeywordSelect($keyword_select);
2219 # {{{ sub AddKeyword
2223 Takes a paramhash of Keyword and KeywordSelect. If Keyword is a valid choice
2224 for KeywordSelect, creates a KeywordObject. If the KeywordSelect says this should
2225 be a single KeywordObject, automatically removes the old value.
2227 Issues: probably doesn't enforce the depth restrictions or make sure that keywords
2228 are coming from the right part of the tree. really should.
2235 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2236 return (0, 'Permission Denied');
2239 return($self->_AddKeyword(@_));
2244 # Helper version of AddKeyword without that pesky ACL check
2247 my %args = ( KeywordSelect => undef, # id of a keyword select record
2248 Keyword => undef, #id of the keyword to add
2255 #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'}
2257 #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue.
2259 my $Keyword = new RT::Keyword($self->CurrentUser);
2260 unless ($Keyword->Load($args{'Keyword'}) ) {
2261 $RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n");
2262 return(0, "Couldn't load keyword");
2265 my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser);
2266 unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) {
2267 $RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'});
2268 return(0, "Couldn't load keywordselect");
2271 my $Keywords = $self->KeywordsObj($KeywordSelectObj->id);
2273 #If the ticket already has this keyword, just get out of here.
2274 if ($Keywords->HasEntry($Keyword->id)) {
2275 return(0, "That is already the current value");
2278 #If the keywordselect wants this to be a singleton:
2280 if ($KeywordSelectObj->Single) {
2282 #Whack any old values...keep track of the last value that we get.
2283 #we shouldn't need a loop ehre, but we do it anyway, to try to
2284 # help keep the database clean.
2285 while (my $OldKey = $Keywords->Next) {
2286 $OldValue = $OldKey->KeywordObj->Name;
2293 # create the new objectkeyword
2294 my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
2295 my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id,
2296 ObjectType => 'Ticket',
2297 ObjectId => $self->Id,
2298 KeywordSelect => $KeywordSelectObj->Id );
2301 # record a single transaction, unless we were told not to
2302 unless ($args{'Silent'}) {
2303 my ($TransactionId, $Msg, $TransactionObj) =
2304 $self->_NewTransaction( Type => 'Keyword',
2305 Field => $KeywordSelectObj->Id,
2306 OldValue => $OldValue,
2307 NewValue => $Keyword->Name );
2309 return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added.");
2315 # {{{ sub DeleteKeyword
2317 =head2 DeleteKeyword
2319 Takes a paramhash. Deletes the Keyword denoted by the I<Keyword> parameter from this
2320 ticket's object keywords.
2326 my %args = ( Keyword => undef,
2327 KeywordSelect => undef,
2331 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2332 return (0, 'Permission Denied');
2336 #Load up the ObjectKeyword we\'re talking about
2337 my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
2338 $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'},
2339 KeywordSelect => $args{'KeywordSelect'},
2340 ObjectType => 'Ticket',
2341 ObjectId => $self->id()
2344 #if we can\'t find it, bail
2345 unless ($ObjectKeyword->id) {
2346 $RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} .
2347 " for keywordselect ". $args{'KeywordSelect'} .
2348 "for ticket ".$self->id );
2349 return (undef, "Couldn't load keyword while trying to delete it.");
2352 #record transaction here.
2353 my ($TransactionId, $Msg, $TransObj) =
2354 $self->_NewTransaction( Type => 'Keyword',
2355 OldValue => $ObjectKeyword->KeywordObj->Name);
2357 $ObjectKeyword->Delete();
2359 return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted.");
2367 # {{{ Routines dealing with ownership
2373 Takes nothing and returns an RT::User object of
2381 #If this gets ACLed, we lose on a rights check in User.pm and
2382 #get deep recursion. if we need ACLs here, we need
2383 #an equiv without ACLs
2385 $owner = new RT::User ($self->CurrentUser);
2386 $owner->Load($self->__Value('Owner'));
2388 #Return the owner object
2394 # {{{ sub OwnerAsString
2396 =head2 OwnerAsString
2398 Returns the owner's email address
2404 return($self->OwnerObj->EmailAddress);
2414 Takes two arguments:
2415 the Id or Name of the owner
2416 and (optionally) the type of the SetOwner Transaction. It defaults
2417 to 'Give'. 'Steal' is also a valid option.
2423 my $NewOwner = shift;
2424 my $Type = shift || "Give";
2426 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2427 return (0, "Permission Denied");
2430 my $NewOwnerObj = RT::User->new($self->CurrentUser);
2431 my $OldOwnerObj = $self->OwnerObj;
2433 $NewOwnerObj->Load($NewOwner);
2434 if (!$NewOwnerObj->Id) {
2435 return (0, "That user does not exist");
2438 #If thie ticket has an owner and it's not the current user
2440 if (($Type ne 'Steal' ) and ($Type ne 'Force') and #If we're not stealing
2441 ($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
2442 ($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us
2443 return(0, "You can only reassign tickets that you own or that are unowned");
2446 #If we've specified a new owner and that user can't modify the ticket
2447 elsif (($NewOwnerObj->Id) and
2448 (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket',
2449 QueueObj => $self->QueueObj,
2450 TicketObj => $self))
2452 return (0, "That user may not own requests in that queue");
2456 #If the ticket has an owner and it's the new owner, we don't need
2458 elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) {
2459 return(0, "That user already owns that request");
2463 my ($trans,$msg)=$self->_Set(Field => 'Owner',
2464 Value => $NewOwnerObj->Id,
2466 TransactionType => $Type);
2469 $msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name;
2471 return ($trans, $msg);
2481 A convenince method to set the ticket's owner to the current user
2487 return ($self->SetOwner($self->CurrentUser->Id, 'Take'));
2496 Convenience method to set the owner to 'nobody' if the current user is the owner.
2502 return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake'));
2510 A convenience method to change the owner of the current ticket to the
2511 current user. Even if it's owned by another user.
2518 if ($self->IsOwner($self->CurrentUser)) {
2519 return (0,"You already own this ticket");
2521 return($self->SetOwner($self->CurrentUser->Id, 'Steal'));
2531 # {{{ Routines dealing with status
2533 # {{{ sub ValidateStatus
2535 =head2 ValidateStatus STATUS
2537 Takes a string. Returns true if that status is a valid status for this ticket.
2538 Returns false otherwise.
2542 sub ValidateStatus {
2546 #Make sure the status passed in is valid
2547 unless ($self->QueueObj->IsValidStatus($status)) {
2560 =head2 SetStatus STATUS
2562 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead.
2571 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2572 return (0, 'Permission Denied');
2575 my $now = new RT::Date($self->CurrentUser);
2578 #If we're changing the status from new, record that we've started
2579 if (($self->Status =~ /new/) && ($status ne 'new')) {
2580 #Set the Started time to "now"
2581 $self->_Set(Field => 'Started',
2583 RecordTransaction => 0);
2587 if ($status eq 'resolved') {
2588 #When we resolve a ticket, set the 'Resolved' attribute to now.
2589 $self->_Set(Field => 'Resolved',
2591 RecordTransaction => 0);
2595 #Actually update the status
2596 return($self->_Set(Field => 'Status',
2599 TransactionType => 'Status'));
2608 Takes no arguments. Marks this ticket for garbage collection
2614 return ($self->SetStatus('dead'));
2615 # TODO: garbage collection
2624 Sets this ticket's status to stalled
2630 return ($self->SetStatus('stalled'));
2639 Sets this ticket\'s status to Open
2645 return ($self->SetStatus('open'));
2654 Sets this ticket\'s status to Resolved
2660 return ($self->SetStatus('resolved'));
2667 # {{{ Actions + Routines dealing with transactions
2669 # {{{ sub SetTold and _SetTold
2671 =head2 SetTold ISO [TIMETAKEN]
2673 Updates the told and records a transaction
2680 $told = shift if (@_);
2681 my $timetaken=shift || 0;
2683 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2684 return (0, "Permission Denied");
2687 my $datetold = new RT::Date($self->CurrentUser);
2689 $datetold->Set( Format => 'iso',
2693 $datetold->SetToNow();
2696 return($self->_Set(Field => 'Told',
2697 Value => $datetold->ISO,
2698 TimeTaken => $timetaken,
2699 TransactionType => 'Told'));
2704 Updates the told without a transaction or acl check. Useful when we're sending replies.
2711 my $now = new RT::Date($self->CurrentUser);
2713 #use __Set to get no ACLs ;)
2714 return($self->__Set(Field => 'Told',
2715 Value => $now->ISO));
2720 # {{{ sub Transactions
2724 Returns an RT::Transactions object of all transactions on this ticket
2731 use RT::Transactions;
2732 my $transactions = RT::Transactions->new($self->CurrentUser);
2734 #If the user has no rights, return an empty object
2735 if ($self->CurrentUserHasRight('ShowTicket')) {
2736 my $tickets = $transactions->NewAlias('Tickets');
2737 $transactions->Join( ALIAS1 => 'main',
2741 $transactions->Limit( ALIAS => $tickets,
2742 FIELD => 'EffectiveId',
2743 VALUE => $self->id());
2744 # if the user may not see comments do not return them
2745 unless ($self->CurrentUserHasRight('ShowTicketComments')) {
2746 $transactions->Limit( FIELD => 'Type',
2748 VALUE => "Comment");
2752 return($transactions);
2757 # {{{ sub _NewTransaction
2759 sub _NewTransaction {
2761 my %args = ( TimeTaken => 0,
2771 require RT::Transaction;
2772 my $trans = new RT::Transaction($self->CurrentUser);
2773 my ($transaction, $msg) =
2774 $trans->Create( Ticket => $self->Id,
2775 TimeTaken => $args{'TimeTaken'},
2776 Type => $args{'Type'},
2777 Data => $args{'Data'},
2778 Field => $args{'Field'},
2779 NewValue => $args{'NewValue'},
2780 OldValue => $args{'OldValue'},
2781 MIMEObj => $args{'MIMEObj'}
2784 $RT::Logger->warning($msg) unless $transaction;
2786 $self->_SetLastUpdated;
2788 if (defined $args{'TimeTaken'} ) {
2789 $self->_UpdateTimeTaken($args{'TimeTaken'});
2791 return($transaction, $msg, $trans);
2798 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
2800 # {{{ sub _ClassAccessible
2802 sub _ClassAccessible {
2804 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
2805 Queue => { 'read' => 1, 'write' => 1 },
2806 Requestors => { 'read' => 1, 'write' => 1 },
2807 Owner => { 'read' => 1, 'write' => 1 },
2808 Subject => { 'read' => 1, 'write' => 1 },
2809 InitialPriority => { 'read' => 1, 'write' => 1 },
2810 FinalPriority => { 'read' => 1, 'write' => 1 },
2811 Priority => { 'read' => 1, 'write' => 1 },
2812 Status => { 'read' => 1, 'write' => 1 },
2813 TimeWorked => { 'read' => 1, 'write' => 1 },
2814 TimeLeft => { 'read' => 1, 'write' => 1 },
2815 Created => { 'read' => 1, 'auto' => 1 },
2816 Creator => { 'read' => 1, 'auto' => 1 },
2817 Told => { 'read' => 1, 'write' => 1 },
2818 Resolved => {'read' => 1},
2819 Starts => { 'read' => 1, 'write' => 1 },
2820 Started => { 'read' => 1, 'write' => 1 },
2821 Due => { 'read' => 1, 'write' => 1 },
2822 Creator => { 'read' => 1, 'auto' => 1 },
2823 Created => { 'read' => 1, 'auto' => 1 },
2824 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
2825 LastUpdated => { 'read' => 1, 'auto' => 1 }
2837 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2838 return (0, "Permission Denied");
2841 my %args = (Field => undef,
2844 RecordTransaction => 1,
2845 TransactionType => 'Set',
2848 #if the user is trying to modify the record
2850 #Take care of the old value we really don't want to get in an ACL loop.
2851 # so ask the super::_Value
2852 my $Old=$self->SUPER::_Value("$args{'Field'}");
2855 my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'},
2856 Value=> $args{'Value'});
2858 #If we can't actually set the field to the value, don't record
2859 # a transaction. instead, get out of here.
2860 if ($ret==0) {return (0,$msg);}
2862 if ($args{'RecordTransaction'} == 1) {
2864 my ($Trans, $Msg, $TransObj) =
2865 $self->_NewTransaction(Type => $args{'TransactionType'},
2866 Field => $args{'Field'},
2867 NewValue => $args{'Value'},
2869 TimeTaken => $args{'TimeTaken'},
2871 return ($Trans,$TransObj->Description);
2874 return ($ret, $msg);
2884 Takes the name of a table column.
2885 Returns its value as a string, if the user passes an ACL check
2895 #if the field is public, return it.
2896 if ($self->_Accessible($field, 'public')) {
2897 #$RT::Logger->debug("Skipping ACL check for $field\n");
2898 return($self->SUPER::_Value($field));
2902 #If the current user doesn't have ACLs, don't let em at it.
2904 unless ($self->CurrentUserHasRight('ShowTicket')) {
2907 return($self->SUPER::_Value($field));
2913 # {{{ sub _UpdateTimeTaken
2915 =head2 _UpdateTimeTaken
2917 This routine will increment the timeworked counter. it should
2918 only be called from _NewTransaction
2922 sub _UpdateTimeTaken {
2924 my $Minutes = shift;
2927 $Total = $self->SUPER::_Value("TimeWorked");
2928 $Total = ($Total || 0) + ($Minutes || 0);
2929 $self->SUPER::_Set(Field => "TimeWorked",
2939 # {{{ Routines dealing with ACCESS CONTROL
2941 # {{{ sub CurrentUserHasRight
2943 =head2 CurrentUserHasRight
2945 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
2946 1 if the user has that right. It returns 0 if the user doesn't have that right.
2950 sub CurrentUserHasRight {
2954 return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(),
2955 Right => "$right"));
2965 Takes a paramhash with the attributes 'Right' and 'Principal'
2966 'Right' is a ticket-scoped textual right from RT::ACE
2967 'Principal' is an RT::User object
2969 Returns 1 if the principal has the right. Returns undef if not.
2975 my %args = ( Right => undef,
2979 unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) {
2980 $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
2983 return($args{'Principal'}->HasQueueRight(TicketObj => $self,
2984 Right => $args{'Right'}));
2996 Jesse Vincent, jesse@fsck.com