+=cut
+
+sub ApplyQuoteWrap {
+ my $self = shift;
+ my %args = @_;
+ my $content = $args{content};
+
+ # What's the longest line like?
+ my $max = 0;
+ foreach ( split ( /\n/, $args{content} ) ) {
+ $max = length if length > $max;
+ }
+
+ if ( $max > 76 ) {
+ require Text::Quoted;
+ require Text::Wrapper;
+
+ my $structure = Text::Quoted::extract($args{content});
+ $content = $self->QuoteWrap(content_ref => $structure,
+ cols => $args{cols},
+ max => $max );
+ }
+
+ $content =~ s/^/> /gm; # use regex since string might be multi-line
+ return $content;
+}
+
+=head2 QuoteWrap PARAMHASH
+
+Wrap the contents of transactions based on Wrap settings, maintaining
+the quote character from the original.
+
+=cut
+
+sub QuoteWrap {
+ my $self = shift;
+ my %args = @_;
+ my $ref = $args{content_ref};
+ my $final_string;
+
+ if ( ref $ref eq 'ARRAY' ){
+ foreach my $array (@$ref){
+ $final_string .= $self->QuoteWrap(content_ref => $array,
+ cols => $args{cols},
+ max => $args{max} );
+ }
+ }
+ elsif ( ref $ref eq 'HASH' ){
+ return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
+
+ my $col = $args{cols} - (length $ref->{quoter});
+ my $wrapper = Text::Wrapper->new( columns => $col );
+
+ # Wrap on individual lines to honor incoming line breaks
+ # Otherwise deliberate separate lines (like a list or a sig)
+ # all get combined incorrectly into single paragraphs.
+
+ my @lines = split /\n/, $ref->{text};
+ my $wrap = join '', map { $wrapper->wrap($_) } @lines;
+ my $quoter = $ref->{quoter};
+
+ # Only add the space if actually quoting
+ $quoter .= ' ' if length $quoter;
+ $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line
+
+ return $wrap;
+ }
+ else{
+ $RT::Logger->warning("Can't apply quoting with $ref");
+ return;
+ }
+ return $final_string;
+}
+
+
+=head2 Addresses
+
+Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
+
+=cut
+
+sub Addresses {
+ my $self = shift;
+
+ if (my $attach = $self->Attachments->First) {
+ return $attach->Addresses;
+ }
+ else {
+ return {};
+ }
+
+}
+
+
+
+=head2 ContentObj
+
+Returns the RT::Attachment object which contains the content for this Transaction
+
+=cut
+
+
+sub ContentObj {
+ my $self = shift;
+ my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
+
+ # If we don't have any content, return undef now.
+ # Get the set of toplevel attachments to this transaction.
+
+ my $Attachment = $args{'Attachment'};
+
+ $Attachment ||= $self->Attachments->First;
+
+ return undef unless ($Attachment);
+
+ my $Attachments = $self->Attachments;
+ while ( my $Attachment = $Attachments->Next ) {
+ if ( my $content = _FindPreferredContentObj( %args, Attachment => $Attachment ) ) {
+ return $content;
+ }
+ }
+
+ # If that fails, return the first top-level textual part which has some content.
+ # We probably really want this to become "recurse, looking for the other type of
+ # displayable". For now, this maintains backcompat
+ my $all_parts = $self->Attachments;
+ while ( my $part = $all_parts->Next ) {
+ next unless _IsDisplayableTextualContentType($part->ContentType)
+ && $part->Content;
+ return $part;
+ }
+
+ return;
+}
+
+
+sub _FindPreferredContentObj {
+ my %args = @_;
+ my $Attachment = $args{Attachment};
+
+ # If we don't have any content, return undef now.
+ return undef unless $Attachment;
+
+ # If it's a textual part, just return the body.
+ if ( _IsDisplayableTextualContentType($Attachment->ContentType) ) {
+ return ($Attachment);
+ }
+
+ # If it's a multipart object, first try returning the first part with preferred
+ # MIME type ('text/plain' by default).
+
+ elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
+ my $kids = $Attachment->Children;
+ while (my $child = $kids->Next) {
+ my $ret = _FindPreferredContentObj(%args, Attachment => $child);
+ return $ret if ($ret);
+ }
+ }
+ elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
+ if ( $args{Type} ) {
+ my $plain_parts = $Attachment->Children;
+ $plain_parts->ContentType( VALUE => $args{Type} );
+ $plain_parts->LimitNotEmpty;
+
+ # If we actully found a part, return its content
+ if ( my $first = $plain_parts->First ) {
+ return $first;
+ }
+ } else {
+ my $parts = $Attachment->Children;
+ $parts->LimitNotEmpty;
+
+ # If we actully found a part, return its content
+ while (my $part = $parts->Next) {
+ next unless _IsDisplayableTextualContentType($part->ContentType);
+ return $part;
+ }
+
+ }
+ }
+
+ # If this is a message/rfc822 mail, we need to dig into it in order to find
+ # the actual textual content
+
+ elsif ( $Attachment->ContentType =~ '^message/rfc822' ) {
+ my $children = $Attachment->Children;
+ while ( my $child = $children->Next ) {
+ if ( my $content = _FindPreferredContentObj( %args, Attachment => $child ) ) {
+ return $content;
+ }
+ }
+ }
+
+ # We found no content. suck
+ return (undef);
+}
+
+=head2 _IsDisplayableTextualContentType
+
+We may need to pull this out to another module later, but for now, this
+is better than RT::I18N::IsTextualContentType because that believes that
+a message/rfc822 email is displayable, despite it having no content
+
+=cut
+
+sub _IsDisplayableTextualContentType {
+ my $type = shift;
+ ($type =~ m{^text/(?:plain|html)\b}i) ? 1 : 0;
+}
+
+
+=head2 Subject
+
+If this transaction has attached mime objects, returns the first one's subject
+Otherwise, returns null
+
+=cut
+
+sub Subject {
+ my $self = shift;
+ return undef unless my $first = $self->Attachments->First;
+ return $first->Subject;
+}
+
+
+
+=head2 Attachments
+
+Returns all the RT::Attachment objects which are attached
+to this transaction. Takes an optional parameter, which is
+a ContentType that Attachments should be restricted to.
+
+=cut
+
+sub Attachments {
+ my $self = shift;
+
+ if ( $self->{'attachments'} ) {
+ $self->{'attachments'}->GotoFirstItem;
+ return $self->{'attachments'};
+ }
+
+ $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
+
+ unless ( $self->CurrentUserCanSee ) {
+ $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
+ return $self->{'attachments'};
+ }
+
+ $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
+
+ # Get the self->{'attachments'} in the order they're put into
+ # the database. Arguably, we should be returning a tree
+ # of self->{'attachments'}, not a set...but no current app seems to need
+ # it.
+
+ $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
+
+ return $self->{'attachments'};
+}
+
+
+
+=head2 _Attach
+
+A private method used to attach a mime object to this transaction.
+
+=cut
+
+sub _Attach {
+ my $self = shift;
+ my $MIMEObject = shift;
+
+ unless ( defined $MIMEObject ) {
+ $RT::Logger->error("We can't attach a mime object if you don't give us one.");
+ return ( 0, $self->loc("[_1]: no attachment specified", $self) );
+ }
+
+ my $Attachment = RT::Attachment->new( $self->CurrentUser );
+ my ($id, $msg) = $Attachment->Create(
+ TransactionId => $self->Id,
+ Attachment => $MIMEObject
+ );
+ return ( $Attachment, $msg || $self->loc("Attachment created") );
+}
+
+
+
+sub ContentAsMIME {
+ my $self = shift;
+
+ # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
+ # since it has less information available without looking to it's parent
+ # transaction. Check ACLs here before we go any further.
+ return unless $self->CurrentUserCanSee;
+
+ my $attachments = RT::Attachments->new( $self->CurrentUser );
+ $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
+ $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
+ $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
+ $attachments->RowsPerPage(1);
+
+ my $top = $attachments->First;
+ return unless $top;
+
+ my $entity = MIME::Entity->build(
+ Type => 'message/rfc822',
+ Description => 'transaction ' . $self->id,
+ Data => $top->ContentAsMIME(Children => 1)->as_string,
+ );
+
+ return $entity;
+}
+
+
+
+=head2 Description
+
+Returns a text string which describes this transaction
+
+=cut
+
+sub Description {
+ my $self = shift;
+
+ unless ( $self->CurrentUserCanSee ) {
+ return ( $self->loc("Permission Denied") );
+ }
+
+ unless ( defined $self->Type ) {
+ return ( $self->loc("No transaction type specified"));
+ }
+
+ return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
+}
+
+
+
+=head2 BriefDescription
+
+Returns a text string which briefly describes this transaction
+
+=cut
+
+{
+ my $scrubber = HTML::Scrubber->new(default => 0); # deny everything
+
+ sub BriefDescription {
+ my $self = shift;
+ my $desc = $self->BriefDescriptionAsHTML;
+ $desc = $scrubber->scrub($desc);
+ $desc = HTML::Entities::decode_entities($desc);
+ return $desc;
+ }
+}
+
+=head2 BriefDescriptionAsHTML
+
+Returns an HTML string which briefly describes this transaction.
+
+=cut
+
+sub BriefDescriptionAsHTML {
+ my $self = shift;
+
+ unless ( $self->CurrentUserCanSee ) {
+ return ( $self->loc("Permission Denied") );
+ }
+
+ my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field);
+
+ unless ( defined $type ) {
+ return $self->loc("No transaction type specified");
+ }
+
+ my ($template, @params);
+
+ my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
+ ( $field
+ ? ("$objecttype-$type-$field", "$type-$field")
+ : () ),
+ "$objecttype-$type", $type;
+
+ if (@code) {
+ ($template, @params) = $code[0]->($self);
+ }
+
+ unless ($template) {
+ ($template, @params) = (
+ "Default: [_1]/[_2] changed from [_3] to [_4]", #loc
+ $type,
+ $field,
+ (
+ $self->OldValue
+ ? "'" . $self->OldValue . "'"
+ : $self->loc("(no value)")
+ ),
+ (
+ $self->NewValue
+ ? "'" . $self->NewValue . "'"
+ : $self->loc("(no value)")
+ ),
+ );
+ }
+ return $self->loc($template, $self->_ProcessReturnValues(@params));
+}
+
+sub _ProcessReturnValues {
+ my $self = shift;
+ my @values = @_;
+ return map {
+ if (ref eq 'ARRAY') { $_ = join "", $self->_ProcessReturnValues(@$_) }
+ elsif (ref eq 'SCALAR') { $_ = $$_ }
+ else { RT::Interface::Web::EscapeHTML(\$_) }
+ $_
+ } @values;
+}
+
+sub _FormatPrincipal {
+ my $self = shift;
+ my $principal = shift;
+ if ($principal->IsUser) {
+ return $self->_FormatUser( $principal->Object );
+ } else {
+ return $self->loc("group [_1]", $principal->Object->Name);
+ }
+}
+
+sub _FormatUser {
+ my $self = shift;
+ my $user = shift;
+ return [
+ \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
+ $user->Format,
+ \'</span>'
+ ];
+}
+
+%_BriefDescriptions = (
+ Create => sub {
+ my $self = shift;
+ return ( "[_1] created", $self->FriendlyObjectType ); #loc()
+ },
+ Enabled => sub {
+ my $self = shift;
+ return ( "[_1] enabled", $self->Field ? $self->loc($self->Field) : $self->FriendlyObjectType ); #loc()
+ },
+ Disabled => sub {
+ my $self = shift;
+ return ( "[_1] disabled", $self->Field ? $self->loc($self->Field) : $self->FriendlyObjectType ); #loc()
+ },
+ Status => sub {
+ my $self = shift;
+ if ( $self->Field eq 'Status' ) {
+ if ( $self->NewValue eq 'deleted' ) {
+ return ( "[_1] deleted", $self->FriendlyObjectType ); #loc()
+ }
+ else {
+ my $canon = $self->Object->DOES("RT::Record::Role::Status")
+ ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
+ : sub { return $_[0] };
+ return (
+ "Status changed from [_1] to [_2]",
+ "'" . $self->loc( $canon->($self->OldValue) ) . "'",
+ "'" . $self->loc( $canon->($self->NewValue) ) . "'"
+ ); # loc()
+ }
+ }
+
+ # Generic:
+ my $no_value = $self->loc("(no value)");
+ return (
+ "[_1] changed from [_2] to [_3]",
+ $self->Field,
+ ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
+ "'" . $self->NewValue . "'"
+ ); #loc()
+ },
+ SystemError => sub {
+ my $self = shift;
+ return $self->Data // ("System error"); #loc()
+ },
+ AttachmentTruncate => sub {
+ my $self = shift;
+ if ( defined $self->Data ) {
+ return ( "File '[_1]' truncated because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
+ $self->Data, $self->OldValue, $self->NewValue ); #loc()
+ }
+ else {
+ return ( "Content truncated because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
+ $self->OldValue, $self->NewValue ); #loc()
+ }
+ },
+ AttachmentDrop => sub {
+ my $self = shift;
+ if ( defined $self->Data ) {
+ return ( "File '[_1]' dropped because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
+ $self->Data, $self->OldValue, $self->NewValue ); #loc()
+ }
+ else {
+ return ( "Content dropped because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
+ $self->OldValue, $self->NewValue ); #loc()
+ }
+ },
+ AttachmentError => sub {
+ my $self = shift;
+ if ( defined $self->Data ) {
+ return ( "File '[_1]' insert failed. See error log for details.", $self->Data ); #loc()
+ }
+ else {
+ return ( "Content insert failed. See error log for details." ); #loc()
+ }
+ },
+ "Forward Transaction" => sub {
+ my $self = shift;
+ my $recipients = join ", ", map {
+ RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
+ } RT::EmailParser->ParseEmailAddress($self->Data);
+
+ return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]",
+ $self->Field, $recipients,
+ [\'<a href="#txn-', $self->Field, \'">'], \'</a>'); #loc()
+ },
+ "Forward Ticket" => sub {
+ my $self = shift;
+ my $recipients = join ", ", map {
+ RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
+ } RT::EmailParser->ParseEmailAddress($self->Data);
+
+ return ( "Forwarded Ticket to [_1]", $recipients ); #loc()
+ },
+ CommentEmailRecord => sub {
+ my $self = shift;
+ return ("Outgoing email about a comment recorded"); #loc()
+ },
+ EmailRecord => sub {
+ my $self = shift;
+ return ("Outgoing email recorded"); #loc()
+ },
+ Correspond => sub {
+ my $self = shift;
+ return ("Correspondence added"); #loc()
+ },
+ Comment => sub {
+ my $self = shift;
+ return ("Comments added"); #loc()
+ },
+ CustomField => sub {
+ my $self = shift;
+ my $field = $self->loc('CustomField');
+
+ my $cf;
+ if ( $self->Field ) {
+ $cf = RT::CustomField->new( $self->CurrentUser );
+ $cf->SetContextObject( $self->Object );
+ $cf->Load( $self->Field );
+ $field = $cf->Name();
+ $field = $self->loc('a custom field') if !defined($field);
+ }
+
+ my $new = $self->NewValue;
+ my $old = $self->OldValue;
+
+ if ( $cf ) {
+
+ if ( $cf->Type eq 'DateTime' ) {
+ if ($old) {
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set( Format => 'ISO', Value => $old );
+ $old = $date->AsString;
+ }
+
+ if ($new) {
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set( Format => 'ISO', Value => $new );
+ $new = $date->AsString;
+ }
+ }
+ elsif ( $cf->Type eq 'Date' ) {
+ if ($old) {
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set(
+ Format => 'unknown',
+ Value => $old,
+ Timezone => 'UTC',
+ );
+ $old = $date->AsString( Time => 0, Timezone => 'UTC' );
+ }
+
+ if ($new) {
+ my $date = RT::Date->new( $self->CurrentUser );
+ $date->Set(
+ Format => 'unknown',
+ Value => $new,
+ Timezone => 'UTC',
+ );
+ $new = $date->AsString( Time => 0, Timezone => 'UTC' );
+ }
+ }
+ }
+
+ if ( !defined($old) || $old eq '' ) {
+ return ("[_1] [_2] added", $field, $new); #loc()
+ }
+ elsif ( !defined($new) || $new eq '' ) {
+ return ("[_1] [_2] deleted", $field, $old); #loc()
+ }
+ else {
+ return ("[_1] [_2] changed to [_3]", $field, $old, $new); #loc()
+ }
+ },
+ Untake => sub {
+ my $self = shift;
+ return ("Untaken"); #loc()
+ },
+ Take => sub {
+ my $self = shift;
+ return ("Taken"); #loc()
+ },
+ Force => sub {
+ my $self = shift;
+ my $Old = RT::User->new( $self->CurrentUser );
+ $Old->Load( $self->OldValue );
+ my $New = RT::User->new( $self->CurrentUser );
+ $New->Load( $self->NewValue );
+
+ return ("Owner forcibly changed from [_1] to [_2]",
+ map { $self->_FormatUser($_) } $Old, $New); #loc()
+ },
+ Steal => sub {
+ my $self = shift;
+ my $Old = RT::User->new( $self->CurrentUser );
+ $Old->Load( $self->OldValue );
+ return ("Stolen from [_1]", $self->_FormatUser($Old)); #loc()
+ },
+ Give => sub {
+ my $self = shift;
+ my $New = RT::User->new( $self->CurrentUser );
+ $New->Load( $self->NewValue );
+ return ( "Given to [_1]", $self->_FormatUser($New)); #loc()
+ },
+ AddWatcher => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->NewValue);
+ return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
+ },
+ DelWatcher => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->OldValue);
+ return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
+ },
+ SetWatcher => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->NewValue);
+ return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
+ },
+ Subject => sub {
+ my $self = shift;
+ return ( "Subject changed to [_1]", $self->Data ); #loc()
+ },
+ AddLink => sub {
+ my $self = shift;
+ my $value;
+ if ( $self->NewValue ) {
+ my $URI = RT::URI->new( $self->CurrentUser );
+ if ( $URI->FromURI( $self->NewValue ) ) {
+ $value = [
+ \'<a href="', $URI->AsHREF, \'">',
+ $URI->AsString,
+ \'</a>'
+ ];
+ }
+ else {
+ $value = $self->NewValue;
+ }
+
+ if ( $self->Field eq 'DependsOn' ) {
+ return ( "Dependency on [_1] added", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'DependedOnBy' ) {
+ return ( "Dependency by [_1] added", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'RefersTo' ) {
+ return ( "Reference to [_1] added", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'ReferredToBy' ) {
+ return ( "Reference by [_1] added", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'MemberOf' ) {
+ return ( "Membership in [_1] added", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'HasMember' ) {
+ return ( "Member [_1] added", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'MergedInto' ) {
+ return ( "Merged into [_1]", $value ); #loc()
+ }
+ }
+ else {
+ return ( "[_1]", $self->Data ); #loc()
+ }
+ },
+ DeleteLink => sub {
+ my $self = shift;
+ my $value;
+ if ( $self->OldValue ) {
+ my $URI = RT::URI->new( $self->CurrentUser );
+ if ( $URI->FromURI( $self->OldValue ) ) {
+ $value = [
+ \'<a href="', $URI->AsHREF, \'">',
+ $URI->AsString,
+ \'</a>'
+ ];
+ }
+ else {
+ $value = $self->OldValue;
+ }
+
+ if ( $self->Field eq 'DependsOn' ) {
+ return ( "Dependency on [_1] deleted", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'DependedOnBy' ) {
+ return ( "Dependency by [_1] deleted", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'RefersTo' ) {
+ return ( "Reference to [_1] deleted", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'ReferredToBy' ) {
+ return ( "Reference by [_1] deleted", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'MemberOf' ) {
+ return ( "Membership in [_1] deleted", $value ); #loc()
+ }
+ elsif ( $self->Field eq 'HasMember' ) {
+ return ( "Member [_1] deleted", $value ); #loc()
+ }
+ }
+ else {
+ return ( "[_1]", $self->Data ); #loc()
+ }
+ },
+ Told => sub {
+ my $self = shift;
+ if ( $self->Field eq 'Told' ) {
+ my $t1 = RT::Date->new($self->CurrentUser);
+ $t1->Set(Format => 'ISO', Value => $self->NewValue);
+ my $t2 = RT::Date->new($self->CurrentUser);
+ $t2->Set(Format => 'ISO', Value => $self->OldValue);
+ return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc()
+ }
+ else {
+ return ( "[_1] changed from [_2] to [_3]",
+ $self->loc($self->Field),
+ ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" ); #loc()
+ }
+ },
+ Set => sub {
+ my $self = shift;
+ if ( $self->Field eq 'Password' ) {
+ return ('Password changed'); #loc()
+ }
+ elsif ( $self->Field eq 'Queue' ) {
+ my $q1 = RT::Queue->new( $self->CurrentUser );
+ $q1->Load( $self->OldValue );
+ my $q2 = RT::Queue->new( $self->CurrentUser );
+ $q2->Load( $self->NewValue );
+ return ("[_1] changed from [_2] to [_3]",
+ $self->loc($self->Field), $q1->Name // '#'.$q1->id, $q2->Name // '#'.$q2->id); #loc()
+ }
+
+ # Write the date/time change at local time:
+ elsif ($self->Field =~ /^(?:Due|Starts|Started|Told|WillResolve)$/) {
+ my $t1 = RT::Date->new($self->CurrentUser);
+ $t1->Set(Format => 'ISO', Value => $self->NewValue);
+ my $t2 = RT::Date->new($self->CurrentUser);
+ $t2->Set(Format => 'ISO', Value => $self->OldValue);
+ return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc()
+ }
+ elsif ( $self->Field eq 'Owner' ) {
+ my $Old = RT::User->new( $self->CurrentUser );
+ $Old->Load( $self->OldValue );
+ my $New = RT::User->new( $self->CurrentUser );
+ $New->Load( $self->NewValue );
+
+ if ( $Old->id == RT->Nobody->id ) {
+ if ( $New->id == $self->Creator ) {
+ return ("Taken"); #loc()
+ }
+ else {
+ return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
+ }
+ }
+ else {
+ if ( $New->id == $self->Creator ) {
+ return ("Stolen from [_1]", $self->_FormatUser($Old) ); #loc()
+ }
+ elsif ( $Old->id == $self->Creator ) {
+ if ( $New->id == RT->Nobody->id ) {
+ return ("Untaken"); #loc()
+ }
+ else {
+ return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
+ }
+ }
+ else {
+ return (
+ "Owner forcibly changed from [_1] to [_2]",
+ map { $self->_FormatUser($_) } $Old, $New
+ ); #loc()
+ }
+ }
+ }
+ else {
+ return ( "[_1] changed from [_2] to [_3]",
+ $self->loc($self->Field),
+ ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
+ ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)"))); #loc()
+ }
+ },
+ "Set-TimeWorked" => sub {
+ my $self = shift;
+ my $old = $self->OldValue || 0;
+ my $new = $self->NewValue || 0;
+ my $duration = $new - $old;
+ if ($duration < 0) {
+ return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration); # loc()
+ }
+ elsif ($duration < 60) {
+ return ("Worked [quant,_1,minute,minutes]", $duration); # loc()
+ } else {
+ return ("Worked [quant,_1,hour,hours] ([quant,_2,minute,minutes])", sprintf("%.1f", $duration / 60), $duration); # loc()
+ }
+ },
+ PurgeTransaction => sub {
+ my $self = shift;
+ return ("Transaction [_1] purged", $self->Data); #loc()
+ },
+ AddReminder => sub {
+ my $self = shift;
+ my $ticket = RT::Ticket->new($self->CurrentUser);
+ $ticket->Load($self->NewValue);
+ if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
+ my $subject = [
+ \'<a href="', RT->Config->Get('WebPath'),
+ "/Ticket/Reminders.html?id=", $self->ObjectId,
+ "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
+ ];
+ return ("Reminder '[_1]' added", $subject); #loc()
+ } else {
+ return ("Reminder added"); #loc()
+ }
+ },
+ OpenReminder => sub {
+ my $self = shift;
+ my $ticket = RT::Ticket->new($self->CurrentUser);
+ $ticket->Load($self->NewValue);
+ if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
+ my $subject = [
+ \'<a href="', RT->Config->Get('WebPath'),
+ "/Ticket/Reminders.html?id=", $self->ObjectId,
+ "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
+ ];
+ return ("Reminder '[_1]' reopened", $subject); #loc()
+ } else {
+ return ("Reminder reopened"); #loc()
+ }
+ },
+ ResolveReminder => sub {
+ my $self = shift;
+ my $ticket = RT::Ticket->new($self->CurrentUser);
+ $ticket->Load($self->NewValue);
+ if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
+ my $subject = [
+ \'<a href="', RT->Config->Get('WebPath'),
+ "/Ticket/Reminders.html?id=", $self->ObjectId,
+ "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
+ ];
+ return ("Reminder '[_1]' completed", $subject); #loc()
+ } else {
+ return ("Reminder completed"); #loc()
+ }
+ },
+ AddMember => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->Field);
+
+ if ($principal->IsUser) {
+ return ("Added user '[_1]'", $principal->Object->Name); #loc()
+ }
+ else {
+ return ("Added group '[_1]'", $principal->Object->Name); #loc()
+ }
+ },
+ DeleteMember => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->Field);
+
+ if ($principal->IsUser) {
+ return ("Removed user '[_1]'", $principal->Object->Name); #loc()
+ }
+ else {
+ return ("Removed group '[_1]'", $principal->Object->Name); #loc()
+ }
+ },
+ AddMembership => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->Field);
+ return ("Added to group '[_1]'", $principal->Object->Name); #loc()
+ },
+ DeleteMembership => sub {
+ my $self = shift;
+ my $principal = RT::Principal->new($self->CurrentUser);
+ $principal->Load($self->Field);
+ return ("Removed from group '[_1]'", $principal->Object->Name); #loc()
+ },
+);
+
+
+
+
+=head2 IsInbound
+
+Returns true if the creator of the transaction is a requestor of the ticket.
+Returns false otherwise
+
+=cut
+
+sub IsInbound {
+ my $self = shift;
+ $self->ObjectType eq 'RT::Ticket' or return undef;
+ return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
+}