diff options
Diffstat (limited to 'rt/lib/RT/Ticket.pm')
| -rwxr-xr-x | rt/lib/RT/Ticket.pm | 3026 | 
1 files changed, 2684 insertions, 342 deletions
| diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm index 2f075a20c..f7275e4e3 100755 --- a/rt/lib/RT/Ticket.pm +++ b/rt/lib/RT/Ticket.pm @@ -1,662 +1,3004 @@ -# BEGIN LICENSE BLOCK -#  -# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com> -#  -# (Except where explictly superceded by other copyright notices) -#  -# This work is made available to you under the terms of Version 2 of -# the GNU General Public License. A copy of that license should have -# been provided with this software, but in any event can be snarfed -# from www.gnu.org. -#  -# This work is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU -# General Public License for more details. -#  -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. -#  -#  -# END LICENSE BLOCK -# Autogenerated by DBIx::SearchBuilder factory (by <jesse@bestpractical.com>) -# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.   -#  -# !! DO NOT EDIT THIS FILE !! +# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $ +# (c) 1996-2001 Jesse Vincent <jesse@fsck.com> +# This software is redistributable under the terms of the GNU GPL  # -use strict; - -  =head1 NAME -RT::Ticket - +  RT::Ticket - RT ticket object  =head1 SYNOPSIS +  use RT::Ticket; +  my $ticket = new RT::Ticket($CurrentUser); +  $ticket->Load($ticket_id); +  =head1 DESCRIPTION +This module lets you manipulate RT\'s ticket object. + +  =head1 METHODS  =cut + +  package RT::Ticket; -use RT::Record;   use RT::Queue; +use RT::User; +use RT::Record; +use RT::Link; +use RT::Links; +use RT::Date; +use RT::Watcher; -use vars qw( @ISA ); -@ISA= qw( RT::Record ); +@ISA= qw(RT::Record); -sub _Init { -  my $self = shift;  -  $self->Table('Tickets'); -  $self->SUPER::_Init(@_); +=begin testing + +use RT::TestHarness; + +ok(require RT::Ticket, "Loading the RT::Ticket library"); + +=end testing + +=cut + +# {{{ sub _Init + +sub _Init { +    my $self = shift; +    $self->{'table'} = "Tickets"; +    return ($self->SUPER::_Init(@_));  } +# }}} +# {{{ sub Load +=head2 Load +Takes a single argument. This can be a ticket id, ticket alias or  +local ticket uri.  If the ticket can't be loaded, returns undef. +Otherwise, returns the ticket id. -=item Create PARAMHASH +=cut + +sub Load { +   my $self = shift; +   my $id = shift; + +   #TODO modify this routine to look at EffectiveId and do the recursive load +   # thing. be careful to cache all the interim tickets we try so we don't loop forever. +    +   #If it's a local URI, turn it into a ticket id +   if ($id =~ /^$RT::TicketBaseURI(\d+)$/)  { +       $id = $1; +   } +   #If it's a remote URI, we're going to punt for now +   elsif ($id =~ '://' ) { +       return (undef); +   } +    +   #If we have an integer URI, load the ticket +   if ( $id =~ /^\d+$/ ) { +       my $ticketid = $self->LoadById($id); +    +       unless ($ticketid) { +	   $RT::Logger->debug("$self tried to load a bogus ticket: $id\n"); +	   return(undef); +       } +   } +    +   #It's not a URI. It's not a numerical ticket ID. Punt! +   else { +       return(undef); +   } +    +   #If we're merged, resolve the merge. +   if (($self->EffectiveId) and +       ($self->EffectiveId != $self->Id)) { +	   return ($self->Load($self->EffectiveId)); +       } + +   #Ok. we're loaded. lets get outa here. +   return ($self->Id); +    +} -Create takes a hash of values and creates a row in the database: +# }}} -  int(11) 'EffectiveId'. -  int(11) 'Queue'. -  varchar(16) 'Type'. -  int(11) 'IssueStatement'. -  int(11) 'Resolution'. -  int(11) 'Owner'. -  varchar(200) 'Subject' defaults to '[no subject]'. -  int(11) 'InitialPriority'. -  int(11) 'FinalPriority'. -  int(11) 'Priority'. -  int(11) 'TimeEstimated'. -  int(11) 'TimeWorked'. -  varchar(10) 'Status'. -  int(11) 'TimeLeft'. -  datetime 'Told'. -  datetime 'Starts'. -  datetime 'Started'. -  datetime 'Due'. -  datetime 'Resolved'. -  smallint(6) 'Disabled'. +# {{{ sub LoadByURI + +=head2 LoadByURI + +Given a local ticket URI, loads the specified ticket.  =cut +sub LoadByURI { +    my $self = shift; +    my $uri = shift; +     +    if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) { +        my $id = $1; +        return ($self->Load($id)); +    } +    else { +        return(undef); +    } +} + +# }}} + +# {{{ sub Create +=head2 Create (ARGS) +Arguments: ARGS is a hash of named parameters.  Valid parameters are: + +  Queue  - Either a Queue object or a Queue Name +  Requestor -  A reference to a list of RT::User objects, email addresses or RT user Names +  Cc  - A reference to a list of RT::User objects, email addresses or Names +  AdminCc  - A reference to a  list of RT::User objects, email addresses or Names +  Type -- The ticket\'s type. ignore this for now +  Owner -- This ticket\'s owner. either an RT::User object or this user\'s id +  Subject -- A string describing the subject of the ticket +  InitialPriority -- an integer from 0 to 99 +  FinalPriority -- an integer from 0 to 99 +  Status -- any valid status (Defined in RT::Queue) +  TimeWorked -- an integer +  TimeLeft -- an integer +  Starts -- an ISO date describing the ticket\'s start date and time in GMT +  Due -- an ISO date describing the ticket\'s due date and time in GMT +  MIMEObj -- a MIME::Entity object with the content of the initial ticket request. + +  KeywordSelect-<id> -- an array of keyword ids for that keyword select + + +Returns: TICKETID, Transaction Object, Error Message + + +=begin testing + +my $t = RT::Ticket->new($RT::SystemUser); + +ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created"); + +ok ( my $id = $t->Id, "Got ticket id"); + +=end testing + +=cut  sub Create {      my $self = shift; -    my %args = (  -                EffectiveId => '0', -                Queue => '0', -                Type => '', -                IssueStatement => '0', -                Resolution => '0', -                Owner => '0', -                Subject => '[no subject]', -                InitialPriority => '0', -                FinalPriority => '0', -                Priority => '0', -                TimeEstimated => '0', -                TimeWorked => '0', -                Status => '', -                TimeLeft => '0', -                Told => '', -                Starts => '', -                Started => '', -                Due => '', -                Resolved => '', -                Disabled => '0', +     +    my %args = ( +		Queue => undef, +		Requestor => undef, +		Cc => undef, +		AdminCc => undef, +		Type => 'ticket', +		Owner => $RT::Nobody->UserObj, +		Subject => '[no subject]', +		InitialPriority => undef, +		FinalPriority => undef, +		Status => 'new', +		TimeWorked => "0", +		TimeLeft => 0, +		Due => undef, +		Starts => undef, +		MIMEObj => undef, +		@_); + +    my ($ErrStr, $QueueObj, $Owner, $resolved); +    my (@non_fatal_errors); +     +    my $now = RT::Date->new($self->CurrentUser); +    $now->SetToNow(); + +    if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) { +	$QueueObj=RT::Queue->new($RT::SystemUser); +	$QueueObj->Load($args{'Queue'}); +    } +    elsif (ref($args{'Queue'}) eq 'RT::Queue') { +	$QueueObj=RT::Queue->new($RT::SystemUser); +	$QueueObj->Load($args{'Queue'}->Id); +    } +    else { +	$RT::Logger->debug("$self ". $args{'Queue'} .  +			 " not a recognised queue object."); +    } +   +    #Can't create a ticket without a queue. +    unless (defined ($QueueObj)) { +	$RT::Logger->debug( "$self No queue given for ticket creation."); +	return (0, 0,'Could not create ticket. Queue not set'); +    } +     +    #Now that we have a queue, Check the ACLS +    unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket', +					      QueueObj => $QueueObj )) { +	return (0,0,"No permission to create tickets in the queue '".  +		$QueueObj->Name."'."); +    } +     +    #Since we have a queue, we can set queue defaults +    #Initial Priority + +    # If there's no queue default initial priority and it's not set, set it to 0 +    $args{'InitialPriority'} = ($QueueObj->InitialPriority || 0) +      unless (defined $args{'InitialPriority'}); +	 +    #Final priority  + +    # If there's no queue default final priority and it's not set, set it to 0 +    $args{'FinalPriority'} = ($QueueObj->FinalPriority  || 0) +      unless (defined $args{'FinalPriority'}); +     +     +    #TODO we should see what sort of due date we're getting, rather + +    # than assuming it's in ISO format. +     +    #Set the due date. if we didn't get fed one, use the queue default due in +    my $due = new RT::Date($self->CurrentUser); +    if (defined $args{'Due'}) { +	$due->Set (Format => 'ISO', +		   Value => $args{'Due'}); +    }	 +    elsif (defined ($QueueObj->DefaultDueIn)) { +	$due->SetToNow; +	$due->AddDays($QueueObj->DefaultDueIn); +    }	 +     +    my $starts = new RT::Date($self->CurrentUser); +    if (defined $args{'Starts'}) { +	$starts->Set (Format => 'ISO', +		   Value => $args{'Starts'}); +    } + +	 +    # {{{ Deal with setting the owner +     +    if (ref($args{'Owner'}) eq 'RT::User') { +	$Owner = $args{'Owner'}; +    } +    #If we've been handed something else, try to load the user. +    elsif ($args{'Owner'}) { +	$Owner = new RT::User($self->CurrentUser); +	$Owner->Load($args{'Owner'}); +	 +    } +    #If we can't handle it, call it nobody +    else { +	if (ref($args{'Owner'})) { +	    $RT::Logger->warning("$ticket ->Create called with an Owner of ". +		 "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n"); + +	    push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'."; +	} +	else {  +	    $RT::Logger->warning("$self ->Create called with an ". +				 "unknown datatype for Owner: ".$args{'Owner'} . +				 ". Defaulting to Nobody.\n"); +	} +    } +     +    #If we have a proposed owner and they don't have the right  +    #to own a ticket, scream about it and make them not the owner +    if ((defined ($Owner)) and +	($Owner->Id != $RT::Nobody->Id) and  +	(!$Owner->HasQueueRight( QueueObj => $QueueObj,  +				 Right => 'OwnTicket'))) { +	 +	$RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id . +			     ") was proposed ". +			     "as a ticket owner but has no rights to own ". +			     "tickets in this queue\n"); + +	push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'."; + +	$Owner = undef; +    } +     +    #If we haven't been handed a valid owner, make it nobody. +    unless (defined ($Owner)) { +	$Owner = new RT::User($self->CurrentUser); +	$Owner->Load($RT::Nobody->UserObj->Id); +    }	 + +    # }}} + +    unless ($self->ValidateStatus($args{'Status'})) { +	return (0,0,'Invalid value for status'); +    } + +    if ($args{'Status'} eq 'resolved') { +	$resolved = $now->ISO; +    } else{ +	$resolved = undef; +    } + +    my $id = $self->SUPER::Create( +				  Queue => $QueueObj->Id, +				  Owner => $Owner->Id, +				  Subject => $args{'Subject'}, +				  InitialPriority => $args{'InitialPriority'}, +				  FinalPriority => $args{'FinalPriority'}, +				  Priority => $args{'InitialPriority'}, +				  Status => $args{'Status'}, +				  TimeWorked => $args{'TimeWorked'}, +				  TimeLeft => $args{'TimeLeft'}, +				  Type => $args{'Type'},	 +				  Starts => $starts->ISO, +				  Resolved => $resolved, +				  Due => $due->ISO +				 ); +    #Set the ticket's effective ID now that we've created it. +    my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id); +     +    unless ($val) { +	$RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n"); +    }	 +      -		  @_); -    $self->SUPER::Create( -                         EffectiveId => $args{'EffectiveId'}, -                         Queue => $args{'Queue'}, -                         Type => $args{'Type'}, -                         IssueStatement => $args{'IssueStatement'}, -                         Resolution => $args{'Resolution'}, -                         Owner => $args{'Owner'}, -                         Subject => $args{'Subject'}, -                         InitialPriority => $args{'InitialPriority'}, -                         FinalPriority => $args{'FinalPriority'}, -                         Priority => $args{'Priority'}, -                         TimeEstimated => $args{'TimeEstimated'}, -                         TimeWorked => $args{'TimeWorked'}, -                         Status => $args{'Status'}, -                         TimeLeft => $args{'TimeLeft'}, -                         Told => $args{'Told'}, -                         Starts => $args{'Starts'}, -                         Started => $args{'Started'}, -                         Due => $args{'Due'}, -                         Resolved => $args{'Resolved'}, -                         Disabled => $args{'Disabled'}, -); +    my $watcher; +    foreach $watcher (@{$args{'Cc'}}) { +	my ($wval, $wmsg) =  +	  $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1); +	push @non_fatal_errors, $wmsg	unless ($wval); +    }	 + +    foreach $watcher (@{$args{'Requestor'}}) { +	my ($wval, $wmsg) =  +	  $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1); +	push @non_fatal_errors, $wmsg	unless ($wval); +    } + +    foreach $watcher (@{$args{'AdminCc'}}) { +	# Note that we're using AddWatcher, rather than _AddWatcher, as we  +	# actually _want_ that ACL check. Otherwise, random ticket creators +	# could make themselves adminccs and maybe get ticket rights. that would +	# be poor +	my ($wval, $wmsg) =  +	  $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1); +	push @non_fatal_errors, $wmsg	unless ($wval); +    } + +    # Iterate through all the KeywordSelect-<int> params passed in, calling _AddKeyword +    # for each of them + + +    foreach my $key (keys %args) { + +	next unless ($key =~ /^KeywordSelect-(.*)$/); +	 +	my $ks = $1; + + +	my @keywords = ref($args{$key}) eq 'ARRAY' ? +	      @{$args{$key}} : ($args{$key}); +	 +	foreach my $keyword (@keywords) {   +	    my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks, +						   Keyword => $keyword, +						   Silent => 1); +	}	 +	push @non_fatal_errors, $kmsg unless ($kval); +    } + +     +     +    #Add a transaction for the create +    my ($Trans, $Msg, $TransObj) =  +	$self->_NewTransaction( Type => "Create", +			  	TimeTaken => 0,  +			  	MIMEObj=>$args{'MIMEObj'}); +     +    # Logging +    if ($self->Id && $Trans) { +	$ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name.  +	  "'.\n" . join("\n", @non_fatal_errors); +	 +	$RT::Logger->info($ErrStr); +    } +    else { +	# TODO where does this get errstr from? +	$RT::Logger->warning("Ticket couldn't be created: $ErrStr"); +    } +     +    return($self->Id, $TransObj->Id, $ErrStr); +} +# }}} + +# {{{ sub Import + +=head2 Import PARAMHASH + +Import a ticket.  +Doesn\'t create a transaction.  +Doesn\'t supply queue defaults, etc. + +Arguments are identical to Create(), with the addition of +    Id -    Ticket Id + +Returns: TICKETID + +=cut + + +sub Import { +    my $self = shift; +    my ( $ErrStr, $QueueObj, $Owner); +     +    my %args = (id => undef, +		EffectiveId => undef, +		Queue => undef, +		Requestor => undef, +		Type => 'ticket', +		Owner => $RT::Nobody->Id, +		Subject => '[no subject]', +		InitialPriority => undef, +		FinalPriority => undef, +		Status => 'new', +		TimeWorked => "0", +		Due => undef, +		Created => undef, +		Updated => undef, +	Resolved => undef, +		Told => undef, +		@_); +     +    if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) { +	$QueueObj=RT::Queue->new($RT::SystemUser); +	$QueueObj->Load($args{'Queue'}); +	#TODO error check this and return 0 if it\'s not loading properly +++ +    } +    elsif (ref($args{'Queue'}) eq 'RT::Queue') { +	$QueueObj=RT::Queue->new($RT::SystemUser); +	$QueueObj->Load($args{'Queue'}->Id); +    } +    else { +	$RT::Logger->debug("$self ". $args{'Queue'} .  +			   " not a recognised queue object."); +    } +     +    #Can't create a ticket without a queue. +    unless (defined ($QueueObj) and $QueueObj->Id) { +	$RT::Logger->debug( "$self No queue given for ticket creation."); +	return (0,'Could not create ticket. Queue not set'); +    } +     +    #Now that we have a queue, Check the ACLS +    unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket', +					      QueueObj => $QueueObj )) { +	return (0,"No permission to create tickets in the queue '".  +		$QueueObj->Name."'."); +    } +     +     + + +    # {{{ Deal with setting the owner +       +    # Attempt to take user object, user name or user id. +    # Assign to nobody if lookup fails. +    if (defined ($args{'Owner'})) {  +	if ( ref($args{'Owner'}) ) { +	    $Owner = $args{'Owner'}; +	} +	else { +	    $Owner = new RT::User($self->CurrentUser); +	    $Owner->Load($args{'Owner'}); +	    if ( ! defined($Owner->id) ) { +		$Owner->Load($RT::Nobody->id); +	    } +	} +    } +     + +    #If we have a proposed owner and they don't have the right  +    #to own a ticket, scream about it and make them not the owner +    if ((defined ($Owner)) and +	($Owner->Id != $RT::Nobody->Id) and  +	(!$Owner->HasQueueRight( QueueObj => $QueueObj,  +				 Right => 'OwnTicket'))) { +	 +	$RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id . +			     ") was proposed ". +			     "as a ticket owner but has no rights to own ". +			     "tickets in '".$QueueObj->Name."'\n"); +	 +	$Owner = undef; +    } +     +    #If we haven't been handed a valid owner, make it nobody. +    unless (defined ($Owner)) { +	$Owner = new RT::User($self->CurrentUser); +	$Owner->Load($RT::Nobody->UserObj->Id); +    }	 + +    # }}} + +    unless ($self->ValidateStatus($args{'Status'})) { +	return (0,"'$args{'Status'}' is an invalid value for status"); +    } +     +    $self->{'_AccessibleCache'}{Created} = { 'read'=>1, 'write'=>1 }; +    $self->{'_AccessibleCache'}{Creator} = { 'read'=>1, 'auto'=>1 }; +    $self->{'_AccessibleCache'}{LastUpdated} = { 'read'=>1, 'write'=>1 }; +    $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read'=>1, 'auto'=>1 }; + + +    # If we're coming in with an id, set that now. +    my $EffectiveId = undef; +    if ($args{'id'}) { +	$EffectiveId = $args{'id'}; + +    } + + +    my $id = $self->SUPER::Create( +				  id => $args{'id'}, +				  EffectiveId => $EffectiveId, +				  Queue => $QueueObj->Id, +				  Owner => $Owner->Id, +				  Subject => $args{'Subject'}, +				  InitialPriority => $args{'InitialPriority'}, +				  FinalPriority => $args{'FinalPriority'}, +				  Priority => $args{'InitialPriority'}, +				  Status => $args{'Status'}, +				  TimeWorked => $args{'TimeWorked'}, +				  Type => $args{'Type'},	 +				  Created => $args{'Created'}, +				  Told => $args{'Told'}, +				  LastUpdated => $args{'Updated'}, +	Resolved => $args{Resolved}, +				  Due => $args{'Due'}, +				 ); + + + +    # If the ticket didn't have an id +    # Set the ticket's effective ID now that we've created it. +    if ($args{'id'} ) {  +	  $self->Load($args{'id'}); +    } +    else { +	   my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id); +     +    	   unless ($val) { +	    $RT::Logger->err($self."->Import couldn't set EffectiveId: $msg\n"); +   	   }	 +    }  + +    my $watcher; +    foreach $watcher (@{$args{'Cc'}}) { +	$self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1); +    }	 +    foreach $watcher (@{$args{'AdminCc'}}) { +	$self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1); +    }	 +    foreach $watcher (@{$args{'Requestor'}}) { +	$self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1); +    } +     +    return($self->Id, $ErrStr);  } +# }}} +# {{{ sub Delete + +sub Delete { +    my $self = shift; +    return (0, 'Deleting this object would violate referential integrity.'. +	    ' That\'s bad.'); +} +# }}} -=item id +# {{{ Routines dealing with watchers. -Returns the current value of id.  -(In the database, id is stored as int(11).) +# {{{ Routines dealing with adding new watchers +# {{{ sub AddWatcher + +=head2 AddWatcher + +AddWatcher takes a parameter hash. The keys are as follows: + +Email +Type +Owner + +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.  =cut +sub AddWatcher { +    my $self = shift; +    my %args = ( Email => undef, +		 Type => undef, +		 Owner => undef, +		 @_ +	       ); + +    # {{{ Check ACLS +    #If the watcher we're trying to add is for the current user +    if ( ( $self->CurrentUser->EmailAddress && +           ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or +           ($args{'Owner'} eq $self->CurrentUser->Id)  +        ) { + +	 +	#  If it's an AdminCc and they don't have  +	#   'WatchAsAdminCc' or 'ModifyTicket', bail +	if ($args{'Type'} eq 'AdminCc') { +	    unless ($self->CurrentUserHasRight('ModifyTicket') or  +		    $self->CurrentUserHasRight('WatchAsAdminCc')) { +		return(0, 'Permission Denied'); +	    } +	} + +	#  If it's a Requestor or Cc and they don't have +	#   'Watch' or 'ModifyTicket', bail +	elsif (($args{'Type'} eq 'Cc') or  +	       ($args{'Type'} eq 'Requestor')) { +		    +	    unless ($self->CurrentUserHasRight('ModifyTicket') or  +		    $self->CurrentUserHasRight('Watch')) { +		return(0, 'Permission Denied'); +	    } +	} +	else { +	    $RT::Logger->warn("$self -> AddWatcher hit code". +			      " it never should. We got passed ". +			      " a type of ". $args{'Type'}); +	    return (0,'Error in parameters to TicketAddWatcher'); +	} +    } +    # If the watcher isn't the current user  +    # and the current user  doesn't have 'ModifyTicket' +    # bail +    else { +	unless ($self->CurrentUserHasRight('ModifyTicket')) { +	    return (0, "Permission Denied"); +	} +    } +    # }}} + +    return ($self->_AddWatcher(%args)); +} + -=item EffectiveId +#This contains the meat of AddWatcher. but can be called from a routine like +# Create, which doesn't need the additional acl check +sub _AddWatcher { +    my $self = shift; +    my %args = ( +		Type => undef, +		Silent => undef, +		Email => undef, +		Owner => 0, +		Person => undef, +		@_ ); +     +     +     +    #clear the watchers cache +    $self->{'watchers_cache'} = undef; +     +    if (defined $args{'Person'}) { +	#if it's an RT::User object, pull out the id and shove it in Owner +	if (ref ($args{'Person'}) =~ /RT::User/) { +	    $args{'Owner'} = $args{'Person'}->id; +	}	 +	#if it's an int, shove it in Owner +	elsif ($args{'Person'} =~ /^\d+$/) { +	    $args{'Owner'} = $args{'Person'}; +	} +	#if it's an email address, shove it in Email +       else { +	   $args{'Email'} = $args{'Person'}; +       }	 +    }	 + +    # Turn an email address int a watcher if we possibly can. +    if ($args{'Email'}) { +	my $watcher = new RT::User($self->CurrentUser); +	$watcher->LoadByEmail($args{'Email'}); +	if ($watcher->Id) { +		$args{'Owner'} = $watcher->Id; +		delete $args{'Email'}; +	} +    } + + +    # see if this user is already a watcher. if we have an owner, check it +    # otherwise, we've got an email-address watcher. use that. + +    if ($self->IsWatcher(Type => $args{'Type'}, +                         Id => ($args{'Owner'} || $args{'Email'}) ) ) { + + +        return(0, 'That user is already that sort of watcher for this ticket'); +    } + +     +    require RT::Watcher; +    my $Watcher = new RT::Watcher ($self->CurrentUser); +    my ($retval, $msg) = ($Watcher->Create( Value => $self->Id, +					    Scope => 'Ticket', +					    Email => $args{'Email'}, +					    Type => $args{'Type'}, +					    Owner => $args{'Owner'}, +					  )); +     +    unless ($args{'Silent'}) { +	$self->_NewTransaction( Type => 'AddWatcher', +				NewValue => $Watcher->Email, +				Field => $Watcher->Type); +    } +     +    return ($retval, $msg); +} + +# }}} + +# {{{ sub AddRequestor -Returns the current value of EffectiveId.  -(In the database, EffectiveId is stored as int(11).) +=head2 AddRequestor +AddRequestor takes what AddWatcher does, except it presets +the "Type" parameter to \'Requestor\' +=cut + +sub AddRequestor { +   my $self = shift; +   return ($self->AddWatcher ( Type => 'Requestor', @_)); +} -=item SetEffectiveId VALUE +# }}} +# {{{ sub AddCc -Set EffectiveId to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, EffectiveId will be stored as a int(11).) +=head2 AddCc +AddCc takes what AddWatcher does, except it presets +the "Type" parameter to \'Cc\'  =cut +sub AddCc { +   my $self = shift; +   return ($self->AddWatcher ( Type => 'Cc', @_)); +} +# }}} +	 +# {{{ sub AddAdminCc + +=head2 AddAdminCc -=item Queue +AddAdminCc takes what AddWatcher does, except it presets +the "Type" parameter to \'AdminCc\' -Returns the current value of Queue.  -(In the database, Queue is stored as int(11).) +=cut +sub AddAdminCc { +   my $self = shift; +   return ($self->AddWatcher ( Type => 'AdminCc', @_)); +} +# }}} -=item SetQueue VALUE +# }}} +# {{{ sub DeleteWatcher -Set Queue to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Queue will be stored as a int(11).) +=head2 DeleteWatcher id [type] + +DeleteWatcher takes a single argument which is either an email address  +or a watcher id.   +If the first argument is an email address, you need to specify the watcher type you're talking +about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'. +It removes that watcher from this Ticket\'s list of watchers.  =cut +#TODO It is lame that you can't call this the same way you can call AddWatcher -=item QueueObj +sub DeleteWatcher { +    my $self = shift; +    my $id = shift; + +    my $type; +     +    $type = shift if (@_); +     +    my $Watcher = new RT::Watcher($self->CurrentUser); +     +    #If it\'s a numeric watcherid +    if ($id =~ /^(\d*)$/) { +	$Watcher->Load($id); +    } +     +    #Otherwise, we'll assume it's an email address +    elsif ($type) { +	my ($result, $msg) =  +	  $Watcher->LoadByValue( Email => $id, +				 Scope => 'Ticket', +				 Value => $self->id, +				 Type => $type); +	return (0,$msg) unless ($result); +    } +     +    else { +	return(0,"Can\'t delete a watcher by email address without specifying a type"); +    } +     +    # {{{ Check ACLS  + +    #If the watcher we're trying to delete is for the current user +    if ($Watcher->Email eq $self->CurrentUser->EmailAddress) { +		 +	#  If it's an AdminCc and they don't have  +	#   'WatchAsAdminCc' or 'ModifyTicket', bail +	if ($Watcher->Type eq 'AdminCc') { +	    unless ($self->CurrentUserHasRight('ModifyTicket') or  +		    $self->CurrentUserHasRight('WatchAsAdminCc')) { +		return(0, 'Permission Denied'); +	    } +	} + +	#  If it's a Requestor or Cc and they don't have +	#   'Watch' or 'ModifyTicket', bail +	elsif (($Watcher->Type eq 'Cc') or  +	       ($Watcher->Type eq 'Requestor')) { +		    +	    unless ($self->CurrentUserHasRight('ModifyTicket') or  +		    $self->CurrentUserHasRight('Watch')) { +		return(0, 'Permission Denied'); +	    } +	} +	else { +	    $RT::Logger->warn("$self -> DeleteWatcher hit code". +			      " it never should. We got passed ". +			      " a type of ". $args{'Type'}); +	    return (0,'Error in parameters to $self DeleteWatcher'); +	} +    } +    # If the watcher isn't the current user  +    # and the current user  doesn't have 'ModifyTicket' +    # bail +    else { +	unless ($self->CurrentUserHasRight('ModifyTicket')) { +	    return (0, "Permission Denied"); +	} +    }	 +     +    # }}} +     +    unless (($Watcher->Scope eq 'Ticket') and +	    ($Watcher->Value == $self->id) ) { +	return (0, "Not a watcher for this ticket"); +    } + + +    #Clear out the watchers hash. +    $self->{'watchers'} = undef; +     +    #If we\'ve validated that it is a watcher for this ticket  +    $self->_NewTransaction ( Type => 'DelWatcher',         +			     OldValue => $Watcher->Email, +			     Field => $Watcher->Type, +			   ); +     +    my $retval = $Watcher->Delete(); +     +    unless ($retval) { +	return(0,"Watcher could not be deleted. Database inconsistency possible."); +    } +     +    return(1, "Watcher deleted"); +} -Returns the Queue Object which has the id returned by Queue +# {{{ sub DeleteRequestor + +=head2 DeleteRequestor EMAIL + +Takes an email address. It calls DeleteWatcher with a preset  +type of 'Requestor'  =cut -sub QueueObj { -	my $self = shift; -	my $Queue =  RT::Queue->new($self->CurrentUser); -	$Queue->Load($self->__Value('Queue')); -	return($Queue); +sub DeleteRequestor { +   my $self = shift; +   my $id = shift; +   return ($self->DeleteWatcher ($id, 'Requestor'))  } -=item Type +# }}} -Returns the current value of Type.  -(In the database, Type is stored as varchar(16).) +# {{{ sub DeleteCc +=head2 DeleteCc EMAIL +Takes an email address. It calls DeleteWatcher with a preset  +type of 'Cc' -=item SetType VALUE +=cut + +sub DeleteCc { +   my $self = shift; +   my $id = shift; +   return ($self->DeleteWatcher ($id, 'Cc')) +} -Set Type to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Type will be stored as a varchar(16).) +# }}} +# {{{ sub DeleteAdminCc + +=head2 DeleteAdminCc EMAIL + +Takes an email address. It calls DeleteWatcher with a preset  +type of 'AdminCc' + + +=cut + +sub DeleteAdminCc { +   my $self = shift; +   my $id = shift; +   return ($self->DeleteWatcher ($id, 'AdminCc')) +} + +# }}} + + +# }}} + +# {{{ sub Watchers + +=head2 Watchers + +Watchers returns a Watchers object preloaded with this ticket\'s watchers. + +# It should return only the ticket watchers. the actual FooAsString +# methods capture the queue watchers too. I don't feel thrilled about this, +# but we don't want the Cc Requestors and AdminCc objects to get filled up +# with all the queue watchers too. we've got seperate objects for that. +  # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch?  =cut +sub Watchers { +  my $self = shift; +   +  require RT::Watchers; +  my $watchers=RT::Watchers->new($self->CurrentUser); +  if ($self->CurrentUserHasRight('ShowTicket')) { +      $watchers->LimitToTicket($self->id); +  } +   +  return($watchers); +   +} -=item IssueStatement +# }}} -Returns the current value of IssueStatement.  -(In the database, IssueStatement is stored as int(11).) +# {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string +=head2 RequestorsAsString + B<Returns> String: All Ticket Requestor email addresses as a string. -=item SetIssueStatement VALUE +=cut +sub RequestorsAsString { +    my $self=shift; -Set IssueStatement to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, IssueStatement will be stored as a int(11).) +    unless ($self->CurrentUserHasRight('ShowTicket')) { +        return undef; +    } +     +    return ($self->Requestors->EmailsAsString() ); +} +=head2 WatchersAsString + +B<Returns> String: All Ticket Watchers email addresses as a string  =cut +sub WatchersAsString { +    my $self=shift; -=item Resolution +    unless ($self->CurrentUserHasRight('ShowTicket')) { +	return (0, "Permission Denied"); +    } +     +    return ($self->Watchers->EmailsAsString()); -Returns the current value of Resolution.  -(In the database, Resolution is stored as int(11).) +} +=head2 AdminCcAsString +returns String: All Ticket AdminCc email addresses as a string -=item SetResolution VALUE +=cut -Set Resolution to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Resolution will be stored as a int(11).) +sub AdminCcAsString { +    my $self=shift; +    unless ($self->CurrentUserHasRight('ShowTicket')) { +	return undef; +    } +     +    return ($self->AdminCc->EmailsAsString()); +     +} + +=head2 CcAsString + +returns String: All Ticket Ccs as a string of email addresses  =cut +sub CcAsString { +    my $self=shift; + +    unless ($self->CurrentUserHasRight('ShowTicket')) { +        return undef;  +    } +     +    return ($self->Cc->EmailsAsString()); + +} + +# }}} -=item Owner +# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs -Returns the current value of Owner.  -(In the database, Owner is stored as int(11).) +# {{{ sub Requestors +=head2 Requestors +Takes nothing. +Returns this ticket's Requestors as an RT::Watchers object -=item SetOwner VALUE +=cut +sub Requestors { +    my $self = shift; +     +    my $requestors = $self->Watchers(); +    if ($self->CurrentUserHasRight('ShowTicket')) { +	$requestors->LimitToRequestors(); +    }	 +     +    return($requestors); +     +} -Set Owner to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Owner will be stored as a int(11).) +# }}} +# {{{ sub Cc + +=head2 Cc + +Takes nothing. +Returns a watchers object which contains this ticket's Cc watchers  =cut +sub Cc { +    my $self = shift; +     +    my $cc = $self->Watchers(); +     +    if ($self->CurrentUserHasRight('ShowTicket')) { +	$cc->LimitToCc(); +    } +     +    return($cc); +     +} + +# }}} -=item Subject +# {{{ sub AdminCc -Returns the current value of Subject.  -(In the database, Subject is stored as varchar(200).) +=head2 AdminCc +Takes nothing. +Returns this ticket\'s administrative Ccs as an RT::Watchers object + +=cut + +sub AdminCc { +    my $self = shift; +     +    my $admincc = $self->Watchers(); +    if ($self->CurrentUserHasRight('ShowTicket')) { +	$admincc->LimitToAdminCc(); +    } +    return($admincc); +} +# }}} -=item SetSubject VALUE +# }}} +# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc -Set Subject to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Subject will be stored as a varchar(200).) +# {{{ sub IsWatcher +# a generic routine to be called by IsRequestor, IsCc and IsAdminCc +=head2 IsWatcher + +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 +is a ticket watcher. Returns undef otherwise  =cut +sub IsWatcher { +    my $self = shift; -=item InitialPriority +    my %args = ( Type => 'Requestor', +		 Email => undef, +		 Id => undef, +		 @_ +	       ); +     +    my %cols = ('Type' => $args{'Type'}, +		'Scope' => 'Ticket', +		'Value' => $self->Id, +		'Owner' => undef, +		'Email' => undef +	       ); +     +    if (ref($args{'Id'})){  +	#If it's a ref, it's an RT::User object; +	$cols{'Owner'} = $args{'Id'}->Id; +    } +    elsif ($args{'Id'} =~ /^\d+$/) {  +	# if it's an integer, it's a reference to an RT::User obj +	$cols{'Owner'} = $args{'Id'}; +    } +    else { +	$cols{'Email'} = $args{'Id'}; +    }	 +     +    if ($args{'Email'}) { +	$cols{'Email'} = $args{'Email'}; +    } + +    my $description = join(":",%cols); +     +    #If we've cached a positive match... +    if (defined $self->{'watchers_cache'}->{"$description"}) { +	if ($self->{'watchers_cache'}->{"$description"} == 1) { +	    return(1); +	} +	else { #If we've cached a negative match... +	    return(undef); +	} +    } +     +     +    my $watcher = new RT::Watcher($self->CurrentUser); +    $watcher->LoadByCols(%cols); +     +     +    if ($watcher->id) { +	$self->{'watchers_cache'}->{"$description"} = 1; +	return(1); +    }	 +    else { +	$self->{'watchers_cache'}->{"$description"} = 0; +	return(undef); +    } +     +} +# }}} -Returns the current value of InitialPriority.  -(In the database, InitialPriority is stored as int(11).) +# {{{ sub IsRequestor +=head2 IsRequestor +   +  Takes an email address, RT::User object or integer (RT user id) +  Returns true if the string is a requestor of the current ticket. -=item SetInitialPriority VALUE +=cut +sub IsRequestor { +    my $self = shift; +    my $person = shift; + +    return ($self->IsWatcher(Type => 'Requestor', Id => $person)); +	     +}; -Set InitialPriority to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, InitialPriority will be stored as a int(11).) +# }}} +# {{{ sub IsCc + +=head2 IsCc + +Takes a string. Returns true if the string is a Cc watcher of the current ticket.  =cut +sub IsCc { +  my $self = shift; +  my $cc = shift; +   +  return ($self->IsWatcher( Type => 'Cc', Id => $cc )); +   +} + +# }}} + +# {{{ sub IsAdminCc -=item FinalPriority +=head2 IsAdminCc -Returns the current value of FinalPriority.  -(In the database, FinalPriority is stored as int(11).) +Takes a string. Returns true if the string is an AdminCc watcher of the current ticket. +=cut +sub IsAdminCc { +  my $self = shift; +  my $person = shift; +   +  return ($self->IsWatcher( Type => 'AdminCc', Id => $person )); +   +} -=item SetFinalPriority VALUE +# }}} +# {{{ sub IsOwner -Set FinalPriority to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, FinalPriority will be stored as a int(11).) +=head2 IsOwner +  Takes an RT::User object. Returns true if that user is this ticket's owner. +returns undef otherwise  =cut +sub IsOwner { +    my $self = shift; +    my $person = shift; +   + +    # no ACL check since this is used in acl decisions +    # unless ($self->CurrentUserHasRight('ShowTicket')) { +    #	return(undef); +    #   }	 + +     +    #Tickets won't yet have owners when they're being created. +    unless ($self->OwnerObj->id) { +        return(undef); +    } +     +    if ($person->id == $self->OwnerObj->id) { +	return(1); +    } +    else { +	return(undef); +    } +} + + +# }}} + +# }}} + +# }}} -=item Priority +# {{{ Routines dealing with queues  -Returns the current value of Priority.  -(In the database, Priority is stored as int(11).) +# {{{ sub ValidateQueue +sub ValidateQueue { +  my $self = shift; +  my $Value = shift; +   +  #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue, +  if (!$Value) { +    $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok."); +    return (1); +  } +   +  my $QueueObj = RT::Queue->new($self->CurrentUser); +  my $id = $QueueObj->Load($Value); +   +  if ($id) { +    return (1); +  } +  else { +    return (undef); +  } +} + +# }}} +# {{{ sub SetQueue   -=item SetPriority VALUE +sub SetQueue { +    my $self = shift; +    my $NewQueue = shift; + +    #Redundant. ACL gets checked in _Set; +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    } +    +  +    my $NewQueueObj = RT::Queue->new($self->CurrentUser); +    $NewQueueObj->Load($NewQueue); +     +    unless ($NewQueueObj->Id()) { +	return (0, "That queue does not exist"); +    } +     +    if ($NewQueueObj->Id == $self->QueueObj->Id) { +	return (0, 'That is the same value'); +    } +    unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket', +					      QueueObj => $NewQueueObj )) { +	return (0, "You may not create requests in that queue."); +    } +     +    unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket',   +					   QueueObj => $NewQueueObj)) { +	    $self->Untake(); +    } + +    return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id())); +     +} +# }}} -Set Priority to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Priority will be stored as a int(11).) +# {{{ sub QueueObj +=head2 QueueObj + +Takes nothing. returns this ticket's queue object  =cut +sub QueueObj { +    my $self = shift; +     +    my $queue_obj = RT::Queue->new($self->CurrentUser); +    #We call __Value so that we can avoid the ACL decision and some deep recursion +    my ($result) = $queue_obj->Load($self->__Value('Queue')); +    return ($queue_obj); +} + + +# }}} + +# }}} -=item TimeEstimated +# {{{ Date printing routines -Returns the current value of TimeEstimated.  -(In the database, TimeEstimated is stored as int(11).) +# {{{ sub DueObj +=head2 DueObj +  Returns an RT::Date object containing this ticket's due date -=item SetTimeEstimated VALUE +=cut +sub DueObj { +    my $self = shift; +     +    my $time = new RT::Date($self->CurrentUser); + +    # -1 is RT::Date slang for never +    if ($self->Due) { +	$time->Set(Format => 'sql', Value => $self->Due ); +    } +    else { +	$time->Set(Format => 'unix', Value => -1); +    } +     +    return $time; +} +# }}} +# {{{ sub DueAsString  -Set TimeEstimated to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, TimeEstimated will be stored as a int(11).) +=head2 DueAsString +Returns this ticket's due date as a human readable string  =cut +sub DueAsString { +  my $self = shift; +  return $self->DueObj->AsString(); +} + +# }}} + +# {{{ sub GraceTimeAsString  -=item TimeWorked +=head2 GraceTimeAsString -Returns the current value of TimeWorked.  -(In the database, TimeWorked is stored as int(11).) +Return the time until this ticket is due as a string +=cut + +# TODO This should be deprecated  +sub GraceTimeAsString { +    my $self=shift; +     +    if ($self->Due) { +	return ($self->DueObj->AgeAsString()); +    } else { +	return ""; +    } +} -=item SetTimeWorked VALUE +# }}} -Set TimeWorked to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, TimeWorked will be stored as a int(11).) +# {{{ sub ResolvedObj +=head2 ResolvedObj + +  Returns an RT::Date object of this ticket's 'resolved' time.  =cut +sub ResolvedObj { +  my $self = shift; -=item Status +  my $time = new RT::Date($self->CurrentUser); +  $time->Set(Format => 'sql', Value => $self->Resolved); +  return $time; +} +# }}} -Returns the current value of Status.  -(In the database, Status is stored as varchar(10).) +# {{{ sub SetStarted +=head2 SetStarted +Takes a date in ISO format or undef +Returns a transaction id and a message +The client calls "Start" to note that the project was started on the date in $date. +A null date means "now" -=item SetStatus VALUE +=cut +   +sub SetStarted { +    my $self = shift; +    my $time = shift || 0; +     + +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    } + +    #We create a date object to catch date weirdness +    my $time_obj = new RT::Date($self->CurrentUser()); +    if ($time != 0)  { +	$time_obj->Set(Format => 'ISO', Value => $time); +    } +    else { +	$time_obj->SetToNow(); +    } +     +    #Now that we're starting, open this ticket +    #TODO do we really want to force this as policy? it should be a scrip +     +    #We need $TicketAsSystem, in case the current user doesn't have +    #ShowTicket +    # +    my $TicketAsSystem = new RT::Ticket($RT::SystemUser); +    $TicketAsSystem->Load($self->Id);	 +    if ($TicketAsSystem->Status eq 'new') { +	$TicketAsSystem->Open(); +    }	 +     +    return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO)); +     +} + +# }}} +# {{{ sub StartedObj -Set Status to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Status will be stored as a varchar(10).) +=head2 StartedObj +  Returns an RT::Date object which contains this ticket's  +'Started' time.  =cut -=item TimeLeft +sub StartedObj { +    my $self = shift; +     +    my $time = new RT::Date($self->CurrentUser); +    $time->Set(Format => 'sql', Value => $self->Started); +    return $time; +} +# }}} + +# {{{ sub StartsObj -Returns the current value of TimeLeft.  -(In the database, TimeLeft is stored as int(11).) +=head2 StartsObj +  Returns an RT::Date object which contains this ticket's  +'Starts' time. +=cut -=item SetTimeLeft VALUE +sub StartsObj { +  my $self = shift; +   +  my $time = new RT::Date($self->CurrentUser); +  $time->Set(Format => 'sql', Value => $self->Starts); +  return $time; +} +# }}} +# {{{ sub ToldObj -Set TimeLeft to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, TimeLeft will be stored as a int(11).) +=head2 ToldObj +  Returns an RT::Date object which contains this ticket's  +'Told' time.  =cut -=item Told +sub ToldObj { +  my $self = shift; +   +  my $time = new RT::Date($self->CurrentUser); +  $time->Set(Format => 'sql', Value => $self->Told); +  return $time; +} + +# }}} -Returns the current value of Told.  -(In the database, Told is stored as datetime.) +# {{{ sub LongSinceToldAsString +# TODO this should be deprecated -=item SetTold VALUE +sub LongSinceToldAsString { +  my $self = shift; +  if ($self->Told) { +      return $self->ToldObj->AgeAsString(); +  } else { +      return "Never"; +  } +} +# }}} -Set Told to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Told will be stored as a datetime.) +# {{{ sub ToldAsString +=head2 ToldAsString + +A convenience method that returns ToldObj->AsString + +TODO: This should be deprecated  =cut -=item Starts +sub ToldAsString { +    my $self = shift; +    if ($self->Told) { +	return $self->ToldObj->AsString(); +    } +    else { +	return("Never"); +    } +} +# }}} -Returns the current value of Starts.  -(In the database, Starts is stored as datetime.) +# {{{ sub TimeWorkedAsString +=head2 TimeWorkedAsString +Returns the amount of time worked on this ticket as a Text String -=item SetStarts VALUE +=cut + +sub TimeWorkedAsString { +    my $self=shift; +    return "0" unless $self->TimeWorked; +     +    #This is not really a date object, but if we diff a number of seconds  +    #vs the epoch, we'll get a nice description of time worked. +     +    my $worked = new RT::Date($self->CurrentUser); +    #return the  #of minutes worked turned into seconds and written as +    # a simple text string + +    return($worked->DurationAsString($self->TimeWorked*60)); +} +# }}} -Set Starts to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Starts will be stored as a datetime.) +# }}} + +# {{{ Routines dealing with correspondence/comments + +# {{{ sub Comment + +=head2 Comment + +Comment on this ticket. +Takes a hashref with the follwoing attributes: + +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo  =cut +sub Comment { +  my $self = shift; +   +  my %args = ( +          CcMessageTo => undef, +          BccMessageTo => undef, +	      MIMEObj => undef, +	      TimeTaken => 0, +	      @_ ); + +  unless (($self->CurrentUserHasRight('CommentOnTicket')) or +	  ($self->CurrentUserHasRight('ModifyTicket'))) { +      return (0, "Permission Denied"); +  } +  +   unless ($args{'MIMEObj'}) { +       return(0,"No correspondence attached"); +   } + +  # If we've been passed in CcMessageTo and BccMessageTo fields, +  # add them to the mime object for passing on to the transaction handler +  # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and +  # RT-Send-Bcc: headers +  +  $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'}); +  $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'}); + +  #Record the correspondence (write the transaction) +  my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment', +				      Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'), +				      TimeTaken => $args{'TimeTaken'}, +				      MIMEObj => $args{'MIMEObj'} +				    ); +   +   +  return ($Trans, "The comment has been recorded"); +} -=item Started +# }}} -Returns the current value of Started.  -(In the database, Started is stored as datetime.) +# {{{ sub Correspond +=head2 Correspond +Correspond on this ticket. +Takes a hashref with the following attributes: -=item SetStarted VALUE +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo -Set Started to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Started will be stored as a datetime.) +=cut + +sub Correspond { +    my $self = shift; +    my %args = ( +          CcMessageTo => undef, +          BccMessageTo => undef, +		MIMEObj => undef, +		TimeTaken => 0, +		@_ ); +     +    unless (($self->CurrentUserHasRight('ReplyToTicket')) or +	    ($self->CurrentUserHasRight('ModifyTicket'))) { +	return (0, "Permission Denied"); +    } +     +    unless ($args{'MIMEObj'}) { +	return(0,"No correspondence attached"); +    } +     +  # If we've been passed in CcMessageTo and BccMessageTo fields, +  # add them to the mime object for passing on to the transaction handler +  # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc: +  # headers +  +  $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'}); +  $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'}); + +    #Record the correspondence (write the transaction) +    my ($Trans,$msg, $TransObj) = $self->_NewTransaction +      (Type => 'Correspond', +       Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'), +       TimeTaken => $args{'TimeTaken'}, +       MIMEObj=> $args{'MIMEObj'}      +      ); +     +    # TODO this bit of logic should really become a scrip for 2.2 +    my $TicketAsSystem = new RT::Ticket($RT::SystemUser); +    $TicketAsSystem->Load($self->Id); +	 +    if ( +	($TicketAsSystem->Status ne 'open') and +	($TicketAsSystem->Status ne 'new') +       ) { +	 +	my $oldstatus = $TicketAsSystem->Status(); +	$TicketAsSystem->__Set(Field => 'Status', Value => 'open'); +	$TicketAsSystem->_NewTransaction  +	  ( Type => 'Set', +	    Field => 'Status', +	    OldValue => $oldstatus, +	    NewValue => 'open', +	    Data => 'Ticket auto-opened on incoming correspondence' +	  ); +    } +     +    unless ($Trans) { +	$RT::Logger->err("$self couldn't init a transaction ($msg)\n"); +	return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'}); +    } +     +    #Set the last told date to now if this isn't mail from the requestor. +    #TODO: Note that this will wrongly ack mail from any non-requestor as a "told" +     +    unless ($TransObj->IsInbound) { +	$self->_SetTold; +    } +     +    return ($Trans, "correspondence sent"); +} + +# }}} + +# }}} + +# {{{ Routines dealing with Links and Relations between tickets +# {{{ Link Collections + +# {{{ sub Members + +=head2 Members + +  This returns an RT::Links object which references all the tickets  +which are 'MembersOf' this ticket  =cut +sub Members { +   my $self = shift; +   return ($self->_Links('Target', 'MemberOf')); +} + +# }}} + +# {{{ sub MemberOf -=item Due +=head2 MemberOf -Returns the current value of Due.  -(In the database, Due is stored as datetime.) +  This returns an RT::Links object which references all the tickets that this +ticket is a 'MemberOf' +=cut +sub MemberOf { +   my $self = shift; +   return ($self->_Links('Base', 'MemberOf')); +} -=item SetDue VALUE +# }}} +# {{{ RefersTo -Set Due to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Due will be stored as a datetime.) +=head2 RefersTo +  This returns an RT::Links object which shows all references for which this ticket is a base  =cut +sub RefersTo { +    my $self = shift; +    return ($self->_Links('Base', 'RefersTo')); +} -=item Resolved +# }}} -Returns the current value of Resolved.  -(In the database, Resolved is stored as datetime.) +# {{{ ReferredToBy +=head2 ReferredToBy +  This returns an RT::Links object which shows all references for which this ticket is a target -=item SetResolved VALUE +=cut +sub ReferredToBy { +    my $self = shift; +    return ($self->_Links('Target', 'RefersTo')); +} -Set Resolved to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Resolved will be stored as a datetime.) +# }}} +# {{{ DependedOnBy + +=head2 DependedOnBy + +  This returns an RT::Links object which references all the tickets that depend on this one  =cut +sub DependedOnBy { +    my $self = shift; +    return ($self->_Links('Target','DependsOn')); +} +# }}} -=item LastUpdatedBy +# {{{ DependsOn -Returns the current value of LastUpdatedBy.  -(In the database, LastUpdatedBy is stored as int(11).) +=head2 DependsOn +  This returns an RT::Links object which references all the tickets that this ticket depends on  =cut +sub DependsOn { +   my $self = shift; +    return ($self->_Links('Base','DependsOn')); +} +# }}} -=item LastUpdated +# {{{ sub _Links  -Returns the current value of LastUpdated.  -(In the database, LastUpdated is stored as datetime.) +sub _Links { +    my $self = shift; +     +    #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic --- +    #tobias meant by $f +    my $field = shift; +    my $type =shift || ""; + +    unless ($self->{"$field$type"}) { +	$self->{"$field$type"} = new RT::Links($self->CurrentUser); +	if ($self->CurrentUserHasRight('ShowTicket')) { +	     +	    $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI); +	    $self->{"$field$type"}->Limit(FIELD=>'Type',  +					  VALUE=>$type) if ($type); +	} +    } +    return ($self->{"$field$type"}); +} + +# }}} + +# }}} + + +# {{{ sub DeleteLink  + +=head2 DeleteLink + +Delete a link. takes a paramhash of Base, Target and Type. +Either Base or Target must be null. The null value will  +be replaced with this ticket\'s id + +=cut  + +sub DeleteLink { +    my $self = shift; +    my %args = ( Base =>  undef, +		 Target => undef, +		 Type => undef, +		 @_ ); +     +    #check acls +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +        $RT::Logger->debug("No permission to delete links\n");  +        return (0, 'Permission Denied'); + +     +    } +     +    #we want one of base and target. we don't care which +    #but we only want _one_ + +    if ($args{'Base'} and $args{'Target'}) { +	$RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n"); +	return (0, 'Can\'t specifiy both base and target'); +    } +    elsif ($args{'Base'}) { +	$args{'Target'} = $self->Id(); +    } +    elsif ($args{'Target'}) { +	$args{'Base'} = $self->Id(); +    } +    else {   +        $RT::Logger->debug("$self: Base or Target must be specified\n"); +	return (0, 'Either base or target must be specified'); +    } +      +    my $link = new RT::Link($self->CurrentUser); +    $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n"); +     +    $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'}); +     +     +     +    #it's a real link.  +    if ($link->id) { +        $RT::Logger->debug("We're going to delete link ".$link->id."\n"); +	$link->Delete(); + +	my $TransString= +	  "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}."; +	my ($Trans, $Msg, $TransObj) = $self->_NewTransaction +	  (Type => 'DeleteLink', +	   Field => $args{'Type'}, +	   Data => $TransString, +	   TimeTaken => 0 +	  ); +	 +	return ($linkid, "Link deleted ($TransString)", $transactionid); +    } +    #if it's not a link we can find +    else { +        $RT::Logger->debug("Couldn't find that link\n"); +	return (0, "Link not found"); +    } +} + +# }}} + +# {{{ sub AddLink + +=head2 AddLink + +Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.  =cut +sub AddLink { +    my $self = shift; +    my %args = ( Target => '', +		 Base => '', +	 	 Type => '', +		 @_ ); +     +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    } +     +    if ($args{'Base'} and $args{'Target'}) { +	$RT::Logger->debug("$self tried to delete a link. both base and target were specified\n"); +	return (0, 'Can\'t specifiy both base and target'); +    } +    elsif ($args{'Base'}) { +        $args{'Target'} = $self->Id(); +    } +    elsif ($args{'Target'}) { +	$args{'Base'} = $self->Id(); +    } +    else {   +	return (0, 'Either base or target must be specified'); +    } +     +    # {{{ We don't want references to ourself +    if ($args{Base} eq $args{Target}) { +	return (0, "Can\'t link a ticket to itself"); +    }	 +		 +    # }}} +     +    # If the base isn't a URI, make it a URI.  +    # If the target isn't a URI, make it a URI.  +         +    # {{{ Check if the link already exists - we don't want duplicates +    my $old_link= new RT::Link ($self->CurrentUser); +    $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'}); +    if ($old_link->Id) { +	$RT::Logger->debug("$self Somebody tried to duplicate a link"); +	return ($old_link->id, "Link already exists",0); +    } +    # }}} +     +    # Storing the link in the DB. +    my $link = RT::Link->new($self->CurrentUser); +    my ($linkid) = $link->Create(Target => $args{Target},  +				 Base => $args{Base},  +				 Type => $args{Type}); +     +    unless ($linkid) { +	return (0,"Link could not be created"); +    } +	#Write the transaction +     +    my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}."; +     +    my ($Trans, $Msg, $TransObj) = $self->_NewTransaction +      (Type => 'AddLink', +       Field => $args{'Type'}, +       Data => $TransString, +       TimeTaken => 0 +      ); +     +    return ($Trans, "Link created ($TransString)"); +	 +	 +} +# }}} + +# {{{ sub URI  + +=head2 URI + +Returns this ticket's URI + +=cut + +sub URI { +    my $self = shift; +    return $RT::TicketBaseURI.$self->id; +} + +# }}} -=item Creator +# {{{ sub MergeInto -Returns the current value of Creator.  -(In the database, Creator is stored as int(11).) +=head2 MergeInto +MergeInto take the id of the ticket to merge this ticket into. +=cut + +sub MergeInto { +    my $self = shift; +    my $MergeInto = shift; +     +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    } +     +    # Load up the new ticket. +    my $NewTicket = RT::Ticket->new($RT::SystemUser); +    $NewTicket->Load($MergeInto); + +    # make sure it exists. +    unless (defined $NewTicket->Id) { +	return (0, 'New ticket doesn\'t exist'); +    } + +     +    # Make sure the current user can modify the new ticket. +    unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) { +	$RT::Logger->debug("failed..."); +	return (0, "Permission Denied"); +    } +     +    $RT::Logger->debug("checking if the new ticket has the same id and effective id..."); +    unless ($NewTicket->id == $NewTicket->EffectiveId) { +	$RT::Logger->err('$self trying to merge into '.$NewTicket->Id . +			 ' which is itself merged.\n'); +	return (0, "Can't merge into a merged ticket. ". +		"You should never get this error"); +    } + +     +    # We use EffectiveId here even though it duplicates information from +    # the links table becasue of the massive performance hit we'd take +    # by trying to do a seperate database query for merge info everytime  +    # loaded a ticket.  +     +     +    #update this ticket's effective id to the new ticket's id. +    my ($id_val, $id_msg) = $self->__Set(Field => 'EffectiveId',  +					 Value => $NewTicket->Id()); +     +    unless ($id_val) { +	$RT::Logger->error("Couldn't set effective ID for ".$self->Id. +			   ": $id_msg"); +	return(0,"Merge failed. Couldn't set EffectiveId"); +    } +     +    my ($status_val, $status_msg) = $self->__Set(Field => 'Status', +						 Value => 'resolved'); +     +    unless ($status_val) { +	$RT::Logger->error("$self couldn't set status to resolved.". +			   "RT's Database may be inconsistent."); +    }	     +     +    #make a new link: this ticket is merged into that other ticket. +    $self->AddLink( Type =>'MergedInto', +		    Target => $NewTicket->Id() ); +     +    #add all of this ticket's watchers to that ticket. +    my $watchers = $self->Watchers(); +     +    while (my $watcher = $watchers->Next()) { +	unless ( +		($watcher->Owner &&  +		$NewTicket->IsWatcher (Type => $watcher->Type, +				       Id => $watcher->Owner)) or  +		($watcher->Email &&  +		 $NewTicket->IsWatcher (Type => $watcher->Type, +					Id => $watcher->Email))  +	       ) { +	     +	     +	     +	    $NewTicket->_AddWatcher(Silent => 1,  +				    Type => $watcher->Type,  +				    Email => $watcher->Email, +				    Owner => $watcher->Owner); +	} +    } +     +     +    #find all of the tickets that were merged into this ticket.  +    my $old_mergees = new RT::Tickets($self->CurrentUser); +    $old_mergees->Limit( FIELD => 'EffectiveId', +			 OPERATOR => '=', +			 VALUE => $self->Id ); +     +    #   update their EffectiveId fields to the new ticket's id +    while (my $ticket = $old_mergees->Next()) { +	my ($val, $msg) = $ticket->__Set(Field => 'EffectiveId',  +					 Value => $NewTicket->Id()); +    }	 +    $NewTicket->_SetLastUpdated; + +    return ($TransactionObj, "Merge Successful"); +}   + +# }}} + +# }}} + +# {{{ Routines dealing with keywords + +# {{{ sub KeywordsObj + +=head2 KeywordsObj [KEYWORD_SELECT_ID] + +  Returns an B<RT::ObjectKeywords> object preloaded with this ticket's ObjectKeywords. +If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword +select.  =cut +sub KeywordsObj { +    my $self = shift; +    my $keyword_select;  +     +    $keyword_select = shift if (@_); +     +    use RT::ObjectKeywords; +    my $Keywords = new RT::ObjectKeywords($self->CurrentUser); + +    #ACL check +    if ($self->CurrentUserHasRight('ShowTicket')) { +	$Keywords->LimitToTicket($self->id); +	if ($keyword_select) { +	    $Keywords->LimitToKeywordSelect($keyword_select); +	}	 +    } +    return ($Keywords); +} +# }}} + +# {{{ sub AddKeyword -=item Created +=head2 AddKeyword -Returns the current value of Created.  -(In the database, Created is stored as datetime.) +Takes a paramhash of Keyword and KeywordSelect.  If Keyword is a valid choice +for KeywordSelect, creates a KeywordObject.  If the KeywordSelect says this should +be a single KeywordObject, automatically removes the old value. + Issues: probably doesn't enforce the depth restrictions or make sure that keywords +are coming from the right part of the tree. really should.  =cut +sub AddKeyword { +    my $self = shift; +   #ACL check +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, 'Permission Denied'); +    } +     +    return($self->_AddKeyword(@_)); +     +} + -=item Disabled +# Helper version of AddKeyword without that pesky ACL check +sub _AddKeyword { +    my $self = shift; +    my %args = ( KeywordSelect => undef,  # id of a keyword select record +		 Keyword => undef, #id of the keyword to add +		 Silent => 0, +		 @_ +	       ); +     +    my ($OldValue); + +    #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'} + +    #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue. +     +    my $Keyword = new RT::Keyword($self->CurrentUser); +    unless ($Keyword->Load($args{'Keyword'}) ) { +	$RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n"); +	return(0, "Couldn't load keyword"); +    } +     +    my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser); +    unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) { +	$RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'}); +	return(0, "Couldn't load keywordselect"); +    } +     +    my $Keywords = $self->KeywordsObj($KeywordSelectObj->id); + +    #If the ticket already has this keyword, just get out of here. +    if ($Keywords->HasEntry($Keyword->id)) { +	return(0, "That is already the current value"); +    }	 + +    #If the keywordselect wants this to be a singleton: + +    if ($KeywordSelectObj->Single) { + +	#Whack any old values...keep track of the last value that we get. +	#we shouldn't need a loop ehre, but we do it anyway, to try to  +	# help keep the database clean. +	while (my $OldKey = $Keywords->Next) { +	    $OldValue = $OldKey->KeywordObj->Name; +	    $OldKey->Delete(); +	}	 +	 +	 +    } + +    # create the new objectkeyword  +    my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser); +    my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id, +					 ObjectType => 'Ticket', +					 ObjectId => $self->Id, +					 KeywordSelect => $KeywordSelectObj->Id ); +     + +    # record a single transaction, unless we were told not to +    unless ($args{'Silent'}) { +	my ($TransactionId, $Msg, $TransactionObj) =  +	  $self->_NewTransaction( Type => 'Keyword', +				  Field => $KeywordSelectObj->Id, +				  OldValue => $OldValue, +				  NewValue => $Keyword->Name ); +    } +    return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added.");     + +}	 + +# }}} + +# {{{ sub DeleteKeyword + +=head2 DeleteKeyword +   +  Takes a paramhash. Deletes the Keyword denoted by the I<Keyword> parameter from this +  ticket's object keywords. -Returns the current value of Disabled.  -(In the database, Disabled is stored as smallint(6).) +=cut + +sub DeleteKeyword { +    my $self = shift; +    my %args = ( Keyword => undef, +		 KeywordSelect => undef, +		 @_ ); + +   #ACL check +    unless ($self->CurrentUserHasRight('ModifyTicket')) {     +	return (0, 'Permission Denied'); +    } + +     +    #Load up the ObjectKeyword we\'re talking about +    my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser); +    $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'}, +			       KeywordSelect => $args{'KeywordSelect'}, +			       ObjectType => 'Ticket', +			       ObjectId => $self->id() +			      ); +     +    #if we can\'t find it, bail +    unless ($ObjectKeyword->id) { +	$RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} . +			 " for keywordselect ". $args{'KeywordSelect'} .  +			 "for ticket ".$self->id ); +	return (undef, "Couldn't load keyword while trying to delete it."); +    }; +     +    #record transaction here. +    my ($TransactionId, $Msg, $TransObj) =  +      $self->_NewTransaction( Type => 'Keyword',  +			      OldValue => $ObjectKeyword->KeywordObj->Name); +     +    $ObjectKeyword->Delete(); +     +    return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted."); +     +} +# }}} +# }}} -=item SetDisabled VALUE +# {{{ Routines dealing with ownership +# {{{ sub OwnerObj -Set Disabled to VALUE.  -Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Disabled will be stored as a smallint(6).) +=head2 OwnerObj +Takes nothing and returns an RT::User object of  +this ticket's owner  =cut +sub OwnerObj { +    my $self = shift; +     +    #If this gets ACLed, we lose on a rights check in User.pm and +    #get deep recursion. if we need ACLs here, we need +    #an equiv without ACLs +     +    $owner = new RT::User ($self->CurrentUser); +    $owner->Load($self->__Value('Owner')); +     +    #Return the owner object +    return ($owner); +} + +# }}} + +# {{{ sub OwnerAsString  + +=head2 OwnerAsString + +Returns the owner's email address + +=cut + +sub OwnerAsString { +  my $self = shift; +  return($self->OwnerObj->EmailAddress); + +} + +# }}} + +# {{{ sub SetOwner +=head2 SetOwner + +Takes two arguments: +     the Id or Name of the owner  +and  (optionally) the type of the SetOwner Transaction. It defaults +to 'Give'.  'Steal' is also a valid option. + +=cut + +sub SetOwner { +    my $self = shift; +    my $NewOwner = shift; +    my $Type = shift || "Give"; +     +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    }   +     +    my $NewOwnerObj = RT::User->new($self->CurrentUser); +    my $OldOwnerObj = $self->OwnerObj; +   +    $NewOwnerObj->Load($NewOwner); +    if (!$NewOwnerObj->Id) { +	    return (0, "That user does not exist"); +    } +     +    #If thie ticket has an owner and it's not the current user +     +    if (($Type ne 'Steal' ) and ($Type ne 'Force') and	#If we're not stealing +	($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set +	($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us +	return(0, "You can only reassign tickets that you own or that are unowned"); +    } +     +    #If we've specified a new owner and that user can't modify the ticket +    elsif (($NewOwnerObj->Id) and  +	   (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket', +					 QueueObj => $self->QueueObj, +					 TicketObj => $self)) +	  ) { +	return (0, "That user may not own requests in that queue"); +    } +   +   +    #If the ticket has an owner and it's the new owner, we don't need +    #To do anything +    elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) { +	return(0, "That user already owns that request"); +    } +   +   +    my ($trans,$msg)=$self->_Set(Field => 'Owner', +				 Value => $NewOwnerObj->Id,  +				 TimeTaken => 0, +				 TransactionType => $Type); +   +    if ($trans) { +	$msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name; +    } +    return ($trans, $msg); +	   +} + +# }}} + +# {{{ sub Take + +=head2 Take + +A convenince method to set the ticket's owner to the current user + +=cut + +sub Take { +    my $self = shift; +    return ($self->SetOwner($self->CurrentUser->Id, 'Take')); +} + +# }}} + +# {{{ sub Untake + +=head2 Untake + +Convenience method to set the owner to 'nobody' if the current user is the owner. + +=cut + +sub Untake { +    my $self = shift; +    return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake')); +} +# }}} + +# {{{ sub Steal  + +=head2 Steal + +A convenience method to change the owner of the current ticket to the +current user. Even if it's owned by another user. + +=cut + +sub Steal { +    my $self = shift; +   +    if ($self->IsOwner($self->CurrentUser)) { +	return (0,"You already own this ticket");  +    } else { +	return($self->SetOwner($self->CurrentUser->Id, 'Steal')); +       +    } +   +} + +# }}} + +# }}} + +# {{{ Routines dealing with status + +# {{{ sub ValidateStatus  + +=head2 ValidateStatus STATUS + +Takes a string. Returns true if that status is a valid status for this ticket. +Returns false otherwise. + +=cut + +sub ValidateStatus { +    my $self = shift; +    my $status = shift; + +    #Make sure the status passed in is valid +    unless ($self->QueueObj->IsValidStatus($status)) { +	return (undef); +    } +     +    return (1); + +} + + +# }}} + +# {{{ sub SetStatus + +=head2 SetStatus STATUS + +Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead. + +=cut + +sub SetStatus {  +    my $self = shift; +    my $status = shift; + +    #Check ACL +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, 'Permission Denied'); +    } + +    my $now = new RT::Date($self->CurrentUser); +    $now->SetToNow(); +     +    #If we're changing the status from new, record that we've started +    if (($self->Status =~ /new/) && ($status ne 'new')) { +	#Set the Started time to "now" +	$self->_Set(Field => 'Started', +		    Value => $now->ISO, +		    RecordTransaction => 0); +    } +     + +    if ($status eq 'resolved') { +	#When we resolve a ticket, set the 'Resolved' attribute to now. +	$self->_Set(Field => 'Resolved', +		    Value => $now->ISO,  +		    RecordTransaction => 0); +    } +     +     +    #Actually update the status +    return($self->_Set(Field => 'Status',  +		       Value => $status, +		       TimeTaken => 0, +		       TransactionType => 'Status')); +} + +# }}} + +# {{{ sub Kill + +=head2 Kill + +Takes no arguments. Marks this ticket for garbage collection + +=cut + +sub Kill { +  my $self = shift; +  return ($self->SetStatus('dead')); +  # TODO: garbage collection +} + +# }}} + +# {{{ sub Stall + +=head2 Stall + +Sets this ticket's status to stalled + +=cut + +sub Stall { +  my $self = shift; +  return ($self->SetStatus('stalled')); +} + +# }}} + +# {{{ sub Open + +=head2 Open + +Sets this ticket\'s status to Open + +=cut + +sub Open { +    my $self = shift; +    return ($self->SetStatus('open')); +} + +# }}} + +# {{{ sub Resolve + +=head2 Resolve + +Sets this ticket\'s status to Resolved + +=cut + +sub Resolve { +    my $self = shift; +    return ($self->SetStatus('resolved')); +} + +# }}} + +# }}} + +# {{{ Actions + Routines dealing with transactions + +# {{{ sub SetTold and _SetTold + +=head2 SetTold ISO  [TIMETAKEN] + +Updates the told and records a transaction + +=cut + +sub SetTold { +    my $self=shift; +    my $told; +    $told = shift if (@_); +    my $timetaken=shift || 0; +    +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    } + +    my $datetold = new RT::Date($self->CurrentUser); +    if ($told) { +	$datetold->Set( Format => 'iso', +			Value => $told); +    } +    else { +	 $datetold->SetToNow();  +    } +     +    return($self->_Set(Field => 'Told',  +		       Value => $datetold->ISO, +		       TimeTaken => $timetaken, +		       TransactionType => 'Told')); +} + +=head2 _SetTold + +Updates the told without a transaction or acl check. Useful when we're sending replies. + +=cut + +sub _SetTold { +    my $self=shift; +     +    my $now = new RT::Date($self->CurrentUser); +    $now->SetToNow(); +    #use __Set to get no ACLs ;) +    return($self->__Set(Field => 'Told', +			Value => $now->ISO)); +} + +# }}} + +# {{{ sub Transactions  + +=head2 Transactions + +  Returns an RT::Transactions object of all transactions on this ticket + +=cut +   +sub Transactions { +    my $self = shift; +     +    use RT::Transactions; +    my $transactions = RT::Transactions->new($self->CurrentUser); + +    #If the user has no rights, return an empty object +    if ($self->CurrentUserHasRight('ShowTicket')) { +	my $tickets = $transactions->NewAlias('Tickets'); +	$transactions->Join( ALIAS1 => 'main', +			      FIELD1 => 'Ticket', +			      ALIAS2 => $tickets, +			      FIELD2 => 'id'); +	$transactions->Limit( ALIAS => $tickets, +			      FIELD => 'EffectiveId', +			      VALUE => $self->id()); +        # if the user may not see comments do not return them +        unless ($self->CurrentUserHasRight('ShowTicketComments')) { +            $transactions->Limit( FIELD => 'Type', +                                  OPERATOR => '!=', +                                  VALUE => "Comment"); +        } +    } +     +    return($transactions); +} + +# }}} + +# {{{ sub _NewTransaction + +sub _NewTransaction { +    my $self = shift; +    my %args = ( TimeTaken => 0, +		 Type => undef, +		 OldValue => undef, +		 NewValue => undef, +		 Data => undef, +		 Field => undef, +		 MIMEObj => undef, +		 @_ ); +     +     +    require RT::Transaction; +    my $trans = new RT::Transaction($self->CurrentUser); +    my ($transaction, $msg) =  +      $trans->Create( Ticket => $self->Id, +		      TimeTaken => $args{'TimeTaken'}, +		      Type => $args{'Type'}, +		      Data => $args{'Data'}, +		      Field => $args{'Field'}, +		      NewValue => $args{'NewValue'}, +		      OldValue => $args{'OldValue'}, +		      MIMEObj => $args{'MIMEObj'} +		    ); +     +    $RT::Logger->warning($msg) unless $transaction; +     +    $self->_SetLastUpdated; +     +    if (defined $args{'TimeTaken'} ) { +	$self->_UpdateTimeTaken($args{'TimeTaken'});  +    } +    return($transaction, $msg, $trans); +} + +# }}} + +# }}} + +# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record + +# {{{ sub _ClassAccessible  sub _ClassAccessible {      { -      -        id => -		{read => 1, type => 'int(11)', default => ''}, -        EffectiveId =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Queue =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Type =>  -		{read => 1, write => 1, type => 'varchar(16)', default => ''}, -        IssueStatement =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Resolution =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Owner =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Subject =>  -		{read => 1, write => 1, type => 'varchar(200)', default => '[no subject]'}, -        InitialPriority =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        FinalPriority =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Priority =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        TimeEstimated =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        TimeWorked =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Status =>  -		{read => 1, write => 1, type => 'varchar(10)', default => ''}, -        TimeLeft =>  -		{read => 1, write => 1, type => 'int(11)', default => '0'}, -        Told =>  -		{read => 1, write => 1, type => 'datetime', default => ''}, -        Starts =>  -		{read => 1, write => 1, type => 'datetime', default => ''}, -        Started =>  -		{read => 1, write => 1, type => 'datetime', default => ''}, -        Due =>  -		{read => 1, write => 1, type => 'datetime', default => ''}, -        Resolved =>  -		{read => 1, write => 1, type => 'datetime', default => ''}, -        LastUpdatedBy =>  -		{read => 1, auto => 1, type => 'int(11)', default => '0'}, -        LastUpdated =>  -		{read => 1, auto => 1, type => 'datetime', default => ''}, -        Creator =>  -		{read => 1, auto => 1, type => 'int(11)', default => '0'}, -        Created =>  -		{read => 1, auto => 1, type => 'datetime', default => ''}, -        Disabled =>  -		{read => 1, write => 1, type => 'smallint(6)', default => '0'}, - - } -}; +	EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 }, +	Queue => { 'read' => 1, 'write' => 1 }, +	Requestors => { 'read' => 1, 'write' => 1 }, +	Owner => { 'read' => 1, 'write' => 1 }, +	Subject => { 'read' => 1, 'write' => 1 }, +	InitialPriority => { 'read' => 1, 'write' => 1 }, +	FinalPriority => { 'read' => 1, 'write' => 1 }, +	Priority => { 'read' => 1, 'write' => 1 }, +	Status => { 'read' => 1, 'write' => 1 }, +	TimeWorked => { 'read' => 1, 'write' => 1 }, +	TimeLeft => { 'read' => 1, 'write' => 1 }, +	Created => { 'read' => 1, 'auto' => 1 }, +	Creator => { 'read' => 1,  'auto' => 1 }, +	Told => { 'read' => 1, 'write' => 1 }, +	Resolved => {'read' => 1}, +	Starts => { 'read' => 1, 'write' => 1 }, +	Started => { 'read' => 1, 'write' => 1 }, +	Due => { 'read' => 1, 'write' => 1 }, +	Creator => { 'read' => 1, 'auto' => 1 }, +	Created => { 'read' => 1, 'auto' => 1 }, +	LastUpdatedBy => { 'read' => 1, 'auto' => 1 }, +	LastUpdated => { 'read' => 1, 'auto' => 1 } +    }; + +}     + +# }}} + +# {{{ sub _Set + +sub _Set { +    my $self = shift; +     +    unless ($self->CurrentUserHasRight('ModifyTicket')) { +	return (0, "Permission Denied"); +    } +     +    my %args = (Field => undef, +		Value => undef, +		TimeTaken => 0, +		RecordTransaction => 1, +		TransactionType => 'Set', +		@_ +	       ); +    #if the user is trying to modify the record +     +    #Take care of the old value we really don't want to get in an ACL loop. +    # so ask the super::_Value +    my $Old=$self->SUPER::_Value("$args{'Field'}"); +     +    #Set the new value +    my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'},  +				       Value=> $args{'Value'}); +     +    #If we can't actually set the field to the value, don't record +    # a transaction. instead, get out of here. +    if ($ret==0) {return (0,$msg);} +     +    if ($args{'RecordTransaction'} == 1) { +	 +	my ($Trans, $Msg, $TransObj) =	 +	  $self->_NewTransaction(Type => $args{'TransactionType'}, +				 Field => $args{'Field'}, +				 NewValue => $args{'Value'}, +				 OldValue =>  $Old, +				 TimeTaken => $args{'TimeTaken'}, +				); +      return ($Trans,$TransObj->Description); +    } +    else { +	return ($ret, $msg); +  } +} +# }}} -        eval "require RT::Ticket_Overlay"; -        if ($@ && $@ !~ qr{^Can't locate RT/Ticket_Overlay.pm}) { -            die $@; -        }; +# {{{ sub _Value  -        eval "require RT::Ticket_Vendor"; -        if ($@ && $@ !~ qr{^Can't locate RT/Ticket_Vendor.pm}) { -            die $@; -        }; +=head2 _Value -        eval "require RT::Ticket_Local"; -        if ($@ && $@ !~ qr{^Can't locate RT/Ticket_Local.pm}) { -            die $@; -        }; +Takes the name of a table column. +Returns its value as a string, if the user passes an ACL check +=cut +sub _Value  { + +  my $self = shift; +  my $field = shift; + +   +  #if the field is public, return it. +  if ($self->_Accessible($field, 'public')) { +      #$RT::Logger->debug("Skipping ACL check for $field\n"); +      return($self->SUPER::_Value($field)); +       +  } +   +  #If the current user doesn't have ACLs, don't let em at it.   +   +  unless ($self->CurrentUserHasRight('ShowTicket')) { +      return (undef); +  } +  return($self->SUPER::_Value($field)); +   +} +# }}} -=head1 SEE ALSO +# {{{ sub _UpdateTimeTaken + +=head2 _UpdateTimeTaken -This class allows "overlay" methods to be placed -into the following files _Overlay is for a System overlay by the original author, -_Vendor is for 3rd-party vendor add-ons, while _Local is for site-local customizations.   +This routine will increment the timeworked counter. it should +only be called from _NewTransaction  -These overlay files can contain new subs or subs to replace existing subs in this module. +=cut + +sub _UpdateTimeTaken { +  my $self = shift; +  my $Minutes = shift; +  my ($Total); +    +  $Total = $self->SUPER::_Value("TimeWorked"); +  $Total = ($Total || 0) + ($Minutes || 0); +  $self->SUPER::_Set(Field => "TimeWorked",  +		     Value => $Total); + +  return ($Total); +} -If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line  +# }}} -   no warnings qw(redefine); +# }}} -so that perl does not kick and scream when you redefine a subroutine or variable in your overlay. +# {{{ Routines dealing with ACCESS CONTROL -RT::Ticket_Overlay, RT::Ticket_Vendor, RT::Ticket_Local +# {{{ sub CurrentUserHasRight  + +=head2 CurrentUserHasRight + +  Takes the textual name of a Ticket scoped right (from RT::ACE) and returns +1 if the user has that right. It returns 0 if the user doesn't have that right.  =cut +sub CurrentUserHasRight { +  my $self = shift; +  my $right = shift; +   +  return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(), +			    Right => "$right")); + +} + +# }}} + +# {{{ sub HasRight  + +=head2 HasRight + + Takes a paramhash with the attributes 'Right' and 'Principal' +  'Right' is a ticket-scoped textual right from RT::ACE  +  'Principal' is an RT::User object + +  Returns 1 if the principal has the right. Returns undef if not. + +=cut + +sub HasRight { +    my $self = shift; +    my %args = ( Right => undef, +		 Principal => undef, +		 @_); +     +    unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) { +	$RT::Logger->warning("Principal attrib undefined for Ticket::HasRight"); +    } +     +    return($args{'Principal'}->HasQueueRight(TicketObj => $self, +					     Right => $args{'Right'})); +} + +# }}} + +# }}} +  1; + +=head1 AUTHOR + +Jesse Vincent, jesse@fsck.com + +=head1 SEE ALSO + +RT + +=cut + + | 
