#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
=head1 DESCRIPTION
-This module lets you manipulate RT\'s ticket object.
+This module lets you manipulate RT's ticket object.
=head1 METHODS
AdminCc - A reference to a list of email addresses or Names
SquelchMailTo - A reference to a list of email addresses -
who should this ticket not mail
- Type -- The ticket\'s type. ignore this for now
- Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
+ Type -- The ticket's type. ignore this for now
+ Owner -- This ticket's owner. either an RT::User object or this user's id
Subject -- A string describing the subject of the ticket
Priority -- an integer from 0 to 99
InitialPriority -- an integer from 0 to 99
FinalPriority -- an integer from 0 to 99
- Status -- any valid status (Defined in RT::Queue)
+ Status -- any valid status for Queue's Lifecycle, otherwises uses on_create from Lifecycle default
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
+ 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>
Starts => undef,
Started => undef,
Resolved => undef,
+ WillResolve => undef,
MIMEObj => undef,
_RecordTransaction => 1,
DryRun => 0,
$args{'Status'} = $cycle->DefaultOnCreate;
}
+ $args{'Status'} = lc $args{'Status'};
unless ( $cycle->IsValid( $args{'Status'} ) ) {
return ( 0, 0,
$self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
$Started->Set( Format => 'ISO', Value => $args{'Started'} );
}
+ my $WillResolve = RT::Date->new($self->CurrentUser );
+ if ( defined $args{'WillResolve'} ) {
+ $WillResolve->Set( Format => 'ISO', Value => $args{'WillResolve'} );
+ }
+
# If the status is not an initial status, set the started date
elsif ( !$cycle->IsInitial($args{'Status'}) ) {
$Started->SetToNow;
}
}
+ $args{'Type'} = lc $args{'Type'}
+ if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;
+
+ $args{'Subject'} =~ s/\n//g;
+
$RT::Handle->BeginTransaction();
my %params = (
Starts => $Starts->ISO,
Started => $Started->ISO,
Resolved => $Resolved->ISO,
+ WillResolve => $WillResolve->ISO,
Due => $Due->ISO
);
}
}
- if ( $obj && $obj->Status eq 'deleted' ) {
+ if ( $obj && lc $obj->Status eq 'deleted' ) {
push @non_fatal_errors,
$self->loc("Linking. Can't link to a deleted ticket");
next;
}
}
+sub SetType {
+ my $self = shift;
+ my $value = shift;
+
+ # Force lowercase on internal RT types
+ $value = lc $value
+ if $value =~ /^(ticket|approval|reminder)$/i;
+ return $self->_Set(Field => 'Type', Value => $value, @_);
+}
}
$args{$date} = $dateobj->ISO;
}
- $args{'mimeobj'} = MIME::Entity->new();
- $args{'mimeobj'}->build(
- Type => ( $args{'contenttype'} || 'text/plain' ),
- Data => ($args{'content'} || '')
+ $args{'mimeobj'} = MIME::Entity->build(
+ Type => ( $args{'contenttype'} || 'text/plain' ),
+ Charset => "UTF-8",
+ Data => Encode::encode("UTF-8", ($args{'content'} || ''))
);
return (%args);
=head2 Import PARAMHASH
Import a ticket.
-Doesn\'t create a transaction.
-Doesn\'t supply queue defaults, etc.
+Doesn't create a transaction.
+Doesn't supply queue defaults, etc.
Returns: TICKETID
$QueueObj = RT::Queue->new(RT->SystemUser);
$QueueObj->Load( $args{'Queue'} );
- #TODO error check this and return 0 if it\'s not loading properly +++
+ #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);
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 PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
+If the watcher you're trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
=cut
return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
unless $addr;
- if ( lc $self->CurrentUser->UserObj->EmailAddress
+ if ( lc $self->CurrentUser->EmailAddress
eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
{
$args{'PrincipalId'} = $self->CurrentUser->id;
if ( $group->HasMember( $principal)) {
- return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
+ return ( 0, $self->loc('[_1] is already a [_2] for this ticket',
+ $principal->Object->Name, $self->loc($args{'Type'})) );
}
unless ($m_id) {
$RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
- return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
+ return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
+ $principal->Object->Name, $self->loc($args{'Type'})) );
}
unless ( $args{'Silent'} ) {
);
}
- return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
+ return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
+ $principal->Object->Name, $self->loc($args{'Type'})) );
}
}
}
else {
- $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
+ $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
return ( 0,
$self->loc('Error in parameters to Ticket->DeleteWatcher') );
}
unless ( $group->HasMember($principal) ) {
return ( 0,
- $self->loc( 'That principal is not a [_1] for this ticket',
- $args{'Type'} ) );
+ $self->loc( '[_1] is not a [_2] for this ticket',
+ $principal->Object->Name, $args{'Type'} ) );
}
my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
return (0,
$self->loc(
- 'Could not remove that principal as a [_1] for this ticket',
- $args{'Type'} ) );
+ 'Could not remove [_1] as a [_2] for this ticket',
+ $principal->Object->Name, $args{'Type'} ) );
}
unless ( $args{'Silent'} ) {
=head2 RequestorAddresses
- B<Returns> String: All Ticket Requestor email addresses as a string.
+B<Returns> String: All Ticket Requestor email addresses as a string.
=cut
unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
}
- $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
+ $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status };
return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
unless $new_status;
}
return ($self->{_queue_obj});
}
+sub SetSubject {
+ my $self = shift;
+ my $value = shift;
+ $value =~ s/\n//g;
+ return $self->_Set( Field => 'Subject', Value => $value );
+}
+
=head2 SubjectTag
Takes nothing. Returns SubjectTag for this ticket. Includes
return $next;
}
+=head2 FirstInactiveStatus
+
+Returns the first inactive status that the ticket could transition to,
+according to its current Queue's lifecycle. May return undef if there
+is no such possible status to transition to, or we are already in it.
+This is used in resolve action in UnsafeEmailCommands, for instance.
+
+=cut
+
+sub FirstInactiveStatus {
+ my $self = shift;
+
+ my $lifecycle = $self->QueueObj->Lifecycle;
+ my $status = $self->Status;
+ my @inactive = $lifecycle->Inactive;
+ # no change if no inactive statuses in the lifecycle
+ return undef unless @inactive;
+
+ # no change if the ticket is already has first status from the list of inactive
+ return undef if lc $status eq lc $inactive[0];
+
+ my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
+ return $next;
+}
+
=head2 SetStarted
Takes a date in ISO format or undef
}
$args{'NoteType'} = 'Comment';
+ $RT::Handle->BeginTransaction();
if ($args{'DryRun'}) {
- $RT::Handle->BeginTransaction();
$args{'CommitScrips'} = 0;
}
my @results = $self->_RecordNote(%args);
if ($args{'DryRun'}) {
$RT::Handle->Rollback();
+ } else {
+ $RT::Handle->Commit();
}
return(@results);
or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
return ( 0, $self->loc("Permission Denied"), undef );
}
+ $args{'NoteType'} = 'Correspond';
- $args{'NoteType'} = 'Correspond';
+ $RT::Handle->BeginTransaction();
if ($args{'DryRun'}) {
- $RT::Handle->BeginTransaction();
$args{'CommitScrips'} = 0;
}
my @results = $self->_RecordNote(%args);
+ unless ( $results[0] ) {
+ $RT::Handle->Rollback();
+ return @results;
+ }
+
#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 ( $self->IsRequestor($self->CurrentUser->id) ) {
if ($args{'DryRun'}) {
$RT::Handle->Rollback();
+ } else {
+ $RT::Handle->Commit();
}
return (@results);
}
unless ( $args{'MIMEObj'} ) {
+ my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
$args{'MIMEObj'} = MIME::Entity->build(
- Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
+ Type => "text/plain",
+ Charset => "UTF-8",
+ Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
);
}
+ $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
+ unless $args{'MIMEObj'}->head->get('X-RT-Interface');
+
# convert text parts into utf-8
RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
my $addresses = join ', ', (
map { RT::User->CanonicalizeEmailAddress( $_->address ) }
Email::Address->parse( $args{ $type . 'MessageTo' } ) );
- $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
+ $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
}
}
foreach my $argument (qw(Encrypt Sign)) {
$args{'MIMEObj'}->head->replace(
- "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
+ "X-RT-$argument" => Encode::encode( "UTF-8", $args{ $argument } )
) if defined $args{ $argument };
}
# internal Message-ID now, so all emails sent because of this
# message have a common Message-ID
my $org = RT->Config->Get('Organization');
- my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
+ my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
$args{'MIMEObj'}->head->set(
- 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
+ 'RT-Message-ID' => Encode::encode( "UTF-8",
+ RT::Interface::Email::GenMessageId( Ticket => $self )
+ )
);
}
#Record the correspondence (write the transaction)
my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
Type => $args{'NoteType'},
- Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
+ Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
TimeTaken => $args{'TimeTaken'},
MIMEObj => $args{'MIMEObj'},
CommitScrips => $args{'CommitScrips'},
}
my $Message = MIME::Entity->build(
+ Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
Type => 'text/plain',
- Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
Charset => 'UTF-8',
- Data => $args{'UpdateContent'} || "",
+ Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
);
my ( $Transaction, $Description, $Object ) = $self->$action(
my $self = shift;
my %args = @_;
my $Message = MIME::Entity->build(
- Type => 'text/plain',
- Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
+ Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
(defined $args{'Cc'} ?
- ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
+ ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
+ Type => 'text/plain',
Charset => 'UTF-8',
- Data => $args{'Content'} || "",
+ Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
);
my ( $Transaction, $Object, $Description ) = $self->Create(
# at least to myself
$links->Limit(
FIELD => $field, #$limit_on,
- OPERATOR => 'LIKE',
+ OPERATOR => 'MATCHES',
VALUE => 'fsck.com-rt://%/ticket/'. $self->id,
ENTRYAGGREGATOR => 'OR',
);
$links->Limit(
FIELD => $field, #$limit_on,
- OPERATOR => 'LIKE',
+ OPERATOR => 'MATCHES',
VALUE => 'fsck.com-rt://%/ticket/'. $_,
ENTRYAGGREGATOR => 'OR',
) foreach $self->Merged;
Delete a link. takes a paramhash of Base, Target, Type, Silent,
SilentBase and SilentTarget. Either Base or Target must be null.
-The null value will be replaced with this ticket\'s id.
+The null value will be replaced with this ticket's id.
If Silent is true then no transaction would be recorded, in other
case you can control creation of transactions on both base and
}
return ( 0, "Can't link to a deleted ticket" )
- if $other_ticket && $other_ticket->Status eq 'deleted';
+ if $other_ticket && lc $other_ticket->Status eq 'deleted';
return $self->_AddLink(%args);
}
# 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 );
- $uri_obj->FromURI( $args{'URI'} );
-
- unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
+ 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 );
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.
+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.
my $lifecycle = $self->QueueObj->Lifecycle;
- my $new = $args{'Status'};
+ 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)));
}
#Actually update the status
my ($val, $msg)= $self->_Set(
Field => 'Status',
- Value => $args{Status},
+ Value => $new,
TimeTaken => 0,
CheckACL => 0,
TransactionType => 'Status',
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
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;
return;
}
- my $batch = $self->TransactionBatch;
- return unless $batch && @$batch;
-
- return $self->_ApplyTransactionBatch;
+ return $self->ApplyTransactionBatch;
}
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 {
}
+=head2 LoadCustomFieldByIdentifier
-=head2 CustomFieldValues
-
-# Do name => id mapping (if needed) before falling back to
-# RT::Record's CustomFieldValues
-
-See L<RT::Record>
+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 CustomFieldValues {
+sub LoadCustomFieldByIdentifier {
my $self = shift;
my $field = shift;
- return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
+ 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 );
- unless ( $cf->id ) {
- $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
- }
-
- # If we didn't find a valid cfid, give up.
- return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
-
- return $self->SUPER::CustomFieldValues( $cf->id );
+ $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
+ return $cf;
}
-
=head2 CustomFieldLookupType
Returns the RT::Ticket lookup type, which can be passed to