diff options
Diffstat (limited to 'rt/lib/RT/Ticket_Overlay.pm')
-rw-r--r-- | rt/lib/RT/Ticket_Overlay.pm | 4095 |
1 files changed, 0 insertions, 4095 deletions
diff --git a/rt/lib/RT/Ticket_Overlay.pm b/rt/lib/RT/Ticket_Overlay.pm deleted file mode 100644 index 981df4125..000000000 --- a/rt/lib/RT/Ticket_Overlay.pm +++ /dev/null @@ -1,4095 +0,0 @@ -# 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 -# {{{ Front Material - -=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 - -=begin testing - -use_ok ( RT::Queue); -ok(my $testqueue = RT::Queue->new($RT::SystemUser)); -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', - Queue => $testqueue->id, - Type => 'SelectMultiple')); -ok($testcf->AddValue ( Name => 'Value1', - SortOrder => '1', - Description => 'A testing value')); -ok($testcf->AddValue ( Name => 'Value2', - SortOrder => '2', - Description => 'Another testing value')); -ok($testcf->AddValue ( Name => 'Value3', - SortOrder => '3', - Description => 'Yet Another testing value')); - -ok($testcf->Values->Count == 3); - -use_ok(RT::Ticket); - -my $u = RT::User->new($RT::SystemUser); -$u->Load("root"); -ok ($u->Id, "Found the root user"); -ok(my $t = RT::Ticket->new($RT::SystemUser)); -ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id, - Subject => 'Testing', - Owner => $u->Id - )); -ok($id != 0); -ok ($t->OwnerObj->Id == $u->Id, "Root is the ticket owner"); -ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id, - Value => 'Value1')); -ok($cfv != 0, "Custom field creation didn't return an error: $cfm"); -ok($t->CustomFieldValues($testcf->Id)->Count == 1); -ok($t->CustomFieldValues($testcf->Id)->First && - $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');; - -ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id, - Value => 'Value1')); -ok ($cfdv != 0, "Deleted a custom field value: $cfdm"); -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); -ok($t2->OwnerObj->Id == $u->Id); - -my $t3 = RT::Ticket->new($RT::SystemUser); -my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id, - Subject => 'Testing', - Owner => $u->Id); -my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id, - Value => 'Value1'); -ok($cfv1 != 0, "Adding a custom field to ticket 1 is successful: $cfm"); -my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id, - Value => 'Value2'); -ok($cfv2 != 0, "Adding a custom field to ticket 2 is successful: $cfm"); -my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id, - Value => 'Value3'); -ok($cfv3 != 0, "Adding a custom field to ticket 1 is successful: $cfm"); -ok($t->CustomFieldValues($testcf->Id)->Count == 2, - "This ticket has 2 custom field values"); -ok($t3->CustomFieldValues($testcf->Id)->Count == 1, - "This ticket has 1 custom field value"); - -=end testing - -=cut - -use strict; -no warnings qw(redefine); - -use RT::Queue; -use RT::User; -use RT::Record; -use RT::Links; -use RT::Date; -use RT::CustomFields; -use RT::TicketCustomFieldValues; -use RT::Tickets; -use RT::URI::fsck_com_rt; -use RT::URI; - -=begin testing - - -ok(require RT::Ticket, "Loading the RT::Ticket library"); - -=end testing - -=cut - -# }}} - -# {{{ LINKTYPEMAP -# A helper table for relationships mapping to make it easier -# to build and parse links between tickets - -use vars '%LINKTYPEMAP'; - -%LINKTYPEMAP = ( - MemberOf => { Type => 'MemberOf', - Mode => 'Target', }, - Members => { Type => 'MemberOf', - Mode => 'Base', }, - HasMember => { Type => 'MemberOf', - Mode => 'Base', }, - RefersTo => { Type => 'RefersTo', - Mode => 'Target', }, - ReferredToBy => { Type => 'RefersTo', - Mode => 'Base', }, - DependsOn => { Type => 'DependsOn', - Mode => 'Target', }, - DependedOnBy => { Type => 'DependsOn', - Mode => 'Base', }, - -); - -# }}} - -# {{{ LINKDIRMAP -# A helper table for relationships mapping to make it easier -# to build and parse links between tickets - -use vars '%LINKDIRMAP'; - -%LINKDIRMAP = ( - MemberOf => { Base => 'MemberOf', - Target => 'HasMember', }, - RefersTo => { Base => 'RefersTo', - Target => 'ReferredToBy', }, - DependsOn => { Base => 'DependsOn', - Target => 'DependedOnBy', }, - -); - -# }}} - -# {{{ sub Load - -=head2 Load - -Takes a single argument. This can be a ticket id, ticket alias or -local ticket uri. If the ticket can't be loaded, returns undef. -Otherwise, returns the ticket id. - -=cut - -sub Load { - my $self = shift; - my $id = shift; - - #TODO modify this routine to look at EffectiveId and do the recursive load - # thing. be careful to cache all the interim tickets we try so we don't loop forever. - - #If it's a local URI, turn it into a ticket id - if ( $id =~ /^$RT::TicketBaseURI(\d+)$/ ) { - $id = $1; - } - - #If it's a remote URI, we're going to punt for now - elsif ( $id =~ '://' ) { - return (undef); - } - - #If we have an integer URI, load the ticket - if ( $id =~ /^\d+$/ ) { - my $ticketid = $self->LoadById($id); - - unless ($ticketid) { - $RT::Logger->debug("$self tried to load a bogus ticket: $id\n"); - return (undef); - } - } - - #It's not a URI. It's not a numerical ticket ID. Punt! - else { - return (undef); - } - - #If we're merged, resolve the merge. - if ( ( $self->EffectiveId ) and ( $self->EffectiveId != $self->Id ) ) { - return ( $self->Load( $self->EffectiveId ) ); - } - - #Ok. we're loaded. lets get outa here. - return ( $self->Id ); - -} - -# }}} - -# {{{ sub LoadByURI - -=head2 LoadByURI - -Given a local ticket URI, loads the specified ticket. - -=cut - -sub LoadByURI { - my $self = shift; - my $uri = shift; - - if ( $uri =~ /^$RT::TicketBaseURI(\d+)$/ ) { - my $id = $1; - return ( $self->Load($id) ); - } - else { - return (undef); - } -} - -# }}} - -# {{{ sub Create - -=head2 Create (ARGS) - -Arguments: ARGS is a hash of named parameters. Valid parameters are: - - 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 - 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) - TimeEstimated -- an integer. estimated time for this task in minutes - TimeWorked -- an integer. time worked so far in minutes - TimeLeft -- an integer. time remaining in minutes - 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. - CustomField-<n> -- a scalar or array of values for the customfield with the id <n> - - -Returns: TICKETID, Transaction Object, Error Message - - -=begin testing - -my $t = RT::Ticket->new($RT::SystemUser); - -ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created"); - -ok ( my $id = $t->Id, "Got ticket id"); -ok ($t->RefersTo->First->Target =~ /fsck.com/, "Got refers to"); -ok ($t->ReferredToBy->First->Base =~ /cpan.org/, "Got referredtoby"); -ok ($t->ResolvedObj->Unix == -1, "It hasn't been resolved - ". $t->ResolvedObj->Unix); - -=end testing - -=cut - -sub Create { - my $self = shift; - - 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'} ); - } - elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) { - $QueueObj->Load( $args{'Queue'}->Id ); - } - else { - $RT::Logger->debug( $args{'Queue'} . " not a recognised queue object."); - } -; - - #Can't create a ticket without a queue. - unless ( defined($QueueObj) && $QueueObj->Id ) { - $RT::Logger->debug("$self No queue given for ticket creation."); - return ( 0, 0, $self->loc('Could not create ticket. Queue not set') ); - } - - #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 ( $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'} ); - - #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'} ); - - # 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 + - # 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 ( $args{'Due'} ) { - $Due->Set( Format => 'ISO', Value => $args{'Due'} ); - } - elsif ( $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'} ); - } - - my $Started = new RT::Date( $self->CurrentUser ); - if ( defined $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'} ); - } - - - #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'}); - $Resolved->SetToNow; - } - - # }}} - - # {{{ Dealing with time fields - - $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'}; - $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'}; - $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'}; - - # }}} - - # {{{ 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 ( defined $args{'Owner'} ) { - $Owner = RT::User->new( $self->CurrentUser ); - $Owner->Load( $args{'Owner'} ); - - } - - #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' ) ) - ) { - - $RT::Logger->warning( "User " - . $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'."); - - $Owner = undef; - } - - #If we haven't been handed a valid owner, make it nobody. - unless ( defined($Owner) && $Owner->Id ) { - $Owner = new RT::User( $self->CurrentUser ); - $Owner->Load( $RT::Nobody->Id ); - } - - # }}} - - # 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 && $watcher !~ /^\d+$/); - } - } - - - $RT::Handle->BeginTransaction(); - - 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}); - } - - # Delete null integer parameters - foreach my $attr qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority) { - delete $params{$attr} unless (exists $params{$attr} && $params{$attr}); - } - - - my $id = $self->SUPER::Create( %params); - unless ($id) { - $RT::Logger->crit( "Couldn't create a ticket"); - $RT::Handle->Rollback(); - 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 => ($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") ); - } - - 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." ); - $RT::Handle->Rollback(); - return ( 0, 0, - $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 - - $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} ) ) { - - # If there is an empty entry in the list, let's get out of here. - next unless $watcher; - - # 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 ); - } - else { - ( $wval, $wmsg ) = $self->_AddWatcher( Type => $type, - $field => $watcher, - Silent => 1 ); - } - - push @non_fatal_errors, $wmsg unless ($wval); - } - } - - # }}} - # {{{ Deal with setting up links - - - foreach my $type ( keys %LINKTYPEMAP ) { - next unless (defined $args{$type}); - foreach my $link ( - ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) - { - my ( $wval, $wmsg ) = $self->AddLink( - Type => $LINKTYPEMAP{$type}->{'Type'}, - $LINKTYPEMAP{$type}->{'Mode'} => $link, - Silent => 1 - ); - - push @non_fatal_errors, $wmsg unless ($wval); - } - } - - # }}} - - # {{{ 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 (length($value)); - $self->_AddCustomFieldValue( Field => $cfid, - Value => $value, - RecordTransaction => 0 - ); - } - } - # }}} - - if ( $args{'_RecordTransaction'} ) { - # {{{ Add a transaction for the create - my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( - Type => "Create", - TimeTaken => 0, - 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); - } - else { - $RT::Handle->Rollback(); - - # TODO where does this get errstr from? - $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 { - - # 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 ); - - } -} - - -# }}} - -# {{{ 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-<id#> => 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 - -=head2 UpdateFrom822 $MESSAGE - -Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket. -Returns an um. ask me again when the code exists - - -=begin testing - -my $simple_update = <<EOF; -Subject: target -AddRequestor: jesse\@example.com -EOF - -my $ticket = RT::Ticket->new($RT::SystemUser); -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); -$jesse->LoadByEmail('jesse@example.com'); -ok ($jesse->Id, "There's a user for jesse"); -ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor "); - -=end testing - - -=cut - -sub UpdateFrom822 { - my $self = shift; - my $content = shift; - my %args = $self->_Parse822HeadersForAttributes($content); - - - 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'}, - Priority => $args{'priority'}, - 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'} - ); - - foreach my $type qw(Requestor Cc Admincc) { - - foreach my $action ( 'Add', 'Del', '' ) { - - my $lctag = lc($action) . lc($type); - foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) { - - foreach my $entry ( ref($list) ? @{$list} : ($list) ) { - push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry ); - } - - } - - # Todo: if we're given an explicit list, transmute it into a list of adds/deletes - - } - } - - # Add custom field entries to %ticketargs. - # TODO: allow named custom fields - map { - /^customfield-(\d+)$/ - && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} ); - } keys(%args); - -# for each ticket we've been told to update, iterate through the set of -# rfc822 headers and perform that update to the ticket. - - - # {{{ Set basic fields - my @attribs = qw( - Subject - FinalPriority - Priority - TimeEstimated - TimeWorked - TimeLeft - Status - Queue - Type - ); - - - # Resolve the queue from a name to a numeric id. - if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) { - my $tempqueue = RT::Queue->new($RT::SystemUser); - $tempqueue->Load( $ticketargs{'Queue'} ); - $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id ); - } - - # die "updaterecordobject is a webui thingy"; - my @results; - - foreach my $attribute (@attribs) { - my $value = $ticketargs{$attribute}; - - if ( $value ne $self->$attribute() ) { - - my $method = "Set$attribute"; - my ( $code, $msg ) = $self->$method($value); - - push @results, $self->loc($attribute) . ': ' . $msg; - - } - } - - # We special case owner changing, so we can use ForceOwnerChange - if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) { - my $ChownType = "Give"; - $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} ); - - my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType ); - push ( @results, $msg ); - } - - # }}} -# Deal with setting watchers - - -# Acceptable arguments: -# Requestor -# Requestors -# AddRequestor -# AddRequestors -# DelRequestor - - foreach my $type qw(Requestor Cc AdminCc) { - - # If we've been given a number of delresses to del, do it. - foreach my $address (@{$ticketargs{'Del'.$type}}) { - my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address); - push (@results, $msg) ; - } - - # If we've been given a number of addresses to add, do it. - foreach my $address (@{$ticketargs{'Add'.$type}}) { - $RT::Logger->debug("Adding $address as a $type"); - my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address); - push (@results, $msg) ; - - } - - -} - - -} -# }}} - -# {{{ _Parse822HeadersForAttributes Content - -=head2 _Parse822HeadersForAttributes Content - -Takes an RFC822 style message and parses its attributes into a hash. - -=cut - -sub _Parse822HeadersForAttributes { - my $self = shift; - my $content = shift; - my %args; - - my @lines = ( split ( /\n/, $content ) ); - while ( defined( my $line = shift @lines ) ) { - if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) { - my $value = $2; - my $tag = lc($1); - - $tag =~ s/-//g; - if ( defined( $args{$tag} ) ) - { #if we're about to get a second value, make it an array - $args{$tag} = [ $args{$tag} ]; - } - if ( ref( $args{$tag} ) ) - { #If it's an array, we want to push the value - push @{ $args{$tag} }, $value; - } - else { #if there's nothing there, just set the value - $args{$tag} = $value; - } - } elsif ($line =~ /^$/) { - - #TODO: this won't work, since "" isn't of the form "foo:value" - - while ( defined( my $l = shift @lines ) ) { - push @{ $args{'content'} }, $l; - } - } - - } - - foreach my $date qw(due starts started resolved) { - my $dateobj = RT::Date->new($RT::SystemUser); - if ( $args{$date} =~ /^\d+$/ ) { - $dateobj->Set( Format => 'unix', Value => $args{$date} ); - } - else { - $dateobj->Set( Format => 'unknown', Value => $args{$date} ); - } - $args{$date} = $dateobj->ISO; - } - $args{'mimeobj'} = MIME::Entity->new(); - $args{'mimeobj'}->build( - Type => ( $args{'contenttype'} || 'text/plain' ), - Data => ($args{'content'} || '') - ); - - return (%args); -} - -# }}} - -# {{{ sub Import - -=head2 Import PARAMHASH - -Import a ticket. -Doesn\'t create a transaction. -Doesn\'t supply queue defaults, etc. - -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, $self->loc('Could not create ticket. Queue not set') ); - } - - #Now that we have a queue, Check the ACLS - unless ( - $self->CurrentUser->HasRight( - Right => 'CreateTicket', - Object => $QueueObj - ) - ) - { - return ( 0, - $self->loc("No permission to create tickets in the queue '[_1]'" - , $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->HasRight( - Object => $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, $self->loc("'[_1]' is an invalid value for status", $args{'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'}, # loc - InitialPriority => $args{'InitialPriority'}, # loc - FinalPriority => $args{'FinalPriority'}, # loc - Priority => $args{'InitialPriority'}, # loc - Status => $args{'Status'}, # loc - TimeWorked => $args{'TimeWorked'}, # loc - Type => $args{'Type'}, # loc - Created => $args{'Created'}, # loc - Told => $args{'Told'}, # loc - LastUpdated => $args{'Updated'}, # loc - Resolved => $args{'Resolved'}, # loc - Due => $args{'Due'}, # loc - ); - - # 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 ); -} - -# }}} - - -# {{{ Routines dealing with watchers. - -# {{{ _CreateTicketGroups - -=head2 _CreateTicketGroups - -Create the ticket groups and relationships 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. - -It will return true on success and undef on failure. - -=begin testing - -my $ticket = RT::Ticket->new($RT::SystemUser); -my ($id, $msg) = $ticket->Create(Subject => "Foo", - Owner => $RT::SystemUser->Id, - Status => 'open', - Requestor => ['jesse@example.com'], - Queue => '1' - ); -ok ($id, "Ticket $id was created"); -ok(my $group = RT::Group->new($RT::SystemUser)); -ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor')); -ok ($group->Id, "Found the requestors object for this ticket"); - -ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user"); -$jesse->LoadByEmail('jesse@example.com'); -ok($jesse->Id, "Found the jesse rt user"); - - -ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor"); -ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor"); -ok ($add_id, "Add succeeded: ($add_msg)"); -ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user"); -$bob->LoadByEmail('bob@fsck.com'); -ok($bob->Id, "Found the bob rt user"); -ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");; -ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor"); -ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");; - - -$group = RT::Group->new($RT::SystemUser); -ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc')); -ok ($group->Id, "Found the cc object for this ticket"); -$group = RT::Group->new($RT::SystemUser); -ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc')); -ok ($group->Id, "Found the AdminCc object for this ticket"); -$group = RT::Group->new($RT::SystemUser); -ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner')); -ok ($group->Id, "Found the Owner object for this ticket"); -ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'"); - -=end testing - -=cut - - -sub _CreateTicketGroups { - my $self = shift; - - my @types = qw(Requestor Owner Cc AdminCc); - - foreach my $type (@types) { - my $type_obj = RT::Group->new($self->CurrentUser); - my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role', - Instance => $self->Id, - Type => $type); - unless ($id) { - $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ". - $self->Id.": ".$msg); - return(undef); - } - } - return(1); - -} - -# }}} - -# {{{ sub OwnerGroup - -=head2 OwnerGroup - -A constructor which returns an RT::Group object containing the owner of this ticket. - -=cut - -sub OwnerGroup { - my $self = shift; - my $owner_obj = RT::Group->new($self->CurrentUser); - $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner'); - return ($owner_obj); -} - -# }}} - - -# {{{ sub AddWatcher - -=head2 AddWatcher - -AddWatcher takes a parameter hash. The keys are as follows: - -Type One of Requestor, Cc, AdminCc - -PrinicpalId The RT::Principal id of the user or group that's being added as a watcher - -Email The email address of the new watcher. If a user with this - email address can't be found, a new nonprivileged user will be created. - -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 = ( - Type => undef, - PrincipalId => undef, - Email => undef, - @_ - ); - - # {{{ 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 - # 'WatchAsAdminCc' or 'ModifyTicket', bail - if ( $args{'Type'} eq 'AdminCc' ) { - 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')) - } - } - else { - $RT::Logger->warn( "$self -> AddWatcher got passed a bogus type"); - return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') ); - } - } - - # If the watcher isn't the current user - # and the current user doesn't have 'ModifyTicket' - # bail - else { - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - } - - # }}} - - return ( $self->_AddWatcher(%args) ); -} - -#This contains the meat of AddWatcher. but can be called from a routine like -# Create, which doesn't need the additional acl check -sub _AddWatcher { - my $self = shift; - my %args = ( - Type => undef, - Silent => undef, - PrincipalId => undef, - Email => undef, - @_ - ); - - - my $principal = RT::Principal->new($self->CurrentUser); - if ($args{'Email'}) { - my $user = RT::User->new($RT::SystemUser); - my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'}); - if ($pid) { - $args{'PrincipalId'} = $pid; - } - } - if ($args{'PrincipalId'}) { - $principal->Load($args{'PrincipalId'}); - } - - - # If we can't find this watcher, we need to bail. - unless ($principal->Id) { - $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id); - return(0, $self->loc("Could not find or create that user")); - } - - - 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")); - } - - if ( $group->HasMember( $principal)) { - - return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) ); - } - - - my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id, - InsideTransaction => 1 ); - unless ($m_id) { - $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg); - - return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) ); - } - - unless ( $args{'Silent'} ) { - $self->_NewTransaction( - Type => 'AddWatcher', - NewValue => $principal->Id, - Field => $args{'Type'} - ); - } - - return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) ); -} - -# }}} - - -# {{{ sub DeleteWatcher - -=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS } - - -Deletes a Ticket watcher. Takes two arguments: - -Type (one of Requestor,Cc,AdminCc) - -and one of - -PrincipalId (an RT::Principal Id of the watcher you want to remove) - OR -Email (the email address of an existing wathcer) - - -=cut - - -sub DeleteWatcher { - my $self = shift; - - my %args = ( Type => undef, - PrincipalId => undef, - Email => undef, - @_ ); - - unless ($args{'PrincipalId'} || $args{'Email'} ) { - return(0, $self->loc("No principal specified")); - } - 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); - } - # If we can't find this watcher, we need to bail. - 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")); - } - - # {{{ 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 - # 'WatchAsAdminCc' or 'ModifyTicket', bail - if ( $args{'Type'} eq 'AdminCc' ) { - 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')) - } - } - else { - $RT::Logger->warn( "$self -> DeleteWatcher got passed a bogus type"); - return ( 0, $self->loc('Error in parameters to Ticket->DelWatcher') ); - } - } - - # If the watcher isn't the current user - # and the current user doesn't have 'ModifyTicket' bail - else { - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - } - - # }}} - - - # 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'}) ); - } - - 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); - - 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'} - ); - } - - 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 RequestorAddresses - - B<Returns> 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); - -} - -# }}} - -# {{{ sub Cc - -=head2 Cc - -Takes nothing. -Returns an RT::Group object which contains this ticket's Ccs. -If the user doesn't have "ShowTicket" permission, returns an empty group - -=cut - -sub Cc { - my $self = shift; - - my $group = RT::Group->new($self->CurrentUser); - if ( $self->CurrentUserHasRight('ShowTicket') ) { - $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id); - } - return ($group); - -} - -# }}} - -# {{{ sub AdminCc - -=head2 AdminCc - -Takes nothing. -Returns an RT::Group object which contains this ticket's AdminCcs. -If the user doesn't have "ShowTicket" permission, returns an empty group - -=cut - -sub AdminCc { - my $self = shift; - - my $group = RT::Group->new($self->CurrentUser); - if ( $self->CurrentUserHasRight('ShowTicket') ) { - $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id); - } - return ($group); - -} - -# }}} - -# }}} - -# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc - -# {{{ sub IsWatcher -# a generic routine to be called by IsRequestor, IsCc and IsAdminCc - -=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL } - -Takes a param hash with the attributes Type and either PrincipalId or Email - -Type is one of Requestor, Cc, AdminCc and Owner - -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. - -=cut - -sub IsWatcher { - my $self = shift; - - my %args = ( Type => 'Requestor', - PrincipalId => undef, - Email => undef, - @_ - ); - - # Load the relevant group. - my $group = RT::Group->new($self->CurrentUser); - $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id); - - # Find the relevant principal. - my $principal = RT::Principal->new($self->CurrentUser); - if (!$args{PrincipalId} && $args{Email}) { - # Look up the specified user. - my $user = RT::User->new($self->CurrentUser); - $user->LoadByEmail($args{Email}); - if ($user->Id) { - $args{PrincipalId} = $user->PrincipalId; - } - else { - # A non-existent user can't be a group member. - return 0; - } - } - $principal->Load($args{'PrincipalId'}); - - # Ask if it has the member in question - return ($group->HasMember($principal)); -} - -# }}} - -# {{{ sub IsRequestor - -=head2 IsRequestor PRINCIPAL_ID - - Takes an RT::Principal id - Returns true if the principal is a requestor of the current ticket. - - -=cut - -sub IsRequestor { - my $self = shift; - my $person = shift; - - return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) ); - -}; - -# }}} - -# {{{ sub IsCc - -=head2 IsCc PRINCIPAL_ID - - Takes an RT::Principal id. - Returns true if the principal is a requestor of the current ticket. - - -=cut - -sub IsCc { - my $self = shift; - my $cc = shift; - - return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) ); - -} - -# }}} - -# {{{ sub IsAdminCc - -=head2 IsAdminCc PRINCIPAL_ID - - Takes an RT::Principal id. - Returns true if the principal is a requestor of the current ticket. - -=cut - -sub IsAdminCc { - my $self = shift; - my $person = shift; - - return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) ); - -} - -# }}} - -# {{{ sub IsOwner - -=head2 IsOwner - - Takes an RT::User object. Returns true if that user is this ticket's owner. -returns undef otherwise - -=cut - -sub IsOwner { - my $self = shift; - my $person = shift; - - # no ACL check since this is used in acl decisions - # unless ($self->CurrentUserHasRight('ShowTicket')) { - # return(undef); - # } - - #Tickets won't yet have owners when they're being created. - unless ( $self->OwnerObj->id ) { - return (undef); - } - - if ( $person->id == $self->OwnerObj->id ) { - return (1); - } - else { - return (undef); - } -} - -# }}} - -# }}} - -# }}} - -# {{{ Routines dealing with queues - -# {{{ sub ValidateQueue - -sub ValidateQueue { - my $self = shift; - my $Value = shift; - - if ( !$Value ) { - $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok."); - return (1); - } - - my $QueueObj = RT::Queue->new( $self->CurrentUser ); - my $id = $QueueObj->Load($Value); - - if ($id) { - return (1); - } - else { - return (undef); - } -} - -# }}} - -# {{{ sub SetQueue - -sub SetQueue { - my $self = shift; - my $NewQueue = shift; - - #Redundant. ACL gets checked in _Set; - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - - my $NewQueueObj = RT::Queue->new( $self->CurrentUser ); - $NewQueueObj->Load($NewQueue); - - unless ( $NewQueueObj->Id() ) { - return ( 0, $self->loc("That queue does not exist") ); - } - - if ( $NewQueueObj->Id == $self->QueueObj->Id ) { - return ( 0, $self->loc('That is the same value') ); - } - unless ( - $self->CurrentUser->HasRight( - Right => 'CreateTicket', - Object => $NewQueueObj - ) - ) - { - return ( 0, $self->loc("You may not create requests in that queue.") ); - } - - unless ( - $self->OwnerObj->HasRight( - Right => 'OwnTicket', - Object => $NewQueueObj - ) - ) - { - $self->Untake(); - } - - return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) ); - -} - -# }}} - -# {{{ sub QueueObj - -=head2 QueueObj - -Takes nothing. returns this ticket's queue object - -=cut - -sub QueueObj { - my $self = shift; - - my $queue_obj = RT::Queue->new( $self->CurrentUser ); - - #We call __Value so that we can avoid the ACL decision and some deep recursion - my ($result) = $queue_obj->Load( $self->__Value('Queue') ); - return ($queue_obj); -} - -# }}} - -# }}} - -# {{{ Date printing routines - -# {{{ sub DueObj - -=head2 DueObj - - Returns an RT::Date object containing this ticket's due date - -=cut - -sub DueObj { - my $self = shift; - - my $time = new RT::Date( $self->CurrentUser ); - - # -1 is RT::Date slang for never - if ( $self->Due ) { - $time->Set( Format => 'sql', Value => $self->Due ); - } - else { - $time->Set( Format => 'unix', Value => -1 ); - } - - return $time; -} - -# }}} - -# {{{ sub DueAsString - -=head2 DueAsString - -Returns this ticket's due date as a human readable string - -=cut - -sub DueAsString { - my $self = shift; - return $self->DueObj->AsString(); -} - -# }}} - -# {{{ sub ResolvedObj - -=head2 ResolvedObj - - Returns an RT::Date object of this ticket's 'resolved' time. - -=cut - -sub ResolvedObj { - my $self = shift; - - my $time = new RT::Date( $self->CurrentUser ); - $time->Set( Format => 'sql', Value => $self->Resolved ); - return $time; -} - -# }}} - -# {{{ sub SetStarted - -=head2 SetStarted - -Takes a date in ISO format or undef -Returns a transaction id and a message -The client calls "Start" to note that the project was started on the date in $date. -A null date means "now" - -=cut - -sub SetStarted { - my $self = shift; - my $time = shift || 0; - - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, self->loc("Permission Denied") ); - } - - #We create a date object to catch date weirdness - my $time_obj = new RT::Date( $self->CurrentUser() ); - if ( $time != 0 ) { - $time_obj->Set( Format => 'ISO', Value => $time ); - } - else { - $time_obj->SetToNow(); - } - - #Now that we're starting, open this ticket - #TODO do we really want to force this as policy? it should be a scrip - - #We need $TicketAsSystem, in case the current user doesn't have - #ShowTicket - # - my $TicketAsSystem = new RT::Ticket($RT::SystemUser); - $TicketAsSystem->Load( $self->Id ); - if ( $TicketAsSystem->Status eq 'new' ) { - $TicketAsSystem->Open(); - } - - return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) ); - -} - -# }}} - -# {{{ sub StartedObj - -=head2 StartedObj - - Returns an RT::Date object which contains this ticket's -'Started' time. - -=cut - -sub StartedObj { - my $self = shift; - - my $time = new RT::Date( $self->CurrentUser ); - $time->Set( Format => 'sql', Value => $self->Started ); - return $time; -} - -# }}} - -# {{{ sub StartsObj - -=head2 StartsObj - - Returns an RT::Date object which contains this ticket's -'Starts' time. - -=cut - -sub StartsObj { - my $self = shift; - - my $time = new RT::Date( $self->CurrentUser ); - $time->Set( Format => 'sql', Value => $self->Starts ); - return $time; -} - -# }}} - -# {{{ sub ToldObj - -=head2 ToldObj - - Returns an RT::Date object which contains this ticket's -'Told' time. - -=cut - -sub ToldObj { - my $self = shift; - - my $time = new RT::Date( $self->CurrentUser ); - $time->Set( Format => 'sql', Value => $self->Told ); - return $time; -} - -# }}} - -# {{{ sub ToldAsString - -=head2 ToldAsString - -A convenience method that returns ToldObj->AsString - -TODO: This should be deprecated - -=cut - -sub ToldAsString { - my $self = shift; - if ( $self->Told ) { - return $self->ToldObj->AsString(); - } - else { - return ("Never"); - } -} - -# }}} - -# {{{ sub TimeWorkedAsString - -=head2 TimeWorkedAsString - -Returns the amount of time worked on this ticket as a Text String - -=cut - -sub TimeWorkedAsString { - my $self = shift; - return "0" unless $self->TimeWorked; - - #This is not really a date object, but if we diff a number of seconds - #vs the epoch, we'll get a nice description of time worked. - - my $worked = new RT::Date( $self->CurrentUser ); - - #return the #of minutes worked turned into seconds and written as - # a simple text string - - return ( $worked->DurationAsString( $self->TimeWorked * 60 ) ); -} - -# }}} - -# }}} - -# {{{ Routines dealing with correspondence/comments - -# {{{ sub Comment - -=head2 Comment - -Comment on this ticket. -Takes a hashref with the following attributes: -If MIMEObj is undefined, Content will be used to build a MIME::Entity for this -commentl - -MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content. - -=cut - -## Please see file perltidy.ERR -sub Comment { - my $self = shift; - - my %args = ( CcMessageTo => undef, - BccMessageTo => undef, - MIMEObj => undef, - Content => undef, - TimeTaken => 0, - @_ ); - - unless ( ( $self->CurrentUserHasRight('CommentOnTicket') ) - or ( $self->CurrentUserHasRight('ModifyTicket') ) ) { - return ( 0, $self->loc("Permission Denied") ); - } - - 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") ); - } - } - - 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', - 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'}; - - #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, $self->loc("The comment has been recorded") ); -} - -# }}} - -# {{{ sub Correspond - -=head2 Correspond - -Correspond on this ticket. -Takes a hashref with the following attributes: - - -MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content - -if there's no MIMEObj, Content is used to build a MIME::Entity object - - -=cut - -sub Correspond { - my $self = shift; - my %args = ( CcMessageTo => undef, - BccMessageTo => undef, - MIMEObj => undef, - Content => undef, - TimeTaken => 0, - @_ ); - - unless ( ( $self->CurrentUserHasRight('ReplyToTicket') ) - or ( $self->CurrentUserHasRight('ModifyTicket') ) ) { - return ( 0, $self->loc("Permission Denied") ); - } - - 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") ); - } - } - - 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', - 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'}; - - #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'} ); - } - - #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, $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 - -sub MemberOf { - my $self = shift; - return ( $self->_Links( 'Base', 'MemberOf' ) ); -} - -# }}} - -# {{{ RefersTo - -=head2 RefersTo - - This returns an RT::Links object which shows all references for which this ticket is a base - -=cut - -sub RefersTo { - my $self = shift; - return ( $self->_Links( 'Base', 'RefersTo' ) ); -} - -# }}} - -# {{{ ReferredToBy - -=head2 ReferredToBy - - This returns an RT::Links object which shows all references for which this ticket is a target - -=cut - -sub ReferredToBy { - my $self = shift; - return ( $self->_Links( 'Target', 'RefersTo' ) ); -} - -# }}} - -# {{{ DependedOnBy - -=head2 DependedOnBy - - This returns an RT::Links object which references all the tickets that depend on this one - -=cut - -sub DependedOnBy { - my $self = shift; - return ( $self->_Links( 'Target', 'DependsOn' ) ); -} - -# }}} - - - -=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 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; - } - - if ($deps->Count > 0) { - return 1; - } - 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); - - my @live_statuses = RT::Queue->ActiveStatusArray(); - foreach my $status (@live_statuses) { - $deps->LimitStatus(VALUE => $status); - } - $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, - @_ - ); - - 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 ); - } - } - - if ($args{_top}) { - return map { $args{_found}{$_} } sort keys %{$args{_found}}; - } - else { - return 1; - } -} - -# }}} - -# {{{ DependsOn - -=head2 DependsOn - - This returns an RT::Links object which references all the tickets that this ticket depends on - -=cut - -sub DependsOn { - my $self = shift; - return ( $self->_Links( 'Base', 'DependsOn' ) ); -} - -# }}} - - - - -# {{{ sub _Links - -sub _Links { - my $self = shift; - - #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic --- - #tobias meant by $f - my $field = shift; - my $type = shift || ""; - - unless ( $self->{"$field$type"} ) { - $self->{"$field$type"} = new RT::Links( $self->CurrentUser ); - if ( $self->CurrentUserHasRight('ShowTicket') ) { - # Maybe this ticket is a merged ticket - my $Tickets = new RT::Tickets( $self->CurrentUser ); - # at least to myself - $self->{"$field$type"}->Limit( FIELD => $field, - VALUE => $self->URI, - ENTRYAGGREGATOR => 'OR' ); - $Tickets->Limit( FIELD => 'EffectiveId', - VALUE => $self->EffectiveId ); - while (my $Ticket = $Tickets->Next) { - $self->{"$field$type"}->Limit( FIELD => $field, - VALUE => $Ticket->URI, - ENTRYAGGREGATOR => 'OR' ); - } - $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, $self->loc('Permission Denied')) - - } - - #we want one of base and target. we don't care which - #but we only want _one_ - - 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") ); - } - 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'; - } - 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 ); - $remote_uri->FromURI( $remote_link ); - - my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( - Type => 'DeleteLink', - Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, - OldValue => $remote_uri->URI || $remote_link, - TimeTaken => 0 - ); - - return ( $Trans, $self->loc("Link deleted ([_1])", $TransString)); - } - - #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") ); - } -} - -# }}} - -# {{{ 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 => '', - 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") ); - } - 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'; - } - else { - return ( 0, $self->loc('Either base or target must be specified') ); - } - - # 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 - 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} ); - - unless ($linkid) { - return ( 0, $self->loc("Link could not be created") ); - } - - my $TransString = - "Ticket $args{'Base'} $args{Type} ticket $args{'Target'}."; - - # Don't write the transaction if we're doing this on create - if ( $args{'Silent'} ) { - return ( 1, $self->loc( "Link created ([_1])", $TransString ) ); - } - else { - my $remote_uri = RT::URI->new( $RT::SystemUser ); - $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 ) ); - } - -} - -# }}} - -# {{{ sub URI - -=head2 URI - -Returns this ticket's URI - -=cut - -sub URI { - my $self = shift; - my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser); - return($uri->URIForObject($self)); -} - -# }}} - -# {{{ sub MergeInto - -=head2 MergeInto -MergeInto take the id of the ticket to merge this ticket into. - -=cut - -sub MergeInto { - my $self = shift; - my $MergeInto = shift; - - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("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, $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..."); - 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") ); - } - - # 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, $self->loc("Merge failed. Couldn't set EffectiveId") ); - } - - my ( $status_val, $status_msg ) = $self->__Set( Field => 'Status', Value => 'resolved'); - - unless ($status_val) { - $RT::Logger->error( $self->loc("[_1] couldn't set status to resolved. RT's Database may be inconsistent.", $self) ); - } - - - # update all the links that point to that old ticket - my $old_links_to = RT::Links->new($self->CurrentUser); - $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI); - - while (my $link = $old_links_to->Next) { - if ($link->Base eq $NewTicket->URI) { - $link->Delete; - } else { - $link->SetTarget($NewTicket->URI); - } - - } - - my $old_links_from = RT::Links->new($self->CurrentUser); - $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI); - - while (my $link = $old_links_from->Next) { - if ($link->Target eq $NewTicket->URI) { - $link->Delete; - } else { - $link->SetBase($NewTicket->URI); - } - - } - - - #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); - } - - my $Ccs = $self->Cc->MembersObj; - while (my $watcher = $Ccs->Next) { - $NewTicket->_AddWatcher( Type => 'Cc', - Silent => 1, - PrincipalId => $watcher->MemberId); - } - - my $AdminCcs = $self->AdminCc->MembersObj; - while (my $watcher = $AdminCcs->Next) { - $NewTicket->_AddWatcher( Type => 'AdminCc', - Silent => 1, - PrincipalId => $watcher->MemberId); - } - - - #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() - ); - } - - #make a new link: this ticket is merged into that other ticket. - $self->AddLink( Type => 'MergedInto', Target => $NewTicket->Id()); - - $NewTicket->_SetLastUpdated; - - return ( 1, $self->loc("Merge Successful") ); -} - -# }}} - -# }}} - -# {{{ Routines dealing with ownership - -# {{{ sub OwnerObj - -=head2 OwnerObj - -Takes nothing and returns an RT::User object of -this ticket's owner - -=cut - -sub OwnerObj { - my $self = shift; - - #If this gets ACLed, we lose on a rights check in User.pm and - #get deep recursion. if we need ACLs here, we need - #an equiv without ACLs - - my $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. - -=begin testing - -my $root = RT::User->new($RT::SystemUser); -$root->Load('root'); -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"); -$t->Steal(); -ok ($t->OwnerObj->id eq $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'); -my $steal = $txns->First; -ok($steal->OldValue == $root->Id , "Stolen from root"); -ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser"); - -=end testing - -=cut - -sub SetOwner { - my $self = shift; - my $NewOwner = shift; - my $Type = shift || "Give"; - - # must have ModifyTicket rights - # or TakeTicket/StealTicket and $NewOwner is self - # see if it's a take - if ( $self->OwnerObj->Id == $RT::Nobody->Id ) { - unless ( $self->CurrentUserHasRight('ModifyTicket') - || $self->CurrentUserHasRight('TakeTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - } - - # see if it's a steal - elsif ( $self->OwnerObj->Id != $RT::Nobody->Id - && $self->OwnerObj->Id != $self->CurrentUser->id ) { - - unless ( $self->CurrentUserHasRight('ModifyTicket') - || $self->CurrentUserHasRight('StealTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - } - else { - unless ( $self->CurrentUserHasRight('ModifyTicket') ) { - return ( 0, $self->loc("Permission Denied") ); - } - } - my $NewOwnerObj = RT::User->new( $self->CurrentUser ); - my $OldOwnerObj = $self->OwnerObj; - - $NewOwnerObj->Load($NewOwner); - if ( !$NewOwnerObj->Id ) { - return ( 0, $self->loc("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, - $self->loc( -"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->HasRight( Right => 'OwnTicket', - Object => $self ) ) - ) { - return ( 0, $self->loc("That user may not own tickets 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, $self->loc("That user already owns that ticket") ); - } - - $RT::Handle->BeginTransaction(); - - # Delete the owner in the owner group, then add a new one - # TODO: is this safe? it's not how we really want the API to work - # for most things, but it's fast. - my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete(); - unless ($del_id) { - $RT::Handle->Rollback(); - return ( 0, $self->loc("Could not change owner. ") . $del_msg ); - } - - my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember( - PrincipalId => $NewOwnerObj->PrincipalId, - InsideTransaction => 1 ); - unless ($add_id) { - $RT::Handle->Rollback(); - return ( 0, $self->loc("Could not change owner. ") . $add_msg ); - } - - # We call set twice with slightly different arguments, so - # as to not have an SQL transaction span two RT transactions - - my ( $val, $msg ) = $self->_Set( - Field => 'Owner', - RecordTransaction => 0, - Value => $NewOwnerObj->Id, - TimeTaken => 0, - TransactionType => $Type, - CheckACL => 0, # don't check acl - ); - - unless ($val) { - $RT::Handle->Rollback; - return ( 0, $self->loc("Could not change owner. ") . $msg ); - } - - $RT::Handle->Commit(); - - my ( $trans, $msg, undef ) = $self->_NewTransaction( - Type => $Type, - Field => 'Owner', - NewValue => $NewOwnerObj->Id, - OldValue => $OldOwnerObj->Id, - TimeTaken => 0 ); - - if ($trans) { - $msg = $self->loc( "Owner changed from [_1] to [_2]", - $OldOwnerObj->Name, $NewOwnerObj->Name ); - - # TODO: make sure the trans committed properly - } - 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, $self->loc("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, rejected or deleted. - -Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE). If FORCE is true, ignore unresolved dependencies and force a status change. - -=begin testing - -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"); - -($id, $msg) = $tt->SetStatus('open'); -ok($id, $msg); -ok ($msg =~ /open/i, "Status message is correct"); -($id, $msg) = $tt->SetStatus('resolved'); -ok($id, $msg); -ok ($msg =~ /resolved/i, "Status message is correct"); -($id, $msg) = $tt->SetStatus('resolved'); -ok(!$id,$msg); - - -=end testing - - -=cut - -sub SetStatus { - my $self = shift; - my %args; - - if (@_ == 1) { - $args{Status} = shift; - } - else { - %args = (@_); - } - - #Check ACL - 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) { - return (0, $self->loc('That ticket has unresolved dependencies')); - } - - my $now = RT::Date->new( $self->CurrentUser ); - $now->SetToNow(); - - #If we're changing the status from new, record that we've started - if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) { - - #Set the Started time to "now" - $self->_Set( Field => 'Started', - Value => $now->ISO, - RecordTransaction => 0 ); - } - - if ( $args{Status} =~ /^(resolved|rejected|dead)$/ ) { - - #When we resolve a ticket, set the 'Resolved' attribute to now. - $self->_Set( Field => 'Resolved', - Value => $now->ISO, - RecordTransaction => 0 ); - } - - #Actually update the status - my ($val, $msg)= $self->_Set( Field => 'Status', - Value => $args{Status}, - TimeTaken => 0, - TransactionType => 'Status' ); - - return($val,$msg); -} - -# }}} - -# {{{ sub Kill - -=head2 Kill - -Takes no arguments. Marks this ticket for garbage collection - -=cut - -sub Kill { - my $self = shift; - $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead."); - return $self->Delete; -} - -sub Delete { - my $self = shift; - return ( $self->SetStatus('deleted') ); - - # 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 Reject - -=head2 Reject - -Sets this ticket's status to rejected - -=cut - -sub Reject { - my $self = shift; - return ( $self->SetStatus('rejected') ); -} - -# }}} - -# {{{ 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') ); -} - -# }}} - -# }}} - -# {{{ 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()); - $cf_values->OrderBy( FIELD => 'id' ); - - # @values is a CustomFieldValues object; - return ($cf_values); -} - -# }}} - -# {{{ AddCustomFieldValue - -=item AddCustomFieldValue { Field => FIELD, Value => VALUE } - -VALUE should be 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 - -=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, $self->loc("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'} - ); - - - $self->Load($self->Id); - - $RT::Logger->warning($msg) unless $transaction; - - $self->_SetLastUpdated; - - if ( defined $args{'TimeTaken'} ) { - $self->_UpdateTimeTaken( $args{'TimeTaken'} ); - } - if ( $RT::UseTransactionBatch and $transaction ) { - push @{$self->{_TransactionBatch}}, $trans; - } - return ( $transaction, $msg, $trans ); -} - -# }}} - -=head2 TransactionBatch - - Returns an array reference of all transactions created on this ticket during - this ticket object's lifetime, or undef if there were none. - - Only works when the $RT::UseTransactionBatch config variable is set to true. - -=cut - -sub TransactionBatch { - my $self = shift; - return $self->{_TransactionBatch}; -} - -sub DESTROY { - my $self = shift; - - # 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}++; - - my $batch = $self->TransactionBatch or return; - require RT::Scrips; - RT::Scrips->new($RT::SystemUser)->Apply( - Stage => 'TransactionBatch', - TicketObj => $self, - TransactionObj => $batch->[0], - ); -} - -# }}} - -# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record - -# {{{ sub _ClassAccessible - -sub _ClassAccessible { - { - EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 }, - Queue => { 'read' => 1, 'write' => 1 }, - Requestors => { 'read' => 1, 'write' => 1 }, - Owner => { 'read' => 1, 'write' => 1 }, - Subject => { 'read' => 1, 'write' => 1 }, - InitialPriority => { 'read' => 1, 'write' => 1 }, - FinalPriority => { 'read' => 1, 'write' => 1 }, - Priority => { 'read' => 1, 'write' => 1 }, - Status => { 'read' => 1, 'write' => 1 }, - 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 }, - 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; - - my %args = ( Field => undef, - Value => undef, - TimeTaken => 0, - RecordTransaction => 1, - UpdateTicket => 1, - CheckACL => 1, - TransactionType => 'Set', - @_ ); - - if ($args{'CheckACL'}) { - unless ( $self->CurrentUserHasRight('ModifyTicket')) { - return ( 0, $self->loc("Permission Denied")); - } - } - - unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) { - $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket"); - return(0, $self->loc("Internal Error")); - } - - #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'}"); - - my ($ret, $msg); - if ( $args{'UpdateTicket'} ) { - - #Set the new value - ( $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, scalar $TransObj->Description ); - } - else { - return ( $ret, $msg ); - } -} - -# }}} - -# {{{ sub _Value - -=head2 _Value - -Takes the name of a table column. -Returns its value as a string, if the user passes an ACL check - -=cut - -sub _Value { - - my $self = shift; - my $field = shift; - - #if the field is public, return it. - if ( $self->_Accessible( $field, 'public' ) ) { - - #$RT::Logger->debug("Skipping ACL check for $field\n"); - return ( $self->SUPER::_Value($field) ); - - } - - #If the current user doesn't have ACLs, don't let em at it. - - unless ( $self->CurrentUserHasRight('ShowTicket') ) { - return (undef); - } - return ( $self->SUPER::_Value($field) ); - -} - -# }}} - -# {{{ sub _UpdateTimeTaken - -=head2 _UpdateTimeTaken - -This routine will increment the timeworked counter. it should -only be called from _NewTransaction - -=cut - -sub _UpdateTimeTaken { - my $self = shift; - my $Minutes = shift; - my ($Total); - - $Total = $self->SUPER::_Value("TimeWorked"); - $Total = ( $Total || 0 ) + ( $Minutes || 0 ); - $self->SUPER::_Set( - Field => "TimeWorked", - Value => $Total - ); - - return ($Total); -} - -# }}} - -# }}} - -# {{{ Routines dealing with ACCESS CONTROL - -# {{{ sub CurrentUserHasRight - -=head2 CurrentUserHasRight - - Takes the textual name of a Ticket scoped right (from RT::ACE) and returns -1 if the user has that right. It returns 0 if the user doesn't have that right. - -=cut - -sub CurrentUserHasRight { - my $self = shift; - my $right = shift; - - return ( - $self->HasRight( - Principal => $self->CurrentUser->UserObj(), - Right => "$right" - ) - ); - -} - -# }}} - -# {{{ sub HasRight - -=head2 HasRight - - Takes a paramhash with the attributes 'Right' and 'Principal' - 'Right' is a ticket-scoped textual right from RT::ACE - 'Principal' is an RT::User object - - Returns 1 if the principal has the right. Returns undef if not. - -=cut - -sub HasRight { - my $self = shift; - my %args = ( - Right => undef, - Principal => undef, - @_ - ); - - unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) ) - { - $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight"); - } - - return ( - $args{'Principal'}->HasRight( - Object => $self, - Right => $args{'Right'} - ) - ); -} - -# }}} - -# }}} - -1; - -=head1 AUTHOR - -Jesse Vincent, jesse@bestpractical.com - -=head1 SEE ALSO - -RT - -=cut - |