summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Ticket.pm
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Ticket.pm')
-rwxr-xr-xrt/lib/RT/Ticket.pm3004
1 files changed, 3004 insertions, 0 deletions
diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm
new file mode 100755
index 000000000..f7275e4e3
--- /dev/null
+++ b/rt/lib/RT/Ticket.pm
@@ -0,0 +1,3004 @@
+# $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
+#
+
+=head1 NAME
+
+ 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::Queue;
+use RT::User;
+use RT::Record;
+use RT::Link;
+use RT::Links;
+use RT::Date;
+use RT::Watcher;
+
+
+@ISA= qw(RT::Record);
+
+
+=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.
+
+=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);
+
+}
+
+# }}}
+
+# {{{ 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 = (
+ 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");
+ }
+
+
+ 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.');
+}
+# }}}
+
+# {{{ Routines dealing with watchers.
+
+# {{{ 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));
+}
+
+
+#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
+
+=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', @_));
+}
+
+# }}}
+
+# {{{ sub AddCc
+
+=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
+
+AddAdminCc takes what AddWatcher does, except it presets
+the "Type" parameter to \'AdminCc\'
+
+=cut
+
+sub AddAdminCc {
+ my $self = shift;
+ return ($self->AddWatcher ( Type => 'AdminCc', @_));
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub DeleteWatcher
+
+=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
+
+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");
+}
+
+# {{{ sub DeleteRequestor
+
+=head2 DeleteRequestor EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset
+type of 'Requestor'
+
+
+=cut
+
+sub DeleteRequestor {
+ my $self = shift;
+ my $id = shift;
+ return ($self->DeleteWatcher ($id, 'Requestor'))
+}
+
+# }}}
+
+# {{{ sub DeleteCc
+
+=head2 DeleteCc EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset
+type of 'Cc'
+
+
+=cut
+
+sub DeleteCc {
+ my $self = shift;
+ my $id = shift;
+ return ($self->DeleteWatcher ($id, 'Cc'))
+}
+
+# }}}
+
+# {{{ 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);
+
+}
+
+# }}}
+
+# {{{ 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.
+
+=cut
+
+sub RequestorsAsString {
+ my $self=shift;
+
+ 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;
+
+ unless ($self->CurrentUserHasRight('ShowTicket')) {
+ return (0, "Permission Denied");
+ }
+
+ return ($self->Watchers->EmailsAsString());
+
+}
+
+=head2 AdminCcAsString
+
+returns String: All Ticket AdminCc email addresses as a string
+
+=cut
+
+
+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());
+
+}
+
+# }}}
+
+# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
+
+# {{{ sub Requestors
+
+=head2 Requestors
+
+Takes nothing.
+Returns this ticket's Requestors as an RT::Watchers object
+
+=cut
+
+sub Requestors {
+ my $self = shift;
+
+ my $requestors = $self->Watchers();
+ if ($self->CurrentUserHasRight('ShowTicket')) {
+ $requestors->LimitToRequestors();
+ }
+
+ return($requestors);
+
+}
+
+# }}}
+
+# {{{ 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);
+
+}
+
+# }}}
+
+# {{{ sub AdminCc
+
+=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);
+}
+
+# }}}
+
+# }}}
+
+# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
+
+# {{{ 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;
+
+ 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);
+ }
+
+}
+# }}}
+
+# {{{ 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.
+
+
+=cut
+
+sub IsRequestor {
+ my $self = shift;
+ my $person = shift;
+
+ return ($self->IsWatcher(Type => 'Requestor', Id => $person));
+
+};
+
+# }}}
+
+# {{{ 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
+
+=head2 IsAdminCc
+
+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 ));
+
+}
+
+# }}}
+
+# {{{ sub IsOwner
+
+=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);
+ }
+}
+
+
+# }}}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with queues
+
+# {{{ 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
+
+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()));
+
+}
+
+# }}}
+
+# {{{ 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);
+}
+
+
+# }}}
+
+# }}}
+
+# {{{ Date printing routines
+
+# {{{ sub DueObj
+
+=head2 DueObj
+
+ Returns an RT::Date object containing this ticket's due date
+
+=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
+
+=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
+
+=head2 GraceTimeAsString
+
+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 "";
+ }
+}
+
+# }}}
+
+
+# {{{ sub ResolvedObj
+
+=head2 ResolvedObj
+
+ Returns an RT::Date object of this ticket's 'resolved' time.
+
+=cut
+
+sub ResolvedObj {
+ my $self = shift;
+
+ my $time = new RT::Date($self->CurrentUser);
+ $time->Set(Format => 'sql', Value => $self->Resolved);
+ return $time;
+}
+# }}}
+
+# {{{ 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"
+
+=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
+
+=head2 StartedObj
+
+ Returns an RT::Date object which contains this ticket's
+'Started' time.
+
+=cut
+
+
+sub StartedObj {
+ my $self = shift;
+
+ my $time = new RT::Date($self->CurrentUser);
+ $time->Set(Format => 'sql', Value => $self->Started);
+ return $time;
+}
+# }}}
+
+# {{{ sub StartsObj
+
+=head2 StartsObj
+
+ Returns an RT::Date object which contains this ticket's
+'Starts' time.
+
+=cut
+
+sub StartsObj {
+ my $self = shift;
+
+ my $time = new RT::Date($self->CurrentUser);
+ $time->Set(Format => 'sql', Value => $self->Starts);
+ return $time;
+}
+# }}}
+
+# {{{ sub ToldObj
+
+=head2 ToldObj
+
+ Returns an RT::Date object which contains this ticket's
+'Told' time.
+
+=cut
+
+
+sub ToldObj {
+ my $self = shift;
+
+ my $time = new RT::Date($self->CurrentUser);
+ $time->Set(Format => 'sql', Value => $self->Told);
+ return $time;
+}
+
+# }}}
+
+# {{{ sub LongSinceToldAsString
+
+# TODO this should be deprecated
+
+
+sub LongSinceToldAsString {
+ my $self = shift;
+
+ if ($self->Told) {
+ return $self->ToldObj->AgeAsString();
+ } else {
+ return "Never";
+ }
+}
+# }}}
+
+# {{{ sub ToldAsString
+
+=head2 ToldAsString
+
+A convenience method that returns ToldObj->AsString
+
+TODO: This should be deprecated
+
+=cut
+
+
+sub ToldAsString {
+ my $self = shift;
+ if ($self->Told) {
+ return $self->ToldObj->AsString();
+ }
+ else {
+ return("Never");
+ }
+}
+# }}}
+
+# {{{ sub TimeWorkedAsString
+
+=head2 TimeWorkedAsString
+
+Returns the amount of time worked on this ticket as a Text String
+
+=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));
+}
+
+# }}}
+
+
+# }}}
+
+# {{{ 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");
+}
+
+# }}}
+
+# {{{ sub Correspond
+
+=head2 Correspond
+
+Correspond on this ticket.
+Takes a hashref with the following attributes:
+
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
+
+=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
+
+=head2 MemberOf
+
+ 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'));
+}
+
+# }}}
+
+# {{{ RefersTo
+
+=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'));
+}
+
+# }}}
+
+# {{{ ReferredToBy
+
+=head2 ReferredToBy
+
+ This returns an RT::Links object which shows all references for which this ticket is a target
+
+=cut
+
+sub ReferredToBy {
+ my $self = shift;
+ return ($self->_Links('Target', 'RefersTo'));
+}
+
+# }}}
+
+# {{{ 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'));
+}
+
+# }}}
+
+# {{{ DependsOn
+
+=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'));
+}
+
+# }}}
+
+# {{{ sub _Links
+
+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;
+}
+
+# }}}
+
+# {{{ sub MergeInto
+
+=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
+
+=head2 AddKeyword
+
+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(@_));
+
+}
+
+
+# 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.
+
+=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.");
+
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with ownership
+
+# {{{ sub OwnerObj
+
+=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 {
+ {
+ 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);
+ }
+}
+
+# }}}
+
+# {{{ sub _Value
+
+=head2 _Value
+
+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));
+
+}
+
+# }}}
+
+# {{{ sub _UpdateTimeTaken
+
+=head2 _UpdateTimeTaken
+
+This routine will increment the timeworked counter. it should
+only be called from _NewTransaction
+
+=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);
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with ACCESS CONTROL
+
+# {{{ 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
+
+