X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FTicket_Overlay.pm;h=a294dcafd28fd6b4bc839ba263328d3c37d27963;hp=c88bbc90fe1081d2d8dc81904b8b1ad15cc06440;hb=9c68254528b6f2c7d8c1921b452fa56064783782;hpb=945721f48f74d5cfffef7c7cf3a3d6bc2521f5dd diff --git a/rt/lib/RT/Ticket_Overlay.pm b/rt/lib/RT/Ticket_Overlay.pm index c88bbc90f..a294dcafd 100644 --- a/rt/lib/RT/Ticket_Overlay.pm +++ b/rt/lib/RT/Ticket_Overlay.pm @@ -1,8 +1,14 @@ -# BEGIN LICENSE BLOCK +# BEGIN BPS TAGGED BLOCK {{{ # -# Copyright (c) 1996-2003 Jesse Vincent +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC +# # -# (Except where explictly superceded by other copyright notices) +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: # # 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 @@ -14,13 +20,29 @@ # 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. +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# +# CONTRIBUTION SUBMISSION POLICY: # +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) # -# END LICENSE BLOCK +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} # {{{ Front Material =head1 SYNOPSIS @@ -44,12 +66,16 @@ ok($testqueue->Create( Name => 'ticket tests')); ok($testqueue->Id != 0); use_ok(RT::CustomField); ok(my $testcf = RT::CustomField->new($RT::SystemUser)); -ok($testcf->Create( Name => 'selectmulti', +my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti', Queue => $testqueue->id, - Type => 'SelectMultiple')); -ok($testcf->AddValue ( Name => 'Value1', + Type => 'SelectMultiple'); +ok($ret,"Created the custom field - ".$cmsg); +($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1', SortOrder => '1', - Description => 'A testing value')); + Description => 'A testing value'); + +ok($ret, "Added a value - ".$cmsg); + ok($testcf->AddValue ( Name => 'Value2', SortOrder => '2', Description => 'Another testing value')); @@ -85,8 +111,8 @@ ok($t->CustomFieldValues($testcf->Id)->Count == 0); ok(my $t2 = RT::Ticket->new($RT::SystemUser)); ok($t2->Load($id)); -ok($t2->Subject eq 'Testing'); -ok($t2->QueueObj->Id eq $testqueue->id); +is($t2->Subject, 'Testing'); +is($t2->QueueObj->Id, $testqueue->id); ok($t2->OwnerObj->Id == $u->Id); my $t3 = RT::Ticket->new($RT::SystemUser); @@ -111,6 +137,9 @@ ok($t3->CustomFieldValues($testcf->Id)->Count == 1, =cut + +package RT::Ticket; + use strict; no warnings qw(redefine); @@ -120,10 +149,11 @@ use RT::Record; use RT::Links; use RT::Date; use RT::CustomFields; -use RT::TicketCustomFieldValues; use RT::Tickets; +use RT::Transactions; use RT::URI::fsck_com_rt; use RT::URI; +use MIME::Entity; =begin testing @@ -137,7 +167,7 @@ ok(require RT::Ticket, "Loading the RT::Ticket library"); # }}} # {{{ LINKTYPEMAP -# A helper table for relationships mapping to make it easier +# A helper table for links mapping to make it easier # to build and parse links between tickets use vars '%LINKTYPEMAP'; @@ -145,8 +175,12 @@ use vars '%LINKTYPEMAP'; %LINKTYPEMAP = ( MemberOf => { Type => 'MemberOf', Mode => 'Target', }, + Parents => { Type => 'MemberOf', + Mode => 'Target', }, Members => { Type => 'MemberOf', Mode => 'Base', }, + Children => { Type => 'MemberOf', + Mode => 'Base', }, HasMember => { Type => 'MemberOf', Mode => 'Base', }, RefersTo => { Type => 'RefersTo', @@ -157,13 +191,15 @@ use vars '%LINKTYPEMAP'; Mode => 'Target', }, DependedOnBy => { Type => 'DependsOn', Mode => 'Base', }, + MergedInto => { Type => 'MergedInto', + Mode => 'Target', }, ); # }}} # {{{ LINKDIRMAP -# A helper table for relationships mapping to make it easier +# A helper table for links mapping to make it easier # to build and parse links between tickets use vars '%LINKDIRMAP'; @@ -175,11 +211,16 @@ use vars '%LINKDIRMAP'; Target => 'ReferredToBy', }, DependsOn => { Base => 'DependsOn', Target => 'DependedOnBy', }, + MergedInto => { Base => 'MergedInto', + Target => 'MergedInto', }, ); # }}} +sub LINKTYPEMAP { return \%LINKTYPEMAP } +sub LINKDIRMAP { return \%LINKDIRMAP } + # {{{ sub Load =head2 Load @@ -197,8 +238,9 @@ sub Load { #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+)$/ ) { + if ( $RT::TicketBaseURI && $id =~ /^$RT::TicketBaseURI(\d+)$/ ) { $id = $1; } @@ -209,21 +251,23 @@ sub Load { #If we have an integer URI, load the ticket if ( $id =~ /^\d+$/ ) { - my $ticketid = $self->LoadById($id); + my ($ticketid,$msg) = $self->LoadById($id); - unless ($ticketid) { - $RT::Logger->debug("$self tried to load a bogus ticket: $id\n"); + unless ($self->Id) { + $RT::Logger->crit("$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 { + $RT::Logger->warning("Tried to load a bogus ticket id: '$id'"); return (undef); } #If we're merged, resolve the merge. if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) { + $RT::Logger->debug ("We found a merged ticket.". $self->id ."/".$self->EffectiveId); return ( $self->Load( $self->EffectiveId ) ); } @@ -265,12 +309,13 @@ Arguments: ARGS is a hash of named parameters. Valid parameters are: id 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 + Requestor - A reference to a list of email addresses or RT user Names + Cc - A reference to a list of email addresses or Names + AdminCc - A reference to a list of 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 + Priority -- an integer from 0 to 99 InitialPriority -- an integer from 0 to 99 FinalPriority -- an integer from 0 to 99 Status -- any valid status (Defined in RT::Queue) @@ -304,37 +349,37 @@ ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj-> sub Create { my $self = shift; - my %args = ( id => undef, - Queue => undef, - Requestor => undef, - Cc => undef, - AdminCc => undef, - Type => 'ticket', - Owner => undef, - Subject => '', - InitialPriority => undef, - FinalPriority => undef, - Status => 'new', - TimeWorked => "0", - TimeLeft => 0, - TimeEstimated => 0, - Due => undef, - Starts => undef, - Started => undef, - Resolved => undef, - MIMEObj => undef, - _RecordTransaction => 1, - - - - @_ ); + my %args = ( + id => undef, + EffectiveId => undef, + Queue => undef, + Requestor => undef, + Cc => undef, + AdminCc => undef, + Type => 'ticket', + Owner => undef, + Subject => '', + InitialPriority => undef, + FinalPriority => undef, + Priority => undef, + Status => 'new', + TimeWorked => "0", + TimeLeft => 0, + TimeEstimated => 0, + Due => undef, + Starts => undef, + Started => undef, + Resolved => undef, + MIMEObj => undef, + _RecordTransaction => 1, + @_ + ); my ( $ErrStr, $Owner, $resolved ); my (@non_fatal_errors); my $QueueObj = RT::Queue->new($RT::SystemUser); - if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) { $QueueObj->Load( $args{'Queue'} ); } @@ -342,9 +387,8 @@ sub Create { $QueueObj->Load( $args{'Queue'}->Id ); } else { - $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object."); + $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object." ); } -; #Can't create a ticket without a queue. unless ( defined($QueueObj) && $QueueObj->Id ) { @@ -353,30 +397,38 @@ sub Create { } #Now that we have a queue, Check the ACLS - unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', - Object => $QueueObj ) - ) { - return ( 0, 0, - $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name ) ); + unless ( + $self->CurrentUser->HasRight( + Right => 'CreateTicket', + Object => $QueueObj + ) + ) + { + return ( + 0, 0, + $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name)); } unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) { return ( 0, 0, $self->loc('Invalid value for status') ); } - #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'} ); + unless ( $args{'InitialPriority'} ); - #Final priority + #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'} ); + unless ( $args{'FinalPriority'} ); + + # Priority may have changed from InitialPriority, for the case + # where we're importing tickets (eg, from an older RT version.) + my $priority = $args{'Priority'} || $args{'InitialPriority'}; # {{{ Dates #TODO we should see what sort of due date we're getting, rather + @@ -386,32 +438,35 @@ sub Create { my $Due = new RT::Date( $self->CurrentUser ); if ( $args{'Due'} ) { - $Due->Set( Format => 'ISO', Value => $args{'Due'} ); + $Due->Set( Format => 'ISO', Value => $args{'Due'} ); } - elsif ( $QueueObj->DefaultDueIn ) { + elsif ( my $due_in = $QueueObj->DefaultDueIn ) { $Due->SetToNow; - $Due->AddDays( $QueueObj->DefaultDueIn ); + $Due->AddDays( $due_in ); } my $Starts = new RT::Date( $self->CurrentUser ); if ( defined $args{'Starts'} ) { - $Starts->Set( Format => 'ISO', Value => $args{'Starts'} ); + $Starts->Set( Format => 'ISO', Value => $args{'Starts'} ); } my $Started = new RT::Date( $self->CurrentUser ); if ( defined $args{'Started'} ) { - $Started->Set( Format => 'ISO', Value => $args{'Started'} ); + $Started->Set( Format => 'ISO', Value => $args{'Started'} ); } my $Resolved = new RT::Date( $self->CurrentUser ); if ( defined $args{'Resolved'} ) { - $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} ); + $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} ); } - #If the status is an inactive status, set the resolved date - if ($QueueObj->IsInactiveStatus($args{'Status'}) && !$args{'Resolved'}) { - $RT::Logger->debug("Got a ".$args{'Status'} . "ticket with a resolved of ".$args{'Resolved'}); + if ( $QueueObj->IsInactiveStatus( $args{'Status'} ) && !$args{'Resolved'} ) + { + $RT::Logger->debug( "Got a " + . $args{'Status'} + . "ticket with a resolved of " + . $args{'Resolved'} ); $Resolved->SetToNow; } @@ -432,29 +487,44 @@ sub Create { } #If we've been handed something else, try to load the user. - elsif ( defined $args{'Owner'} ) { + elsif ( $args{'Owner'} ) { $Owner = RT::User->new( $self->CurrentUser ); $Owner->Load( $args{'Owner'} ); + push( @non_fatal_errors, + $self->loc("Owner could not be set.") . " " + . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} ) + ) + unless ( $Owner->Id ); } - #If we have a proposed owner and they don't have the right + #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 ) - and ( $Owner->Id != $RT::Nobody->Id ) - and ( !$Owner->HasRight( Object => $QueueObj, - Right => 'OwnTicket' ) ) - ) { + if ( + ( defined($Owner) ) + and ( $Owner->Id ) + and ( $Owner->Id != $RT::Nobody->Id ) + and ( + !$Owner->HasRight( + Object => $QueueObj, + Right => 'OwnTicket' + ) + ) + ) + { $RT::Logger->warning( "User " - . $Owner->Name . "(" - . $Owner->id - . ") was proposed " - . "as a ticket owner but has no rights to own " - . "tickets in ".$QueueObj->Name ); + . $Owner->Name . "(" + . $Owner->id + . ") was proposed " + . "as a ticket owner but has no rights to own " + . "tickets in " + . $QueueObj->Name ); - push @non_fatal_errors, $self->loc("Invalid owner. Defaulting to 'nobody'."); + push @non_fatal_errors, + $self->loc( "Owner '[_1]' does not have rights to own this ticket.", + $Owner->Name + ); $Owner = undef; } @@ -467,106 +537,133 @@ sub Create { # }}} - # We attempt to load or create each of the people who might have a role for this ticket - # _outside_ the transaction, so we don't get into ticket creation races +# We attempt to load or create each of the people who might have a role for this ticket +# _outside_ the transaction, so we don't get into ticket creation races foreach my $type ( "Cc", "AdminCc", "Requestor" ) { - next unless (defined $args{$type}); - foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) { - my $user = RT::User->new($RT::SystemUser); - $user->LoadOrCreateByEmail($watcher) if ($watcher !~ /^\d+$/); + next unless ( defined $args{$type} ); + foreach my $watcher ( + ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) + { + my $user = RT::User->new($RT::SystemUser); + $user->LoadOrCreateByEmail($watcher) + if ( $watcher && $watcher !~ /^\d+$/ ); } } - $RT::Handle->BeginTransaction(); - my %params =( Queue => $QueueObj->Id, - Owner => $Owner->Id, - Subject => $args{'Subject'}, - InitialPriority => $args{'InitialPriority'}, - FinalPriority => $args{'FinalPriority'}, - Priority => $args{'InitialPriority'}, - Status => $args{'Status'}, - TimeWorked => $args{'TimeWorked'}, - TimeEstimated => $args{'TimeEstimated'}, - TimeLeft => $args{'TimeLeft'}, - Type => $args{'Type'}, - Starts => $Starts->ISO, - Started => $Started->ISO, - Resolved => $Resolved->ISO, - Due => $Due->ISO ); - - # Parameters passed in during an import that we probably don't want to touch, otherwise + my %params = ( + Queue => $QueueObj->Id, + Owner => $Owner->Id, + Subject => $args{'Subject'}, + InitialPriority => $args{'InitialPriority'}, + FinalPriority => $args{'FinalPriority'}, + Priority => $priority, + Status => $args{'Status'}, + TimeWorked => $args{'TimeWorked'}, + TimeEstimated => $args{'TimeEstimated'}, + TimeLeft => $args{'TimeLeft'}, + Type => $args{'Type'}, + Starts => $Starts->ISO, + Started => $Started->ISO, + Resolved => $Resolved->ISO, + Due => $Due->ISO + ); + +# Parameters passed in during an import that we probably don't want to touch, otherwise foreach my $attr qw(id Creator Created LastUpdated LastUpdatedBy) { - $params{$attr} = $args{$attr} if ($args{$attr}); + $params{$attr} = $args{$attr} if ( $args{$attr} ); } # Delete null integer parameters - foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) { - delete $params{$attr} unless (exists $params{$attr} && $params{$attr}); + foreach my $attr + qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) { + delete $params{$attr} + unless ( exists $params{$attr} && $params{$attr} ); } - - my $id = $self->SUPER::Create( %params); + # Delete the time worked if we're counting it in the transaction + delete $params{TimeWorked} if $args{'_RecordTransaction'}; + + my ($id,$ticket_message) = $self->SUPER::Create( %params); unless ($id) { - $RT::Logger->crit( "Couldn't create a ticket"); + $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message ); $RT::Handle->Rollback(); - return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") ); + return ( 0, 0, + $self->loc("Ticket could not be created due to an internal error") + ); } #Set the ticket's effective ID now that we've created it. - my ( $val, $msg ) = $self->__Set( Field => 'EffectiveId', Value => $id ); + my ( $val, $msg ) = $self->__Set( + Field => 'EffectiveId', + Value => ( $args{'EffectiveId'} || $id ) + ); unless ($val) { $RT::Logger->crit("$self ->Create couldn't set EffectiveId: $msg\n"); $RT::Handle->Rollback(); - return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error") ); + return ( 0, 0, + $self->loc("Ticket could not be created due to an internal error") + ); } my $create_groups_ret = $self->_CreateTicketGroups(); unless ($create_groups_ret) { $RT::Logger->crit( "Couldn't create ticket groups for ticket " - . $self->Id - . ". aborting Ticket creation." ); + . $self->Id + . ". aborting Ticket creation." ); $RT::Handle->Rollback(); return ( 0, 0, - $self->loc( "Ticket could not be created due to an internal error") ); + $self->loc("Ticket could not be created due to an internal error") + ); } - # Set the owner in the Groups table - # We denormalize it into the Ticket table too because doing otherwise would - # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization +# Set the owner in the Groups table +# We denormalize it into the Ticket table too because doing otherwise would +# kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization - $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId , InsideTransaction => 1); + $self->OwnerGroup->_AddMember( + PrincipalId => $Owner->PrincipalId, + InsideTransaction => 1 + ); # {{{ Deal with setting up watchers - foreach my $type ( "Cc", "AdminCc", "Requestor" ) { - next unless (defined $args{$type}); - foreach my $watcher ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) { + next unless ( defined $args{$type} ); + foreach my $watcher ( + ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) + { - # we reason that all-digits number must be a principal id, not email - # this is the only way to can add - my $field = 'Email'; - $field = 'PrincipalId' if $watcher =~ /^\d+$/; + # If there is an empty entry in the list, let's get out of here. + next unless $watcher; - my ( $wval, $wmsg ); + # we reason that all-digits number must be a principal id, not email + # this is the only way to can add + my $field = 'Email'; + $field = 'PrincipalId' if $watcher =~ /^\d+$/; + + my ( $wval, $wmsg ); if ( $type eq '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 - ( $wval, $wmsg ) = $self->AddWatcher( Type => $type, - $field => $watcher, - Silent => 1 ); + # 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 + ( $wval, $wmsg ) = $self->AddWatcher( + Type => $type, + $field => $watcher, + Silent => 1 + ); } else { - ( $wval, $wmsg ) = $self->_AddWatcher( Type => $type, - $field => $watcher, - Silent => 1 ); + ( $wval, $wmsg ) = $self->_AddWatcher( + Type => $type, + $field => $watcher, + Silent => 1 + ); } push @non_fatal_errors, $wmsg unless ($wval); @@ -576,13 +673,12 @@ sub Create { # }}} # {{{ Deal with setting up links - foreach my $type ( keys %LINKTYPEMAP ) { - next unless (defined $args{$type}); + next unless ( defined $args{$type} ); foreach my $link ( ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) { - my ( $wval, $wmsg ) = $self->AddLink( + my ( $wval, $wmsg ) = $self->_AddLink( Type => $LINKTYPEMAP{$type}->{'Type'}, $LINKTYPEMAP{$type}->{'Mode'} => $link, Silent => 1 @@ -594,47 +690,58 @@ sub Create { # }}} - # {{{ Add all the custom fields + # {{{ Add all the custom fields foreach my $arg ( keys %args ) { - next unless ( $arg =~ /^CustomField-(\d+)$/i ); - my $cfid = $1; - foreach - my $value ( ref( $args{$arg} ) ? @{ $args{$arg} } : ( $args{$arg} ) ) { - next unless ($value); - $self->_AddCustomFieldValue( Field => $cfid, - Value => $value, - RecordTransaction => 0 - ); - } + next unless ( $arg =~ /^CustomField-(\d+)$/i ); + my $cfid = $1; + foreach + my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) ) + { + next unless ( length($value) ); + + # Allow passing in uploaded LargeContent etc by hash reference + $self->_AddCustomFieldValue( + (UNIVERSAL::isa( $value => 'HASH' ) + ? %$value + : (Value => $value) + ), + Field => $cfid, + RecordTransaction => 0, + ); + } } + # }}} if ( $args{'_RecordTransaction'} ) { + # {{{ Add a transaction for the create my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( Type => "Create", - TimeTaken => 0, + TimeTaken => $args{'TimeWorked'}, MIMEObj => $args{'MIMEObj'} ); - if ( $self->Id && $Trans ) { - $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name ); - $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors ); - $RT::Logger->info("Ticket ".$self->Id. " created in queue '".$QueueObj->Name."' by ".$self->CurrentUser->Name); + $TransObj->UpdateCustomFields(ARGSRef => \%args); + + $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name ); + $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name ); + $ErrStr = join( "\n", $ErrStr, @non_fatal_errors ); } else { $RT::Handle->Rollback(); - # TODO where does this get errstr from? + $ErrStr = join( "\n", $ErrStr, @non_fatal_errors ); $RT::Logger->error("Ticket couldn't be created: $ErrStr"); return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error")); } $RT::Handle->Commit(); return ( $self->Id, $TransObj->Id, $ErrStr ); + # }}} } else { @@ -642,8 +749,8 @@ sub Create { # Not going to record a transaction $RT::Handle->Commit(); $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name ); - $ErrStr = join ( "\n", $ErrStr, @non_fatal_errors ); - return ( $self->Id, $0, $ErrStr ); + $ErrStr = join( "\n", $ErrStr, @non_fatal_errors ); + return ( $self->Id, 0, $ErrStr ); } } @@ -651,170 +758,6 @@ sub Create { # }}} -# {{{ sub CreateFromEmailMessage - - -=head2 CreateFromEmailMessage { Message, Queue, ExtractActorFromHeaders } - -This code replaces what was once a large part of the email gateway. -It takes an email message as a parameter, parses out the sender, subject -and a MIME object. It then creates a ticket based on those attributes - -=cut - -sub CreateFromEmailMessage { - my $self = shift; - my %args = ( Message => undef, - Queue => undef, - ExtractActorFromSender => undef, - @_ ); - - - # Pull out requestor - - # Pull out Cc? - - # - - -} - -# }}} - - -# {{{ CreateFrom822 - -=head2 FORMAT - -CreateTickets uses the template as a template for an ordered set of tickets -to create. The basic format is as follows: - - - ===Create-Ticket: identifier - Param: Value - Param2: Value - Param3: Value - Content: Blah - blah - blah - ENDOFCONTENT -=head2 Acceptable fields - -A complete list of acceptable fields for this beastie: - - - * Queue => Name or id# of a queue - Subject => A text string - Status => A valid status. defaults to 'new' - - Due => Dates can be specified in seconds since the epoch - to be handled literally or in a semi-free textual - format which RT will attempt to parse. - Starts => - Started => - Resolved => - Owner => Username or id of an RT user who can and should own - this ticket - + Requestor => Email address - + Cc => Email address - + AdminCc => Email address - TimeWorked => - TimeEstimated => - TimeLeft => - InitialPriority => - FinalPriority => - Type => - + DependsOn => - + DependedOnBy => - + RefersTo => - + ReferredToBy => - + Members => - + MemberOf => - Content => content. Can extend to multiple lines. Everything - within a template after a Content: header is treated - as content until we hit a line containing only - ENDOFCONTENT - ContentType => the content-type of the Content field - CustomField- => custom field value - -Fields marked with an * are required. - -Fields marked with a + man have multiple values, simply -by repeating the fieldname on a new line with an additional value. - - -When parsed, field names are converted to lowercase and have -s stripped. -Refers-To, RefersTo, refersto, refers-to and r-e-f-er-s-tO will all -be treated as the same thing. - - -=begin testing - -use_ok(RT::Ticket); - -=end testing - - -=cut - -sub CreateFrom822 { - my $self = shift; - my $content = shift; - - - - my %args = $self->_Parse822HeadersForAttributes($content); - - # Now we have a %args to work with. - # Make sure we have at least the minimum set of - # reasonable data and do our thang - my $ticket = RT::Ticket->new($RT::SystemUser); - - my %ticketargs = ( - Queue => $args{'queue'}, - Subject => $args{'subject'}, - Status => $args{'status'}, - Due => $args{'due'}, - Starts => $args{'starts'}, - Started => $args{'started'}, - Resolved => $args{'resolved'}, - Owner => $args{'owner'}, - Requestor => $args{'requestor'}, - Cc => $args{'cc'}, - AdminCc => $args{'admincc'}, - TimeWorked => $args{'timeworked'}, - TimeEstimated => $args{'timeestimated'}, - TimeLeft => $args{'timeleft'}, - InitialPriority => $args{'initialpriority'}, - FinalPriority => $args{'finalpriority'}, - Type => $args{'type'}, - DependsOn => $args{'dependson'}, - DependedOnBy => $args{'dependedonby'}, - RefersTo => $args{'refersto'}, - ReferredToBy => $args{'referredtoby'}, - Members => $args{'members'}, - MemberOf => $args{'memberof'}, - MIMEObj => $args{'mimeobj'} - ); - - # Add custom field entries to %ticketargs. - # TODO: allow named custom fields - map { - /^customfield-(\d+)$/ - && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} ); - } keys(%args); - - my ( $id, $transid, $msg ) = $ticket->Create(%ticketargs); - unless ($id) { - $RT::Logger->error( "Couldn't create a related ticket for " - . $self->TicketObj->Id . " " - . $msg ); - } - - return (1); -} - -# }}} # {{{ UpdateFrom822 @@ -832,8 +775,8 @@ AddRequestor: jesse\@example.com EOF my $ticket = RT::Ticket->new($RT::SystemUser); -$ticket->Create(Subject => 'first', Queue => 'general'); -ok($ticket->Id, "Created the test ticket"); +my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general'); +ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg); $ticket->UpdateFrom822($simple_update); is($ticket->Subject, 'target', "changed the subject"); my $jesse = RT::User->new($RT::SystemUser); @@ -930,7 +873,6 @@ sub UpdateFrom822 { $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id ); } - # die "updaterecordobject is a webui thingy"; my @results; foreach my $attribute (@attribs) { @@ -970,7 +912,7 @@ sub UpdateFrom822 { # If we've been given a number of delresses to del, do it. foreach my $address (@{$ticketargs{'Del'.$type}}) { - my ($id, $msg) = $self->DelWatcher( Type => $type, Email => $address); + my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address); push (@results, $msg) ; } @@ -1222,16 +1164,24 @@ sub Import { } } + my $create_groups_ret = $self->_CreateTicketGroups(); + unless ($create_groups_ret) { + $RT::Logger->crit( + "Couldn't create ticket groups for ticket " . $self->Id ); + } + + $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId ); + my $watcher; foreach $watcher ( @{ $args{'Cc'} } ) { - $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1 ); + $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 ); } foreach $watcher ( @{ $args{'AdminCc'} } ) { - $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, + $self->_AddWatcher( Type => 'AdminCc', Email => $watcher, Silent => 1 ); } foreach $watcher ( @{ $args{'Requestor'} } ) { - $self->_AddWatcher( Type => 'Requestor', Person => $watcher, + $self->_AddWatcher( Type => 'Requestor', Email => $watcher, Silent => 1 ); } @@ -1240,14 +1190,13 @@ sub Import { # }}} - # {{{ Routines dealing with watchers. # {{{ _CreateTicketGroups =head2 _CreateTicketGroups -Create the ticket groups and relationships for this ticket. +Create the ticket groups and links for this ticket. This routine expects to be called from Ticket->Create _inside of a transaction_ It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner. @@ -1366,6 +1315,10 @@ sub AddWatcher { @_ ); + # XXX, FIXME, BUG: if only email is provided then we only check + # for ModifyTicket right, but must try to get PrincipalId and + # check Watch* rights too if user exist + # {{{ Check ACLS #If the watcher we're trying to add is for the current user if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) { @@ -1388,7 +1341,7 @@ sub AddWatcher { } } else { - $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type"); + $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type"); return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') ); } } @@ -1424,6 +1377,10 @@ sub _AddWatcher { if ($args{'Email'}) { my $user = RT::User->new($RT::SystemUser); my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'}); + # If we can't load the user by email address, let's try to load by username + unless ($pid) { + ($pid,$msg) = $user->Load($args{'Email'}) + } if ($pid) { $args{'PrincipalId'} = $pid; } @@ -1496,61 +1453,66 @@ Email (the email address of an existing wathcer) sub DeleteWatcher { my $self = shift; - my %args = ( Type => undef, + my %args = ( Type => undef, PrincipalId => undef, - Email => undef, + Email => undef, @_ ); - unless ($args{'PrincipalId'} || $args{'Email'} ) { - return(0, $self->loc("No principal specified")); + unless ( $args{'PrincipalId'} || $args{'Email'} ) { + return ( 0, $self->loc("No principal specified") ); } - my $principal = RT::Principal->new($self->CurrentUser); - if ($args{'PrincipalId'} ) { + my $principal = RT::Principal->new( $self->CurrentUser ); + if ( $args{'PrincipalId'} ) { - $principal->Load($args{'PrincipalId'}); - } else { - my $user = RT::User->new($self->CurrentUser); - $user->LoadByEmail($args{'Email'}); - $principal->Load($user->Id); + $principal->Load( $args{'PrincipalId'} ); + } + else { + my $user = RT::User->new( $self->CurrentUser ); + $user->LoadByEmail( $args{'Email'} ); + $principal->Load( $user->Id ); } + # If we can't find this watcher, we need to bail. - unless ($principal->Id) { - return(0, $self->loc("Could not find that principal")); + unless ( $principal->Id ) { + return ( 0, $self->loc("Could not find that principal") ); } - my $group = RT::Group->new($self->CurrentUser); - $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id); - unless ($group->id) { - return(0,$self->loc("Group not found")); + my $group = RT::Group->new( $self->CurrentUser ); + $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id ); + unless ( $group->id ) { + return ( 0, $self->loc("Group not found") ); } # {{{ Check ACLS #If the watcher we're trying to add is for the current user - if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'}) { - # If it's an AdminCc and they don't have + if ( $self->CurrentUser->PrincipalId eq $args{'PrincipalId'} ) { + + # 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, $self->loc('Permission Denied')) + unless ( $self->CurrentUserHasRight('ModifyTicket') + or $self->CurrentUserHasRight('WatchAsAdminCc') ) { + return ( 0, $self->loc('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, $self->loc('Permission Denied')) + elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) + { + unless ( $self->CurrentUserHasRight('ModifyTicket') + or $self->CurrentUserHasRight('Watch') ) { + return ( 0, $self->loc('Permission Denied') ); } } else { - $RT::Logger->warn( "$self -> DeleteWatcher got passed a bogus type"); - return ( 0, $self->loc('Error in parameters to Ticket->DelWatcher') ); + $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type"); + return ( 0, + $self->loc('Error in parameters to Ticket->DeleteWatcher') ); } } - # If the watcher isn't the current user + # If the watcher isn't the current user # and the current user doesn't have 'ModifyTicket' bail else { unless ( $self->CurrentUserHasRight('ModifyTicket') ) { @@ -1560,111 +1522,197 @@ sub DeleteWatcher { # }}} - # see if this user is already a watcher. - unless ( $group->HasMember($principal)) { - return ( 0, - $self->loc('That principal is not a [_1] for this ticket', $args{'Type'}) ); + unless ( $group->HasMember($principal) ) { + return ( 0, + $self->loc( 'That principal is not a [_1] for this ticket', + $args{'Type'} ) ); } - my ($m_id, $m_msg) = $group->_DeleteMember($principal->Id); + my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id ); unless ($m_id) { - $RT::Logger->error("Failed to delete ".$principal->Id. - " as a member of group ".$group->Id."\n".$m_msg); + $RT::Logger->error( "Failed to delete " + . $principal->Id + . " as a member of group " + . $group->Id . "\n" + . $m_msg ); - return ( 0, $self->loc('Could not remove that principal as a [_1] for this ticket', $args{'Type'}) ); + return (0, + $self->loc( + 'Could not remove that principal as a [_1] for this ticket', + $args{'Type'} ) ); } unless ( $args{'Silent'} ) { - $self->_NewTransaction( - Type => 'DelWatcher', - OldValue => $principal->Id, - Field => $args{'Type'} - ); + $self->_NewTransaction( Type => 'DelWatcher', + OldValue => $principal->Id, + Field => $args{'Type'} ); } - return ( 1, $self->loc("[_1] is no longer a [_2] for this ticket.", $principal->Object->Name, $args{'Type'} )); + return ( 1, + $self->loc( "[_1] is no longer a [_2] for this ticket.", + $principal->Object->Name, + $args{'Type'} ) ); } - # }}} -# {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string +=head2 SquelchMailTo [EMAIL] -=head2 RequestorAddresses +Takes an optional email address to never email about updates to this ticket. - B String: All Ticket Requestor email addresses as a string. -=cut +Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes. -sub RequestorAddresses { - my $self = shift; +=begin testing - unless ( $self->CurrentUserHasRight('ShowTicket') ) { - return undef; - } +my $t = RT::Ticket->new($RT::SystemUser); +ok($t->Create(Queue => 'general', Subject => 'SquelchTest')); - return ( $self->Requestors->MemberEmailAddressesAsString ); -} +is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients"); +my @returned = $t->SquelchMailTo('nobody@example.com'); -=head2 AdminCcAddresses +is($#returned, 0, "The ticket has one squelched recipients"); -returns String: All Ticket AdminCc email addresses as a string +my @names = $t->Attributes->Names; +is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo"); +@returned = $t->SquelchMailTo('nobody@example.com'); -=cut -sub AdminCcAddresses { - my $self = shift; +is($#returned, 0, "The ticket has one squelched recipients"); - unless ( $self->CurrentUserHasRight('ShowTicket') ) { - return undef; - } +@names = $t->Attributes->Names; +is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo"); - return ( $self->AdminCc->MemberEmailAddressesAsString ) -} +my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com'); +ok($ret, "Removed nobody as a squelched recipient - ".$msg); +@returned = $t->SquelchMailTo(); +is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned)); -=head2 CcAddresses -returns String: All Ticket Ccs as a string of email addresses +=end testing =cut -sub CcAddresses { +sub SquelchMailTo { my $self = shift; + if (@_) { + unless ( $self->CurrentUserHasRight('ModifyTicket') ) { + return undef; + } + my $attr = shift; + $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr ) + unless grep { $_->Content eq $attr } + $self->Attributes->Named('SquelchMailTo'); + } unless ( $self->CurrentUserHasRight('ShowTicket') ) { return undef; } - - return ( $self->Cc->MemberEmailAddressesAsString); - + my @attributes = $self->Attributes->Named('SquelchMailTo'); + return (@attributes); } -# }}} - -# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs -# {{{ sub Requestors +=head2 UnsquelchMailTo ADDRESS -=head2 Requestors +Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed. -Takes nothing. -Returns this ticket's Requestors as an RT::Group object +Returns a tuple of (status, message) =cut -sub Requestors { +sub UnsquelchMailTo { my $self = shift; - my $group = RT::Group->new($self->CurrentUser); - if ( $self->CurrentUserHasRight('ShowTicket') ) { - $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id); + my $address = shift; + unless ( $self->CurrentUserHasRight('ModifyTicket') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address); + return ($val, $msg); +} + + +# {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string + +=head2 RequestorAddresses + + B String: All Ticket Requestor email addresses as a string. + +=cut + +sub RequestorAddresses { + my $self = shift; + + unless ( $self->CurrentUserHasRight('ShowTicket') ) { + return undef; + } + + return ( $self->Requestors->MemberEmailAddressesAsString ); +} + + +=head2 AdminCcAddresses + +returns String: All Ticket AdminCc email addresses as a string + +=cut + +sub AdminCcAddresses { + my $self = shift; + + unless ( $self->CurrentUserHasRight('ShowTicket') ) { + return undef; + } + + return ( $self->AdminCc->MemberEmailAddressesAsString ) + +} + +=head2 CcAddresses + +returns String: All Ticket Ccs as a string of email addresses + +=cut + +sub CcAddresses { + my $self = shift; + + unless ( $self->CurrentUserHasRight('ShowTicket') ) { + return undef; + } + + return ( $self->Cc->MemberEmailAddressesAsString); + +} + +# }}} + +# {{{ 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::Group object + +=cut + +sub Requestors { + my $self = shift; + + my $group = RT::Group->new($self->CurrentUser); + if ( $self->CurrentUserHasRight('ShowTicket') ) { + $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id); } return ($group); @@ -1736,6 +1784,8 @@ PrincipalId is an RT::Principal id, and Email is an email address. Returns true if the specified principal (or the one corresponding to the specified address) is a member of the group Type for this ticket. +XX TODO: This should be Memoized. + =cut sub IsWatcher { @@ -1933,11 +1983,16 @@ sub SetQueue { ) ) { - $self->Untake(); + my $clone = RT::Ticket->new( $RT::SystemUser ); + $clone->Load( $self->Id ); + unless ( $clone->Id ) { + return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) ); + } + my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' ); + $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status; } return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) ); - } # }}} @@ -2188,11 +2243,16 @@ Takes a hashref with the following attributes: If MIMEObj is undefined, Content will be used to build a MIME::Entity for this commentl -MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content. +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun + +If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed. +They will, however, be prepared and you'll be able to access them through the TransactionObj + +Returns: Transaction id, Error Message, Transaction Object +(note the different order from Create()!) =cut -## Please see file perltidy.ERR sub Comment { my $self = shift; @@ -2201,49 +2261,27 @@ sub Comment { MIMEObj => undef, Content => undef, TimeTaken => 0, + DryRun => 0, @_ ); unless ( ( $self->CurrentUserHasRight('CommentOnTicket') ) or ( $self->CurrentUserHasRight('ModifyTicket') ) ) { - return ( 0, $self->loc("Permission Denied") ); + return ( 0, $self->loc("Permission Denied"), undef ); } + $args{'NoteType'} = 'Comment'; - unless ( $args{'MIMEObj'} ) { - if ( $args{'Content'} ) { - use MIME::Entity; - $args{'MIMEObj'} = MIME::Entity->build( - Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] ) - ); - } - else { - - return ( 0, $self->loc("No correspondence attached") ); - } + if ($args{'DryRun'}) { + $RT::Handle->BeginTransaction(); + $args{'CommitScrips'} = 0; } - RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8 - - # 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'} ) - if defined $args{'CcMessageTo'}; - $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', $args{'BccMessageTo'} ) - if defined $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'} - ); + my @results = $self->_RecordNote(%args); + if ($args{'DryRun'}) { + $RT::Handle->Rollback(); + } - return ( $Trans, $self->loc("The comment has been recorded") ); + return(@results); } - # }}} # {{{ sub Correspond @@ -2254,10 +2292,16 @@ Correspond on this ticket. Takes a hashref with the following attributes: -MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun if there's no MIMEObj, Content is used to build a MIME::Entity object +If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed. +They will, however, be prepared and you'll be able to access them through the TransactionObj + +Returns: Transaction id, Error Message, Transaction Object +(note the different order from Create()!) + =cut @@ -2272,305 +2316,120 @@ sub Correspond { unless ( ( $self->CurrentUserHasRight('ReplyToTicket') ) or ( $self->CurrentUserHasRight('ModifyTicket') ) ) { - return ( 0, $self->loc("Permission Denied") ); + return ( 0, $self->loc("Permission Denied"), undef ); } - unless ( $args{'MIMEObj'} ) { - if ( $args{'Content'} ) { - use MIME::Entity; - $args{'MIMEObj'} = MIME::Entity->build( - Data => ( ref $args{'Content'} ? $args{'Content'} : [ $args{'Content'} ] ) - ); - - } - else { - - return ( 0, $self->loc("No correspondence attached") ); - } + $args{'NoteType'} = 'Correspond'; + if ($args{'DryRun'}) { + $RT::Handle->BeginTransaction(); + $args{'CommitScrips'} = 0; } - RT::I18N::SetMIMEEntityToUTF8($args{'MIMEObj'}); # convert text parts into utf-8 - - # 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'} ) - if defined $args{'CcMessageTo'}; - $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', $args{'BccMessageTo'} ) - if defined $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'} ); - - unless ($Trans) { - $RT::Logger->err( "$self couldn't init a transaction $msg"); - return ( $Trans, $self->loc("correspondence (probably) not sent"), $args{'MIMEObj'} ); - } + my @results = $self->_RecordNote(%args); #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" + $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id)); - unless ( $TransObj->IsInbound ) { - $self->_SetTold; + if ($args{'DryRun'}) { + $RT::Handle->Rollback(); } - return ( $Trans, $self->loc("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 + return (@results); -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' ) ); -} - -# }}} +# {{{ sub _RecordNote -# {{{ DependedOnBy +=head2 _RecordNote -=head2 DependedOnBy +the meat of both comment and correspond. - This returns an RT::Links object which references all the tickets that depend on this one +Performs no access control checks. hence, dangerous. =cut -sub DependedOnBy { - my $self = shift; - return ( $self->_Links( 'Target', 'DependsOn' ) ); -} - -# }}} - - - -=head2 HasUnresolvedDependencies - - Takes a paramhash of Type (default to '__any'). Returns true if -$self->UnresolvedDependencies returns an object with one or more members -of that type. Returns false otherwise - - -=begin testing - -my $t1 = RT::Ticket->new($RT::SystemUser); -my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general'); -ok($id, "Created dep test 1 - $msg"); - -my $t2 = RT::Ticket->new($RT::SystemUser); -my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general'); -ok($id2, "Created dep test 2 - $msg2"); -my $t3 = RT::Ticket->new($RT::SystemUser); -my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval'); -ok($id3, "Created dep test 3 - $msg3"); - -ok ($t1->AddLink( Type => 'DependsOn', Target => $t2->id)); -ok ($t1->AddLink( Type => 'DependsOn', Target => $t3->id)); - -ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps"); -ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs"); -ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals"); -ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps"); -my ($rid, $rmsg)= $t1->Resolve(); -ok(!$rid, $rmsg); -ok($t2->Resolve); -($rid, $rmsg)= $t1->Resolve(); -ok(!$rid, $rmsg); -ok($t3->Resolve); -($rid, $rmsg)= $t1->Resolve(); -ok($rid, $rmsg); - - -=end testing - -=cut +sub _RecordNote { -sub HasUnresolvedDependencies { my $self = shift; - my %args = ( - Type => undef, - @_ - ); - - my $deps = $self->UnresolvedDependencies; - - if ($args{Type}) { - $deps->Limit( FIELD => 'Type', - OPERATOR => '=', - VALUE => $args{Type}); - } - else { - $deps->IgnoreType; - } + my %args = ( CcMessageTo => undef, + BccMessageTo => undef, + MIMEObj => undef, + Content => undef, + TimeTaken => 0, + CommitScrips => 1, + @_ ); - if ($deps->Count > 0) { - return 1; + unless ( $args{'MIMEObj'} || $args{'Content'} ) { + return ( 0, $self->loc("No message attached"), undef ); } - else { - return (undef); - } -} - - -# {{{ UnresolvedDependencies - -=head2 UnresolvedDependencies - -Returns an RT::Tickets object of tickets which this ticket depends on -and which have a status of new, open or stalled. (That list comes from -RT::Queue->ActiveStatusArray - -=cut - - -sub UnresolvedDependencies { - my $self = shift; - my $deps = RT::Tickets->new($self->CurrentUser); + unless ( $args{'MIMEObj'} ) { + $args{'MIMEObj'} = MIME::Entity->build( Data => ( + ref $args{'Content'} + ? $args{'Content'} + : [ $args{'Content'} ] + ) ); + } - my @live_statuses = RT::Queue->ActiveStatusArray(); - foreach my $status (@live_statuses) { - $deps->LimitStatus(VALUE => $status); + # convert text parts into utf-8 + RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} ); + +# 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', RT::User::CanonicalizeEmailAddress( + undef, $args{'CcMessageTo'} + ) ) + if defined $args{'CcMessageTo'}; + $args{'MIMEObj'}->head->add( 'RT-Send-Bcc', + RT::User::CanonicalizeEmailAddress( + undef, $args{'BccMessageTo'} + ) ) + if defined $args{'BccMessageTo'}; + + # If this is from an external source, we need to come up with its + # internal Message-ID now, so all emails sent because of this + # message have a common Message-ID + unless ($args{'MIMEObj'}->head->get('Message-ID') + =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@$RT::Organization>/) { + $args{'MIMEObj'}->head->set( 'RT-Message-ID', + "id . "-" + . "0" . "-" # Scrip + . "0" . "@" # Email sent + . $RT::Organization + . ">" ); } - $deps->LimitDependedOnBy($self->Id); - - return($deps); - -} - -# }}} - -# {{{ AllDependedOnBy - -=head2 AllDependedOnBy - -Returns an array of RT::Ticket objects which (directly or indirectly) -depends on this ticket; takes an optional 'Type' argument in the param -hash, which will limit returned tickets to that type, as well as cause -tickets with that type to serve as 'leaf' nodes that stops the recursive -dependency search. - -=cut -sub AllDependedOnBy { - my $self = shift; - my $dep = $self->DependedOnBy; - my %args = ( - Type => undef, - _found => {}, - _top => 1, - @_ + #Record the correspondence (write the transaction) + my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction( + Type => $args{'NoteType'}, + Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ), + TimeTaken => $args{'TimeTaken'}, + MIMEObj => $args{'MIMEObj'}, + CommitScrips => $args{'CommitScrips'}, ); - while (my $link = $dep->Next()) { - next unless ($link->BaseURI->IsLocal()); - next if $args{_found}{$link->BaseObj->Id}; - - if (!$args{Type}) { - $args{_found}{$link->BaseObj->Id} = $link->BaseObj; - $link->BaseObj->AllDependedOnBy( %args, _top => 0 ); - } - elsif ($link->BaseObj->Type eq $args{Type}) { - $args{_found}{$link->BaseObj->Id} = $link->BaseObj; - } - else { - $link->BaseObj->AllDependedOnBy( %args, _top => 0 ); - } + unless ($Trans) { + $RT::Logger->err("$self couldn't init a transaction $msg"); + return ( $Trans, $self->loc("Message could not be recorded"), undef ); } - if ($args{_top}) { - return map { $args{_found}{$_} } sort keys %{$args{_found}}; - } - else { - return 1; - } + return ( $Trans, $self->loc("Message recorded"), $TransObj ); } # }}} -# {{{ 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 { @@ -2607,8 +2466,6 @@ sub _Links { # }}} -# }}} - # {{{ sub DeleteLink =head2 DeleteLink @@ -2635,44 +2492,29 @@ sub DeleteLink { } - #we want one of base and target. we don't care which - #but we only want _one_ + my ($val, $Msg) = $self->SUPER::_DeleteLink(%args); - my $direction; - my $remote_link; - - if ( $args{'Base'} and $args{'Target'} ) { - $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n"); - return ( 0, $self->loc("Can't specifiy both base and target") ); + if ( !$val ) { + $RT::Logger->debug("Couldn't find that link\n"); + return ( 0, $Msg ); } - elsif ( $args{'Base'} ) { - $args{'Target'} = $self->URI(); + + my ($direction, $remote_link); + + if ( $args{'Base'} ) { $remote_link = $args{'Base'}; $direction = 'Target'; } elsif ( $args{'Target'} ) { - $args{'Base'} = $self->URI(); $remote_link = $args{'Target'}; $direction='Base'; } - else { - $RT::Logger->debug("$self: Base or Target must be specified\n"); - return ( 0, $self->loc('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->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=> $args{'Target'} ); - #it's a real link. - if ( $link->id ) { - my $linkid = $link->id; - $link->Delete(); - - my $TransString = "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}."; - my $remote_uri = RT::URI->new( $RT::SystemUser ); + if ( $args{'Silent'} ) { + return ( $val, $Msg ); + } + else { + my $remote_uri = RT::URI->new( $self->CurrentUser ); $remote_uri->FromURI( $remote_link ); my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( @@ -2682,13 +2524,18 @@ sub DeleteLink { TimeTaken => 0 ); - return ( $Trans, $self->loc("Link deleted ([_1])", $TransString)); - } + if ( $remote_uri->IsLocal ) { - #if it's not a link we can find - else { - $RT::Logger->debug("Couldn't find that link\n"); - return ( 0, $self->loc("Link not found") ); + my $OtherObj = $remote_uri->Object; + my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink', + Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} + : $LINKDIRMAP{$args{'Type'}}->{Target}, + OldValue => $self->URI, + ActivateScrips => ! $RT::LinkTransactionsRun1Scrip, + TimeTaken => 0 ); + } + + return ( $Trans, $Msg ); } } @@ -2700,7 +2547,6 @@ sub DeleteLink { Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket. - =cut sub AddLink { @@ -2711,160 +2557,187 @@ sub AddLink { Silent => undef, @_ ); + unless ( $self->CurrentUserHasRight('ModifyTicket') ) { return ( 0, $self->loc("Permission Denied") ); } - # Remote_link is the URI of the object that is not this ticket - my $remote_link; - my $direction; - if ( $args{'Base'} and $args{'Target'} ) { - $RT::Logger->debug( -"$self tried to delete a link. both base and target were specified\n" ); - return ( 0, $self->loc("Can't specifiy both base and target") ); + $self->_AddLink(%args); +} + +=head2 _AddLink + +Private non-acled variant of AddLink so that links can be added during create. + +=cut + +sub _AddLink { + my $self = shift; + my %args = ( Target => '', + Base => '', + Type => '', + Silent => undef, + @_ ); + + # {{{ If the other URI is an RT::Ticket, we want to make sure the user + # can modify it too... + my $other_ticket_uri = RT::URI->new($self->CurrentUser); + + if ( $args{'Target'} ) { + $other_ticket_uri->FromURI( $args{'Target'} ); + } elsif ( $args{'Base'} ) { - $args{'Target'} = $self->URI(); - $remote_link = $args{'Base'}; - $direction = 'Target'; - } - elsif ( $args{'Target'} ) { - $args{'Base'} = $self->URI(); - $remote_link = $args{'Target'}; - $direction='Base'; + $other_ticket_uri->FromURI( $args{'Base'} ); } - else { - return ( 0, $self->loc('Either base or target must be specified') ); + + unless ( $other_ticket_uri->Resolver && $other_ticket_uri->Scheme ) { + my $msg = $args{'Target'} ? $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'}) + : $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'}); + $RT::Logger->warning( "$self $msg\n" ); + + return( 0, $msg ); } - # If the base isn't a URI, make it a URI. - # If the target isn't a URI, make it a URI. + if ( $other_ticket_uri->Resolver->Scheme eq 'fsck.com-rt') { + my $object = $other_ticket_uri->Resolver->Object; + + if ( UNIVERSAL::isa( $object, 'RT::Ticket' ) + && $object->id + && !$object->CurrentUserHasRight('ModifyTicket') ) + { + return ( 0, $self->loc("Permission Denied") ); + } - # {{{ Check if the link already exists - we don't want duplicates - use RT::Link; - my $old_link = RT::Link->new( $self->CurrentUser ); - $old_link->LoadByParams( Base => $args{'Base'}, - Type => $args{'Type'}, - Target => $args{'Target'} ); - if ( $old_link->Id ) { - $RT::Logger->debug("$self Somebody tried to duplicate a link"); - return ( $old_link->id, $self->loc("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} ); + my ($val, $Msg) = $self->SUPER::_AddLink(%args); - unless ($linkid) { - return ( 0, $self->loc("Link could not be created") ); + if (!$val) { + return ($val, $Msg); } - my $TransString = - "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}."; + my ($direction, $remote_link); + if ( $args{'Target'} ) { + $remote_link = $args{'Target'}; + $direction = 'Base'; + } elsif ( $args{'Base'} ) { + $remote_link = $args{'Base'}; + $direction = 'Target'; + } # Don't write the transaction if we're doing this on create if ( $args{'Silent'} ) { - return ( 1, $self->loc( "Link created ([_1])", $TransString ) ); + return ( $val, $Msg ); } else { - my $remote_uri = RT::URI->new( $RT::SystemUser ); + my $remote_uri = RT::URI->new( $self->CurrentUser ); $remote_uri->FromURI( $remote_link ); #Write the transaction - my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( - Type => 'AddLink', - Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, - NewValue => $remote_uri->URI || $remote_link, - TimeTaken => 0 ); - return ( $Trans, $self->loc( "Link created ([_1])", $TransString ) ); + my ( $Trans, $Msg, $TransObj ) = + $self->_NewTransaction(Type => 'AddLink', + Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, + NewValue => $remote_uri->URI || $remote_link, + TimeTaken => 0 ); + + if ( $remote_uri->IsLocal ) { + + my $OtherObj = $remote_uri->Object; + my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink', + Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} + : $LINKDIRMAP{$args{'Type'}}->{Target}, + NewValue => $self->URI, + ActivateScrips => ! $RT::LinkTransactionsRun1Scrip, + TimeTaken => 0 ); + } + return ( $val, $Msg ); } } # }}} -# {{{ sub URI -=head2 URI +# {{{ sub MergeInto -Returns this ticket's URI +=head2 MergeInto -=cut +MergeInto take the id of the ticket to merge this ticket into. -sub URI { - my $self = shift; - my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser); - return($uri->URIForObject($self)); -} -# }}} +=begin testing -# {{{ sub MergeInto +my $t1 = RT::Ticket->new($RT::SystemUser); +$t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com'); +my $t1id = $t1->id; +my $t2 = RT::Ticket->new($RT::SystemUser); +$t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com'); +my $t2id = $t2->id; +my ($msg, $val) = $t1->MergeInto($t2->id); +ok ($msg,$val); +$t1 = RT::Ticket->new($RT::SystemUser); +is ($t1->id, undef, "ok. we've got a blank ticket1"); +$t1->Load($t1id); -=head2 MergeInto -MergeInto take the id of the ticket to merge this ticket into. +is ($t1->id, $t2->id); + +is ($t1->Requestors->MembersObj->Count, 2); + + +=end testing =cut sub MergeInto { my $self = shift; - my $MergeInto = shift; + my $ticket_id = shift; unless ( $self->CurrentUserHasRight('ModifyTicket') ) { return ( 0, $self->loc("Permission Denied") ); } # Load up the new ticket. - my $NewTicket = RT::Ticket->new($RT::SystemUser); - $NewTicket->Load($MergeInto); + my $MergeInto = RT::Ticket->new($RT::SystemUser); + $MergeInto->Load($ticket_id); # make sure it exists. - unless ( defined $NewTicket->Id ) { + unless ( $MergeInto->Id ) { return ( 0, $self->loc("New ticket doesn't exist") ); } # Make sure the current user can modify the new ticket. - unless ( $NewTicket->CurrentUserHasRight('ModifyTicket') ) { - $RT::Logger->debug("failed..."); + unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) { return ( 0, $self->loc("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, - $self->loc("Can't merge into a merged ticket. You should never get this error") ); - } + $RT::Handle->BeginTransaction(); # 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 + # by trying to do a separate 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() + Value => $MergeInto->Id() ); unless ($id_val) { - $RT::Logger->error( - "Couldn't set effective ID for " . $self->Id . ": $id_msg" ); + $RT::Handle->Rollback(); return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") ); } my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved'); unless ($status_val) { + $RT::Handle->Rollback(); $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) ); + return ( 0, $self->loc("Merge failed. Couldn't set Status") ); } @@ -2872,11 +2745,24 @@ sub MergeInto { my $old_links_to = RT::Links->new($self->CurrentUser); $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI); + my %old_seen; while (my $link = $old_links_to->Next) { - if ($link->Base eq $NewTicket->URI) { + if (exists $old_seen{$link->Base."-".$link->Type}) { + $link->Delete; + } + elsif ($link->Base eq $MergeInto->URI) { $link->Delete; } else { - $link->SetTarget($NewTicket->URI); + # First, make sure the link doesn't already exist. then move it over. + my $tmp = RT::Link->new($RT::SystemUser); + $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id); + if ($tmp->id) { + $link->Delete; + } else { + $link->SetTarget($MergeInto->URI); + $link->SetLocalTarget($MergeInto->id); + } + $old_seen{$link->Base."-".$link->Type} =1; } } @@ -2885,40 +2771,54 @@ sub MergeInto { $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI); while (my $link = $old_links_from->Next) { - if ($link->Target eq $NewTicket->URI) { + if (exists $old_seen{$link->Type."-".$link->Target}) { + $link->Delete; + } + if ($link->Target eq $MergeInto->URI) { $link->Delete; } else { - $link->SetBase($NewTicket->URI); + # First, make sure the link doesn't already exist. then move it over. + my $tmp = RT::Link->new($RT::SystemUser); + $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id); + if ($tmp->id) { + $link->Delete; + } else { + $link->SetBase($MergeInto->URI); + $link->SetLocalBase($MergeInto->id); + $old_seen{$link->Type."-".$link->Target} =1; + } } } + # Update time fields + foreach my $type qw(TimeEstimated TimeWorked TimeLeft) { - #make a new link: this ticket is merged into that other ticket. - $self->AddLink( Type => 'MergedInto', Target => $NewTicket->Id()); + my $mutator = "Set$type"; + $MergeInto->$mutator( + ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) ); - #add all of this ticket's watchers to that ticket. - my $requestors = $self->Requestors->MembersObj; - while (my $watcher = $requestors->Next) { - $NewTicket->_AddWatcher( Type => 'Requestor', - Silent => 1, - PrincipalId => $watcher->MemberId); } +#add all of this ticket's watchers to that ticket. + foreach my $watcher_type qw(Requestors Cc AdminCc) { - my $Ccs = $self->Cc->MembersObj; - while (my $watcher = $Ccs->Next) { - $NewTicket->_AddWatcher( Type => 'Cc', - Silent => 1, - PrincipalId => $watcher->MemberId); - } + my $people = $self->$watcher_type->MembersObj; + my $addwatcher_type = $watcher_type; + $addwatcher_type =~ s/s$//; - my $AdminCcs = $self->AdminCc->MembersObj; - while (my $watcher = $AdminCcs->Next) { - $NewTicket->_AddWatcher( Type => 'AdminCc', - Silent => 1, - PrincipalId => $watcher->MemberId); + while ( my $watcher = $people->Next ) { + + my ($val, $msg) = $MergeInto->_AddWatcher( + Type => $addwatcher_type, + Silent => 1, + PrincipalId => $watcher->MemberId + ); + unless ($val) { + $RT::Logger->warning($msg); + } } + } #find all of the tickets that were merged into this ticket. my $old_mergees = new RT::Tickets( $self->CurrentUser ); @@ -2932,12 +2832,16 @@ sub MergeInto { while ( my $ticket = $old_mergees->Next() ) { my ( $val, $msg ) = $ticket->__Set( Field => 'EffectiveId', - Value => $NewTicket->Id() + Value => $MergeInto->Id() ); } - $NewTicket->_SetLastUpdated; + #make a new link: this ticket is merged into that other ticket. + $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id()); + + $MergeInto->_SetLastUpdated; + $RT::Handle->Commit(); return ( 1, $self->loc("Merge Successful") ); } @@ -3005,12 +2909,13 @@ ok ($root->Id, "Loaded the root user"); my $t = RT::Ticket->new($RT::SystemUser); $t->Load(1); $t->SetOwner('root'); -ok ($t->OwnerObj->Name eq 'root' , "Root owns the ticket"); +is ($t->OwnerObj->Name, 'root' , "Root owns the ticket"); $t->Steal(); -ok ($t->OwnerObj->id eq $RT::SystemUser->id , "SystemUser owns the ticket"); +is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket"); my $txns = RT::Transactions->new($RT::SystemUser); $txns->OrderBy(FIELD => 'id', ORDER => 'DESC'); -$txns->Limit(FIELD => 'Ticket', VALUE => '1'); +$txns->Limit(FIELD => 'ObjectId', VALUE => '1'); +$txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket'); my $steal = $txns->First; ok($steal->OldValue == $root->Id , "Stolen from root"); ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser"); @@ -3122,21 +3027,21 @@ sub SetOwner { $RT::Handle->Commit(); - my ( $trans, $msg, undef ) = $self->_NewTransaction( - Type => $Type, - Field => 'Owner', - NewValue => $NewOwnerObj->Id, - OldValue => $OldOwnerObj->Id, - TimeTaken => 0 ); + ($val, $msg) = $self->_NewTransaction( + Type => $Type, + Field => 'Owner', + NewValue => $NewOwnerObj->Id, + OldValue => $OldOwnerObj->Id, + TimeTaken => 0, + ); - if ($trans) { + if ( $val ) { $msg = $self->loc( "Owner changed from [_1] to [_2]", $OldOwnerObj->Name, $NewOwnerObj->Name ); # TODO: make sure the trans committed properly } - return ( $trans, $msg ); - + return ( $val, $msg ); } # }}} @@ -3237,14 +3142,14 @@ my $tt = RT::Ticket->new($RT::SystemUser); my ($id, $tid, $msg)= $tt->Create(Queue => 'general', Subject => 'test'); ok($id, $msg); -ok($tt->Status eq 'new', "New ticket is created as new"); +is($tt->Status, 'new', "New ticket is created as new"); ($id, $msg) = $tt->SetStatus('open'); ok($id, $msg); -ok ($msg =~ /open/i, "Status message is correct"); +like($msg, qr/open/i, "Status message is correct"); ($id, $msg) = $tt->SetStatus('resolved'); ok($id, $msg); -ok ($msg =~ /resolved/i, "Status message is correct"); +like($msg, qr/resolved/i, "Status message is correct"); ($id, $msg) = $tt->SetStatus('resolved'); ok(!$id,$msg); @@ -3266,8 +3171,14 @@ sub SetStatus { } #Check ACL - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc('Permission Denied') ); + if ( $args{Status} eq 'deleted') { + unless ($self->CurrentUserHasRight('DeleteTicket')) { + return ( 0, $self->loc('Permission Denied') ); + } + } else { + unless ($self->CurrentUserHasRight('ModifyTicket')) { + return ( 0, $self->loc('Permission Denied') ); + } } if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) { @@ -3286,9 +3197,9 @@ sub SetStatus { RecordTransaction => 0 ); } - if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) { - - #When we resolve a ticket, set the 'Resolved' attribute to now. + #When we close a ticket, set the 'Resolved' attribute to now. + # It's misnamed, but that's just historical. + if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) { $self->_Set( Field => 'Resolved', Value => $now->ISO, RecordTransaction => 0 ); @@ -3298,6 +3209,7 @@ sub SetStatus { my ($val, $msg)= $self->_Set( Field => 'Status', Value => $args{Status}, TimeTaken => 0, + CheckACL => 0, TransactionType => 'Status' ); return($val,$msg); @@ -3315,7 +3227,7 @@ Takes no arguments. Marks this ticket for garbage collection sub Kill { my $self = shift; - $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead."); + $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).")."); return $self->Delete; } @@ -3390,278 +3302,7 @@ sub Resolve { # }}} -# {{{ Routines dealing with custom fields - - -# {{{ FirstCustomFieldValue - -=item FirstCustomFieldValue FIELD - -Return the content of the first value of CustomField FIELD for this ticket -Takes a field id or name - -=cut - -sub FirstCustomFieldValue { - my $self = shift; - my $field = shift; - my $values = $self->CustomFieldValues($field); - if ($values->First) { - return $values->First->Content; - } else { - return undef; - } - -} - - - -# {{{ CustomFieldValues - -=item CustomFieldValues FIELD - -Return a TicketCustomFieldValues object of all values of CustomField FIELD for this ticket. -Takes a field id or name. - - -=cut - -sub CustomFieldValues { - my $self = shift; - my $field = shift; - - my $cf = RT::CustomField->new($self->CurrentUser); - - if ($field =~ /^\d+$/) { - $cf->LoadById($field); - } else { - $cf->LoadByNameAndQueue(Name => $field, Queue => $self->QueueObj->Id); - } - my $cf_values = RT::TicketCustomFieldValues->new( $self->CurrentUser ); - $cf_values->LimitToCustomField($cf->id); - $cf_values->LimitToTicket($self->Id()); - - # @values is a CustomFieldValues object; - return ($cf_values); -} - -# }}} - -# {{{ AddCustomFieldValue - -=item AddCustomFieldValue { Field => FIELD, Value => VALUE } - -VALUE can either be a CustomFieldValue object or a string. -FIELD can be a CustomField object OR a CustomField ID. - - -Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field, -deletes the old value. -If VALUE isn't a valid value for the custom field, returns -(0, 'Error message' ) otherwise, returns (1, 'Success Message') - -=cut - -sub AddCustomFieldValue { - my $self = shift; - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - $self->_AddCustomFieldValue(@_); -} - -sub _AddCustomFieldValue { - my $self = shift; - my %args = ( - Field => undef, - Value => undef, - RecordTransaction => 1, - @_ - ); - - my $cf = RT::CustomField->new( $self->CurrentUser ); - if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) { - $cf->Load( $args{'Field'}->id ); - } - else { - $cf->Load( $args{'Field'} ); - } - - unless ( $cf->Id ) { - return ( 0, $self->loc("Custom field [_1] not found", $args{'Field'}) ); - } - - # Load up a TicketCustomFieldValues object for this custom field and this ticket - my $values = $cf->ValuesForTicket( $self->id ); - - unless ( $cf->ValidateValue( $args{'Value'} ) ) { - return ( 0, $self->loc("Invalid value for custom field") ); - } - - # If the custom field only accepts a single value, delete the existing - # value and record a "changed from foo to bar" transaction - if ( $cf->SingleValue ) { - - # We need to whack any old values here. In most cases, the custom field should - # only have one value to delete. In the pathalogical case, this custom field - # used to be a multiple and we have many values to whack.... - my $cf_values = $values->Count; - - if ( $cf_values > 1 ) { - my $i = 0; #We want to delete all but the last one, so we can then - # execute the same code to "change" the value from old to new - while ( my $value = $values->Next ) { - $i++; - if ( $i < $cf_values ) { - my $old_value = $value->Content; - my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $value->Content); - unless ($val) { - return (0,$msg); - } - my ( $TransactionId, $Msg, $TransactionObj ) = - $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - OldValue => $old_value - ); - } - } - } - - my $old_value; - if (my $value = $cf->ValuesForTicket( $self->Id )->First) { - $old_value = $value->Content(); - return (1) if $old_value eq $args{'Value'}; - } - - my ( $new_value_id, $value_msg ) = $cf->AddValueForTicket( - Ticket => $self->Id, - Content => $args{'Value'} - ); - - unless ($new_value_id) { - return ( 0, - $self->loc("Could not add new custom field value for ticket. [_1] ", - ,$value_msg) ); - } - - my $new_value = RT::TicketCustomFieldValue->new( $self->CurrentUser ); - $new_value->Load($new_value_id); - - # now that adding the new value was successful, delete the old one - if ($old_value) { - my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $old_value); - unless ($val) { - return (0,$msg); - } - } - - if ($args{'RecordTransaction'}) { - my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - OldValue => $old_value, - NewValue => $new_value->Content - ); - } - - if ( $old_value eq '' ) { - return ( 1, $self->loc("[_1] [_2] added", $cf->Name, $new_value->Content) ); - } - elsif ( $new_value->Content eq '' ) { - return ( 1, $self->loc("[_1] [_2] deleted", $cf->Name, $old_value) ); - } - else { - return ( 1, $self->loc("[_1] [_2] changed to [_3]", $cf->Name, $old_value, $new_value->Content ) ); - } - - } - - # otherwise, just add a new value and record "new value added" - else { - my ( $new_value_id ) = $cf->AddValueForTicket( - Ticket => $self->Id, - Content => $args{'Value'} - ); - - unless ($new_value_id) { - return ( 0, - $self->loc("Could not add new custom field value for ticket. ")); - } - if ( $args{'RecordTransaction'} ) { - my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - NewValue => $args{'Value'} - ); - unless ($TransactionId) { - return ( 0, - $self->loc( "Couldn't create a transaction: [_1]", $Msg ) ); - } - } - return ( 1, $self->loc("[_1] added as a value for [_2]",$args{'Value'}, $cf->Name)); - } - -} - -# }}} - -# {{{ DeleteCustomFieldValue - -=item DeleteCustomFieldValue { Field => FIELD, Value => VALUE } - -Deletes VALUE as a value of CustomField FIELD. - -VALUE can be a string, a CustomFieldValue or a TicketCustomFieldValue. - -If VALUE isn't a valid value for the custom field, returns -(0, 'Error message' ) otherwise, returns (1, 'Success Message') - -=cut - -sub DeleteCustomFieldValue { - my $self = shift; - my %args = ( - Field => undef, - Value => undef, - @_); - - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - my $cf = RT::CustomField->new( $self->CurrentUser ); - if ( UNIVERSAL::isa( $args{'Field'}, "RT::CustomField" ) ) { - $cf->LoadById( $args{'Field'}->id ); - } - else { - $cf->LoadById( $args{'Field'} ); - } - - unless ( $cf->Id ) { - return ( 0, $self->loc("Custom field not found") ); - } - - - my ($val, $msg) = $cf->DeleteValueForTicket(Ticket => $self->Id, Content => $args{'Value'}); - unless ($val) { - return (0,$msg); - } - my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction( - Type => 'CustomField', - Field => $cf->Id, - OldValue => $args{'Value'} - ); - unless($TransactionId) { - return(0, $self->loc("Couldn't create a transaction: [_1]", $Msg)); - } - - return($TransactionId, $self->loc("[_1] is no longer a value for custom field [_2]", $args{'Value'}, $cf->Name)); -} - -# }}} - -# }}} - + # {{{ Actions + Routines dealing with transactions # {{{ sub SetTold and _SetTold @@ -3716,100 +3357,50 @@ sub _SetTold { # }}} -# {{{ sub Transactions +=head2 TransactionBatch -=head2 Transactions + Returns an array reference of all transactions created on this ticket during + this ticket object's lifetime, or undef if there were none. - Returns an RT::Transactions object of all transactions on this ticket + Only works when the $RT::UseTransactionBatch config variable is set to true. =cut -sub Transactions { +sub TransactionBatch { 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); + return $self->{_TransactionBatch}; } -# }}} - -# {{{ sub _NewTransaction - -sub _NewTransaction { +sub DESTROY { 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'} - ); - - - $self->Load($self->Id); - - $RT::Logger->warning($msg) unless $transaction; + # DESTROY methods need to localize $@, or it may unset it. This + # causes $m->abort to not bubble all of the way up. See perlbug + # http://rt.perl.org/rt3/Ticket/Display.html?id=17650 + local $@; - $self->_SetLastUpdated; + # The following line eliminates reentrancy. + # It protects against the fact that perl doesn't deal gracefully + # when an object's refcount is changed in its destructor. + return if $self->{_Destroyed}++; - if ( defined $args{'TimeTaken'} ) { - $self->_UpdateTimeTaken( $args{'TimeTaken'} ); - } - return ( $transaction, $msg, $trans ); + my $batch = $self->TransactionBatch or return; + require RT::Scrips; + RT::Scrips->new($RT::SystemUser)->Apply( + Stage => 'TransactionBatch', + TicketObj => $self, + TransactionObj => $batch->[0], + Type => join(',', (map { $_->Type } @{$batch}) ) + ); } # }}} -# }}} - # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record -# {{{ sub _ClassAccessible +# {{{ sub _OverlayAccessible -sub _ClassAccessible { +sub _OverlayAccessible { { EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 }, Queue => { 'read' => 1, 'write' => 1 }, @@ -3823,8 +3414,6 @@ sub _ClassAccessible { TimeEstimated => { '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 }, Type => { 'read' => 1 }, @@ -3881,7 +3470,7 @@ sub _Set { #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 ); } + return ( 0, $msg ) unless $ret; } if ( $args{'RecordTransaction'} == 1 ) { @@ -3893,7 +3482,7 @@ sub _Set { OldValue => $Old, TimeTaken => $args{'TimeTaken'}, ); - return ( $Trans, scalar $TransObj->Description ); + return ( $Trans, scalar $TransObj->BriefDescription ); } else { return ( $ret, $msg ); @@ -4011,7 +3600,9 @@ sub HasRight { unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) ) { - $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight"); + Carp::cluck; + $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight"); + return(undef); } return ( @@ -4026,6 +3617,107 @@ sub HasRight { # }}} +# {{{ sub Transactions + +=head2 Transactions + + Returns an RT::Transactions object of all transactions on this ticket + +=cut + +sub Transactions { + my $self = shift; + + my $transactions = RT::Transactions->new( $self->CurrentUser ); + + #If the user has no rights, return an empty object + if ( $self->CurrentUserHasRight('ShowTicket') ) { + $transactions->LimitToTicket($self->id); + + # if the user may not see comments do not return them + unless ( $self->CurrentUserHasRight('ShowTicketComments') ) { + $transactions->Limit( + FIELD => 'Type', + OPERATOR => '!=', + VALUE => "Comment" + ); + $transactions->Limit( + FIELD => 'Type', + OPERATOR => '!=', + VALUE => "CommentEmailRecord", + ENTRYAGGREGATOR => 'AND' + ); + + } + } + + return ($transactions); +} + +# }}} + + +# {{{ TransactionCustomFields + +=head2 TransactionCustomFields + + Returns the custom fields that transactions on tickets will ahve. + +=cut + +sub TransactionCustomFields { + my $self = shift; + return $self->QueueObj->TicketTransactionCustomFields; +} + +# }}} + +# {{{ sub CustomFieldValues + +=head2 CustomFieldValues + +# Do name => id mapping (if needed) before falling back to +# RT::Record's CustomFieldValues + +See L + +=cut + +sub CustomFieldValues { + my $self = shift; + my $field = shift; + if ( $field and $field !~ /^\d+$/ ) { + my $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue ); + unless ( $cf->id ) { + $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ); + } + unless ( $cf->id ) { + # If we didn't find a valid cfid, give up. + return RT::CustomFieldValues->new($self->CurrentUser); + } + $field = $cf->id; + } + return $self->SUPER::CustomFieldValues($field); +} + +# }}} + +# {{{ sub CustomFieldLookupType + +=head2 CustomFieldLookupType + +Returns the RT::Ticket lookup type, which can be passed to +RT::CustomField->Create() via the 'LookupType' hash key. + +=cut + +# }}} + +sub CustomFieldLookupType { + "RT::Queue-RT::Ticket"; +} + 1; =head1 AUTHOR