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.pm3026
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
+
+