+ # If the other URI is an RT::Ticket, we want to make sure the user
+ # can modify it too...
+ my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+ return (0, $msg) unless $status;
+ if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+ $right++;
+ }
+ if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+ ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+ {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
+ return ( 0, $Msg ) unless $val;
+
+ return ( $val, $Msg ) if $args{'Silent'};
+
+ my ($direction, $remote_link);
+
+ if ( $args{'Base'} ) {
+ $remote_link = $args{'Base'};
+ $direction = 'Target';
+ }
+ elsif ( $args{'Target'} ) {
+ $remote_link = $args{'Target'};
+ $direction = 'Base';
+ }
+
+ my $remote_uri = RT::URI->new( $self->CurrentUser );
+ $remote_uri->FromURI( $remote_link );
+
+ unless ( $args{ 'Silent'. $direction } ) {
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => 'DeleteLink',
+ Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
+ OldValue => $remote_uri->URI || $remote_link,
+ TimeTaken => 0
+ );
+ $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
+ }
+
+ if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
+ my $OtherObj = $remote_uri->Object;
+ my ( $val, $Msg ) = $OtherObj->_NewTransaction(
+ Type => 'DeleteLink',
+ Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+ : $LINKDIRMAP{$args{'Type'}}->{Target},
+ OldValue => $self->URI,
+ ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
+ TimeTaken => 0,
+ );
+ $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
+ }
+
+ return ( $val, $Msg );
+}
+
+
+
+=head2 AddLink
+
+Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
+
+If Silent is true then no transaction would be recorded, in other
+case you can control creation of transactions on both base and
+target with SilentBase and SilentTarget respectively. By default
+both transactions are created.
+
+=cut
+
+sub AddLink {
+ my $self = shift;
+ my %args = ( Target => '',
+ Base => '',
+ Type => '',
+ Silent => undef,
+ SilentBase => undef,
+ SilentTarget => undef,
+ @_ );
+
+ unless ( $args{'Target'} || $args{'Base'} ) {
+ $RT::Logger->error("Base or Target must be specified");
+ return ( 0, $self->loc('Either base or target must be specified') );
+ }
+
+ my $right = 0;
+ $right++ if $self->CurrentUserHasRight('ModifyTicket');
+ if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # If the other URI is an RT::Ticket, we want to make sure the user
+ # can modify it too...
+ my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+ return (0, $msg) unless $status;
+ if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+ $right++;
+ }
+ if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
+ ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
+ {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ return ( 0, "Can't link to a deleted ticket" )
+ if $other_ticket && lc $other_ticket->Status eq 'deleted';
+
+ return $self->_AddLink(%args);
+}
+
+sub __GetTicketFromURI {
+ my $self = shift;
+ my %args = ( URI => '', @_ );
+
+ # If the other URI is an RT::Ticket, we want to make sure the user
+ # can modify it too...
+ my $uri_obj = RT::URI->new( $self->CurrentUser );
+ unless ($uri_obj->FromURI( $args{'URI'} )) {
+ my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
+ $RT::Logger->warning( $msg );
+ return( 0, $msg );
+ }
+ my $obj = $uri_obj->Resolver->Object;
+ unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
+ return (1, 'Found not a ticket', undef);
+ }
+ return (1, 'Found ticket', $obj);
+}
+
+=head2 _AddLink
+
+Private non-acled variant of AddLink so that links can be added during create.
+
+=cut
+
+sub _AddLink {
+ my $self = shift;
+ my %args = ( Target => '',
+ Base => '',
+ Type => '',
+ Silent => undef,
+ SilentBase => undef,
+ SilentTarget => undef,
+ @_ );
+
+ my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
+ return ($val, $msg) if !$val || $exist;
+ return ($val, $msg) if $args{'Silent'};
+
+ my ($direction, $remote_link);
+ if ( $args{'Target'} ) {
+ $remote_link = $args{'Target'};
+ $direction = 'Base';
+ } elsif ( $args{'Base'} ) {
+ $remote_link = $args{'Base'};
+ $direction = 'Target';
+ }
+
+ my $remote_uri = RT::URI->new( $self->CurrentUser );
+ $remote_uri->FromURI( $remote_link );
+
+ unless ( $args{ 'Silent'. $direction } ) {
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => 'AddLink',
+ Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
+ NewValue => $remote_uri->URI || $remote_link,
+ TimeTaken => 0
+ );
+ $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
+ }
+
+ if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
+ my $OtherObj = $remote_uri->Object;
+ my ( $val, $msg ) = $OtherObj->_NewTransaction(
+ Type => 'AddLink',
+ Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+ : $LINKDIRMAP{$args{'Type'}}->{Target},
+ NewValue => $self->URI,
+ ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
+ TimeTaken => 0,
+ );
+ $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
+ }
+
+ return ( $val, $msg );
+}
+
+
+
+
+=head2 MergeInto
+
+MergeInto take the id of the ticket to merge this ticket into.
+
+=cut
+
+sub MergeInto {
+ my $self = shift;
+ my $ticket_id = shift;
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # Load up the new ticket.
+ my $MergeInto = RT::Ticket->new($self->CurrentUser);
+ $MergeInto->Load($ticket_id);
+
+ # make sure it exists.
+ unless ( $MergeInto->Id ) {
+ return ( 0, $self->loc("New ticket doesn't exist") );
+ }
+
+ # Make sure the current user can modify the new ticket.
+ unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ delete $MERGE_CACHE{'effective'}{ $self->id };
+ delete @{ $MERGE_CACHE{'merged'} }{
+ $ticket_id, $MergeInto->id, $self->id
+ };
+
+ $RT::Handle->BeginTransaction();
+
+ $self->_MergeInto( $MergeInto );
+
+ $RT::Handle->Commit();
+
+ return ( 1, $self->loc("Merge Successful") );
+}
+
+sub _MergeInto {
+ my $self = shift;
+ my $MergeInto = shift;
+
+
+ # 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 separate database query for merge info everytime
+ # loaded a ticket.
+
+ #update this ticket's effective id to the new ticket's id.
+ my ( $id_val, $id_msg ) = $self->__Set(
+ Field => 'EffectiveId',
+ Value => $MergeInto->Id()
+ );
+
+ unless ($id_val) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
+ }
+
+
+ my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
+ if ( $force_status && $force_status ne $self->__Value('Status') ) {
+ my ( $status_val, $status_msg )
+ = $self->__Set( Field => 'Status', Value => $force_status );
+
+ unless ($status_val) {
+ $RT::Handle->Rollback();
+ $RT::Logger->error(
+ "Couldn't set status to $force_status. RT's Database may be inconsistent."
+ );
+ return ( 0, $self->loc("Merge failed. Couldn't set Status") );
+ }
+ }
+
+ # 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);
+
+ my %old_seen;
+ while (my $link = $old_links_to->Next) {
+ if (exists $old_seen{$link->Base."-".$link->Type}) {
+ $link->Delete;
+ }
+ elsif ($link->Base eq $MergeInto->URI) {
+ $link->Delete;
+ } else {
+ # First, make sure the link doesn't already exist. then move it over.
+ my $tmp = RT::Link->new(RT->SystemUser);
+ $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
+ if ($tmp->id) {
+ $link->Delete;
+ } else {
+ $link->SetTarget($MergeInto->URI);
+ $link->SetLocalTarget($MergeInto->id);
+ }
+ $old_seen{$link->Base."-".$link->Type} =1;
+ }
+
+ }
+
+ 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 (exists $old_seen{$link->Type."-".$link->Target}) {
+ $link->Delete;
+ }
+ if ($link->Target eq $MergeInto->URI) {
+ $link->Delete;
+ } else {
+ # First, make sure the link doesn't already exist. then move it over.
+ my $tmp = RT::Link->new(RT->SystemUser);
+ $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
+ if ($tmp->id) {
+ $link->Delete;
+ } else {
+ $link->SetBase($MergeInto->URI);
+ $link->SetLocalBase($MergeInto->id);
+ $old_seen{$link->Type."-".$link->Target} =1;
+ }
+ }
+
+ }
+
+ # Update time fields
+ foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
+
+ my $mutator = "Set$type";
+ $MergeInto->$mutator(
+ ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
+
+ }
+#add all of this ticket's watchers to that ticket.
+ foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
+
+ my $people = $self->$watcher_type->MembersObj;
+ my $addwatcher_type = $watcher_type;
+ $addwatcher_type =~ s/s$//;
+
+ while ( my $watcher = $people->Next ) {
+
+ my ($val, $msg) = $MergeInto->_AddWatcher(
+ Type => $addwatcher_type,
+ Silent => 1,
+ PrincipalId => $watcher->MemberId
+ );
+ unless ($val) {
+ $RT::Logger->debug($msg);
+ }
+ }
+
+ }
+
+ #find all of the tickets that were merged into this ticket.
+ my $old_mergees = RT::Tickets->new( $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 => $MergeInto->Id()
+ );
+ }
+
+ #make a new link: this ticket is merged into that other ticket.
+ $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
+
+ $MergeInto->_SetLastUpdated;
+}
+
+=head2 Merged
+
+Returns list of tickets' ids that's been merged into this ticket.
+
+=cut
+
+sub Merged {
+ my $self = shift;
+
+ my $id = $self->id;
+ return @{ $MERGE_CACHE{'merged'}{ $id } }
+ if $MERGE_CACHE{'merged'}{ $id };
+
+ my $mergees = RT::Tickets->new( $self->CurrentUser );
+ $mergees->Limit(
+ FIELD => 'EffectiveId',
+ VALUE => $id,
+ );
+ $mergees->Limit(
+ FIELD => 'id',
+ OPERATOR => '!=',
+ VALUE => $id,
+ );
+ return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
+ = map $_->id, @{ $mergees->ItemsArrayRef || [] };
+}
+
+
+
+
+
+=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 = RT::User->new( $self->CurrentUser );
+ $owner->Load( $self->__Value('Owner') );
+
+ #Return the owner object
+ return ($owner);
+}
+
+
+
+=head2 OwnerAsString
+
+Returns the owner's email address
+
+=cut
+
+sub OwnerAsString {
+ my $self = shift;
+ return ( $self->OwnerObj->EmailAddress );
+
+}
+
+
+
+=head2 SetOwner
+
+Takes two arguments:
+ the Id or Name of the owner
+and (optionally) the type of the SetOwner Transaction. It defaults
+to 'Set'. 'Steal' is also a valid option.
+
+
+=cut
+
+sub SetOwner {
+ my $self = shift;
+ my $NewOwner = shift;
+ my $Type = shift || "Set";
+
+ $RT::Handle->BeginTransaction();
+
+ $self->_SetLastUpdated(); # lock the ticket
+ $self->Load( $self->id ); # in case $self changed while waiting for lock
+
+ my $OldOwnerObj = $self->OwnerObj;
+
+ my $NewOwnerObj = RT::User->new( $self->CurrentUser );
+ $NewOwnerObj->Load( $NewOwner );
+ unless ( $NewOwnerObj->Id ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("That user does not exist") );
+ }
+
+
+ # must have ModifyTicket rights
+ # or TakeTicket/StealTicket and $NewOwner is self
+ # see if it's a take
+ if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ || $self->CurrentUserHasRight('TakeTicket') ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+
+ # see if it's a steal
+ elsif ( $OldOwnerObj->Id != RT->Nobody->Id
+ && $OldOwnerObj->Id != $self->CurrentUser->id ) {
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ || $self->CurrentUserHasRight('StealTicket') ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+ else {
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+
+ # If we're not stealing and the ticket has an owner and it's not
+ # the current user
+ if ( $Type ne 'Steal' and $Type ne 'Force'
+ and $OldOwnerObj->Id != RT->Nobody->Id
+ and $OldOwnerObj->Id != $self->CurrentUser->Id )
+ {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("You can only take tickets that are unowned") )
+ if $NewOwnerObj->id == $self->CurrentUser->id;
+ 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->HasRight( Right => 'OwnTicket', Object => $self ) ) {
+ $RT::Handle->Rollback();
+ 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 ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("That user already owns that ticket") );
+ }
+
+ # 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 );
+ for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
+ ($del_id, $del_msg) = $owner->Delete();
+ last unless ($del_id);
+ }
+
+ unless ($del_id) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Could not change owner: [_1]", $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: [_1]", $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 => 'Set',
+ CheckACL => 0, # don't check acl
+ );
+
+ unless ($val) {
+ $RT::Handle->Rollback;
+ return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
+ }
+
+ ($val, $msg) = $self->_NewTransaction(
+ Type => 'Set',
+ Field => 'Owner',
+ NewValue => $NewOwnerObj->Id,
+ OldValue => $OldOwnerObj->Id,
+ TimeTaken => 0,
+ );
+
+ if ( $val ) {
+ $msg = $self->loc( "Owner changed from [_1] to [_2]",
+ $OldOwnerObj->Name, $NewOwnerObj->Name );
+ }
+ else {
+ $RT::Handle->Rollback();
+ return ( 0, $msg );
+ }
+
+ $RT::Handle->Commit();
+
+ return ( $val, $msg );
+}
+
+
+
+=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' ) );
+}
+
+
+
+=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' ) );
+}
+
+
+
+=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' ) );
+
+ }
+
+}
+
+
+
+
+
+=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
+ return 1 if $self->QueueObj->IsValidStatus($status);
+
+ my $i = 0;
+ while ( my $caller = (caller($i++))[3] ) {
+ return 1 if $caller eq 'RT::Ticket::SetQueue';
+ }
+
+ return 0;
+}
+
+sub Status {
+ my $self = shift;
+ my $value = $self->_Value( 'Status' );
+ return $value unless $self->QueueObj;
+ return $self->QueueObj->Lifecycle->CanonicalCase( $value );
+}
+
+=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, SetStarted => SETSTARTED ).
+If FORCE is true, ignore unresolved dependencies and force a status change.
+if SETSTARTED is true( it's the default value), set Started to current datetime if Started
+is not set and the status is changed from initial to not initial.
+
+=cut
+
+sub SetStatus {
+ my $self = shift;
+ my %args;
+ if (@_ == 1) {
+ $args{Status} = shift;
+ }
+ else {
+ %args = (@_);
+ }
+
+ # this only allows us to SetStarted, not we must SetStarted.
+ # this option was added for rtir initially
+ $args{SetStarted} = 1 unless exists $args{SetStarted};
+
+
+ my $lifecycle = $self->QueueObj->Lifecycle;
+
+ my $new = lc $args{'Status'};
+ unless ( $lifecycle->IsValid( $new ) ) {
+ return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
+ }
+
+ my $old = $self->__Value('Status');
+ unless ( $lifecycle->IsTransition( $old => $new ) ) {
+ return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
+ }
+
+ my $check_right = $lifecycle->CheckRight( $old => $new );
+ unless ( $self->CurrentUserHasRight( $check_right ) ) {
+ return ( 0, $self->loc('Permission Denied') );
+ }
+
+ if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
+ return (0, $self->loc('That ticket has unresolved dependencies'));
+ }
+
+ my $now = RT::Date->new( $self->CurrentUser );
+ $now->SetToNow();
+
+ my $raw_started = RT::Date->new(RT->SystemUser);
+ $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
+
+ #If we're changing the status from new, record that we've started
+ if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
+ #Set the Started time to "now"
+ $self->_Set(
+ Field => 'Started',
+ Value => $now->ISO,
+ RecordTransaction => 0
+ );
+ }
+
+ #When we close a ticket, set the 'Resolved' attribute to now.
+ # It's misnamed, but that's just historical.
+ if ( $lifecycle->IsInactive($new) ) {
+ $self->_Set(
+ Field => 'Resolved',
+ Value => $now->ISO,
+ RecordTransaction => 0,
+ );
+ }
+
+ #Actually update the status
+ my ($val, $msg)= $self->_Set(
+ Field => 'Status',
+ Value => $new,
+ TimeTaken => 0,
+ CheckACL => 0,
+ TransactionType => 'Status',
+ );
+ return ($val, $msg);
+}
+
+
+
+=head2 Delete
+
+Takes no arguments. Marks this ticket for garbage collection
+
+=cut
+
+sub Delete {
+ my $self = shift;
+ unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
+ return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
+ }
+ return ( $self->SetStatus('deleted') );
+}
+
+
+=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 = RT::Date->new( $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 = RT::Date->new( $self->CurrentUser );
+ $now->SetToNow();
+
+ #use __Set to get no ACLs ;)
+ return ( $self->__Set( Field => 'Told',
+ Value => $now->ISO ) );
+}
+
+=head2 SeenUpTo
+
+
+=cut
+
+sub SeenUpTo {
+ my $self = shift;
+ my $uid = $self->CurrentUser->id;
+ my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
+ return if $attr && $attr->Content gt $self->LastUpdated;
+
+ my $txns = $self->Transactions;
+ $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
+ $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
+ $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
+ $txns->Limit(
+ FIELD => 'Created',
+ OPERATOR => '>',
+ VALUE => $attr->Content
+ ) if $attr;
+ $txns->RowsPerPage(1);
+ return $txns->First;
+}
+
+=head2 RanTransactionBatch
+
+Acts as a guard around running TransactionBatch scrips.
+
+Should be false until you enter the code that runs TransactionBatch scrips
+
+Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
+
+=cut
+
+sub RanTransactionBatch {
+ my $self = shift;
+ my $val = shift;
+
+ if ( defined $val ) {
+ return $self->{_RanTransactionBatch} = $val;
+ } else {
+ return $self->{_RanTransactionBatch};
+ }
+
+}
+
+
+=head2 TransactionBatch
+
+Returns an array reference of all transactions created on this ticket during
+this ticket object's lifetime or since last application of a batch, or undef
+if there were none.
+
+Only works when the C<UseTransactionBatch> config option is set to true.
+
+=cut
+
+sub TransactionBatch {
+ my $self = shift;
+ return $self->{_TransactionBatch};
+}
+
+=head2 ApplyTransactionBatch
+
+Applies scrips on the current batch of transactions and shinks it. Usually
+batch is applied when object is destroyed, but in some cases it's too late.
+
+=cut
+
+sub ApplyTransactionBatch {
+ my $self = shift;
+
+ my $batch = $self->TransactionBatch;
+ return unless $batch && @$batch;
+
+ $self->_ApplyTransactionBatch;
+
+ $self->{_TransactionBatch} = [];
+}
+
+sub _ApplyTransactionBatch {
+ my $self = shift;
+
+ return if $self->RanTransactionBatch;
+ $self->RanTransactionBatch(1);
+
+ my $still_exists = RT::Ticket->new( RT->SystemUser );
+ $still_exists->Load( $self->Id );
+ if (not $still_exists->Id) {
+ # The ticket has been removed from the database, but we still
+ # have pending TransactionBatch txns for it. Unfortunately,
+ # because it isn't in the DB anymore, attempting to run scrips
+ # on it may produce unpredictable results; simply drop the
+ # batched transactions.
+ $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips! Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
+ return;
+ }
+
+ my $batch = $self->TransactionBatch;
+
+ my %seen;
+ my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
+
+ require RT::Scrips;
+ RT::Scrips->new(RT->SystemUser)->Apply(
+ Stage => 'TransactionBatch',
+ TicketObj => $self,
+ TransactionObj => $batch->[0],
+ Type => $types,
+ );
+
+ # Entry point of the rule system
+ my $rules = RT::Ruleset->FindAllRules(
+ Stage => 'TransactionBatch',
+ TicketObj => $self,
+ TransactionObj => $batch->[0],
+ Type => $types,
+ );
+ RT::Ruleset->CommitRules($rules);
+}
+
+sub DESTROY {
+ my $self = shift;
+
+ # DESTROY methods need to localize $@, or it may unset it. This
+ # causes $m->abort to not bubble all of the way up. See perlbug
+ # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
+ local $@;
+
+ # The following line eliminates reentrancy.
+ # It protects against the fact that perl doesn't deal gracefully
+ # when an object's refcount is changed in its destructor.
+ return if $self->{_Destroyed}++;
+
+ if (in_global_destruction()) {
+ unless ($ENV{'HARNESS_ACTIVE'}) {
+ warn "Too late to safely run transaction-batch scrips!"
+ ." This is typically caused by using ticket objects"
+ ." at the top-level of a script which uses the RT API."
+ ." Be sure to explicitly undef such ticket objects,"
+ ." or put them inside of a lexical scope.";
+ }
+ return;
+ }
+
+ return $self->ApplyTransactionBatch;
+}
+
+
+
+
+sub _OverlayAccessible {
+ {
+ 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 },
+ 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 {
+ 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.
+ return ( 0, $msg ) unless $ret;
+ }
+
+ if ( $args{'RecordTransaction'} == 1 ) {
+
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => $args{'TransactionType'},
+ Field => $args{'Field'},
+ NewValue => $args{'Value'},
+ OldValue => $Old,
+ TimeTaken => $args{'TimeTaken'},
+ );
+ # Ensure that we can read the transaction, even if the change
+ # just made the ticket unreadable to us
+ $TransObj->{ _object_is_readable } = 1;
+ return ( $Trans, scalar $TransObj->BriefDescription );
+ }
+ else {
+ return ( $ret, $msg );
+ }
+}
+
+
+
+=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");
+ 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) );
+
+}
+
+
+
+=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);
+}
+
+
+
+
+
+=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->CurrentUser->PrincipalObj->HasRight(
+ Object => $self,
+ Right => $right,
+ )
+}
+
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the ticket, using ShowTicket
+
+=cut
+
+sub CurrentUserCanSee {
+ my $self = shift;
+ return $self->CurrentUserHasRight('ShowTicket');
+}
+
+=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'} ) ) )
+ {
+ Carp::cluck("Principal attrib undefined for Ticket::HasRight");
+ $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
+ return(undef);
+ }
+
+ return (
+ $args{'Principal'}->HasRight(
+ Object => $self,
+ Right => $args{'Right'}
+ )
+ );
+}
+
+
+
+=head2 Reminders
+
+Return the Reminders object for this ticket. (It's an RT::Reminders object.)
+It isn't acutally a searchbuilder collection itself.
+
+=cut
+
+sub Reminders {
+ my $self = shift;
+
+ unless ($self->{'__reminders'}) {
+ $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
+ $self->{'__reminders'}->Ticket($self->id);
+ }
+ return $self->{'__reminders'};
+
+}
+
+
+
+
+=head2 Transactions
+
+ Returns an RT::Transactions object of all transactions on this ticket
+
+=cut
+
+sub Transactions {
+ my $self = shift;
+
+ my $transactions = RT::Transactions->new( $self->CurrentUser );
+
+ #If the user has no rights, return an empty object
+ if ( $self->CurrentUserHasRight('ShowTicket') ) {
+ $transactions->LimitToTicket($self->id);
+
+ # if the user may not see comments do not return them
+ unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
+ $transactions->Limit(
+ SUBCLAUSE => 'acl',
+ FIELD => 'Type',
+ OPERATOR => '!=',
+ VALUE => "Comment"
+ );
+ $transactions->Limit(
+ SUBCLAUSE => 'acl',
+ FIELD => 'Type',
+ OPERATOR => '!=',
+ VALUE => "CommentEmailRecord",
+ ENTRYAGGREGATOR => 'AND'
+ );
+
+ }
+ } else {
+ $transactions->Limit(
+ SUBCLAUSE => 'acl',
+ FIELD => 'id',
+ VALUE => 0,
+ ENTRYAGGREGATOR => 'AND'
+ );
+ }
+
+ return ($transactions);
+}
+
+
+
+
+=head2 TransactionCustomFields
+
+ Returns the custom fields that transactions on tickets will have.
+
+=cut
+
+sub TransactionCustomFields {
+ my $self = shift;
+ my $cfs = $self->QueueObj->TicketTransactionCustomFields;
+ $cfs->SetContextObject( $self );
+ return $cfs;
+}
+
+
+=head2 LoadCustomFieldByIdentifier
+
+Finds and returns the custom field of the given name for the ticket,
+overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
+queue-specific CFs before global ones.
+
+=cut
+
+sub LoadCustomFieldByIdentifier {
+ my $self = shift;
+ my $field = shift;
+
+ return $self->SUPER::LoadCustomFieldByIdentifier($field)
+ if ref $field or $field =~ /^\d+$/;
+
+ my $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->SetContextObject( $self );
+ $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
+ $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
+ return $cf;
+}
+
+
+=head2 CustomFieldLookupType
+
+Returns the RT::Ticket lookup type, which can be passed to
+RT::CustomField->Create() via the 'LookupType' hash key.
+
+=cut
+
+
+sub CustomFieldLookupType {
+ "RT::Queue-RT::Ticket";
+}
+
+=head2 ACLEquivalenceObjects
+
+This method returns a list of objects for which a user's rights also apply
+to this ticket. Generally, this is only the ticket's queue, but some RT
+extensions may make other objects available too.
+
+This method is called from L<RT::Principal/HasRight>.
+
+=cut
+
+sub ACLEquivalenceObjects {
+ my $self = shift;
+ return $self->QueueObj;
+
+}
+
+
+1;
+
+=head1 AUTHOR
+
+Jesse Vincent, jesse@bestpractical.com
+
+=head1 SEE ALSO
+
+RT
+
+=cut
+
+
+use RT::Queue;
+use base 'RT::Record';
+
+sub Table {'Tickets'}
+
+
+
+
+
+
+=head2 id
+
+Returns the current value of id.
+(In the database, id is stored as int(11).)
+
+
+=cut
+
+
+=head2 EffectiveId
+
+Returns the current value of EffectiveId.
+(In the database, EffectiveId is stored as int(11).)
+
+
+
+=head2 SetEffectiveId VALUE
+
+
+Set EffectiveId to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, EffectiveId will be stored as a int(11).)
+
+
+=cut
+
+
+=head2 Queue
+
+Returns the current value of Queue.
+(In the database, Queue is stored as int(11).)
+
+
+
+=head2 SetQueue VALUE
+
+
+Set Queue to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Queue will be stored as a int(11).)
+
+
+=cut
+
+
+=head2 Type
+
+Returns the current value of Type.
+(In the database, Type is stored as varchar(16).)
+
+
+
+=head2 SetType VALUE