1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 RT::Transaction - RT's transaction object
61 Each RT::Transaction describes an atomic change to a ticket object
62 or an update to an RT::Ticket object.
63 It can have arbitrary MIME attachments.
72 package RT::Transaction;
74 use base 'RT::Record';
79 use vars qw( %_BriefDescriptions $PreferredContentType );
85 use HTML::FormatText::WithLinks::AndTables;
88 # For EscapeHTML() and decode_entities()
89 require RT::Interface::Web;
90 require HTML::Entities;
92 sub Table {'Transactions'}
98 Create a new transaction.
100 This routine should _never_ be called by anything other than RT::Ticket.
101 It should not be called
102 from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
103 Then the unpleasant stuff will start.
105 TODO: Document what gets passed to this
122 ObjectType => 'RT::Ticket',
124 ReferenceType => undef,
125 OldReference => undef,
126 NewReference => undef,
127 SquelchMailTo => undef,
132 $args{ObjectId} ||= $args{Ticket};
134 #if we didn't specify a ticket, we need to bail
135 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
136 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
139 #lets create our transaction
141 Type => $args{'Type'},
142 Data => $args{'Data'},
143 Field => $args{'Field'},
144 OldValue => $args{'OldValue'},
145 NewValue => $args{'NewValue'},
146 Created => $args{'Created'},
147 ObjectType => $args{'ObjectType'},
148 ObjectId => $args{'ObjectId'},
149 ReferenceType => $args{'ReferenceType'},
150 OldReference => $args{'OldReference'},
151 NewReference => $args{'NewReference'},
154 # Parameters passed in during an import that we probably don't want to touch, otherwise
155 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
156 $params{$attr} = $args{$attr} if ($args{$attr});
159 my $id = $self->SUPER::Create(%params);
161 if ( defined $args{'MIMEObj'} ) {
162 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
164 $RT::Logger->error("Couldn't add attachment: $msg");
165 return ( 0, $self->loc("Couldn't add attachment") );
169 # Set up any custom fields passed at creation. Has to happen
172 $self->UpdateCustomFields(%{ $args{'CustomFields'} });
175 Name => 'SquelchMailTo',
176 Content => RT::User->CanonicalizeEmailAddress($_)
177 ) for @{$args{'SquelchMailTo'} || []};
179 my @return = ( $id, $self->loc("Transaction Created") );
181 return @return unless $args{'ObjectType'} eq 'RT::Ticket';
183 # Provide a way to turn off scrips if we need to
184 unless ( $args{'ActivateScrips'} ) {
185 $RT::Logger->debug('Skipping scrips for transaction #' .$self->Id);
189 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
191 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
193 $self->{'scrips'}->Prepare(
194 Stage => 'TransactionCreate',
195 Type => $args{'Type'},
196 Ticket => $args{'ObjectId'},
197 Transaction => $self->id,
200 # Entry point of the rule system
201 my $ticket = RT::Ticket->new(RT->SystemUser);
202 $ticket->Load($args{'ObjectId'});
203 my $txn = RT::Transaction->new($RT::SystemUser);
204 $txn->Load($self->id);
206 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
207 Stage => 'TransactionCreate',
208 Type => $args{'Type'},
209 TicketObj => $ticket,
210 TransactionObj => $txn,
213 if ($args{'CommitScrips'} ) {
214 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
215 $self->{'scrips'}->Commit();
216 RT::Ruleset->CommitRules($rules);
225 Returns the Scrips object for this transaction.
226 This routine is only useful on a freshly created transaction object.
227 Scrips do not get persisted to the database with transactions.
235 return($self->{'scrips'});
241 Returns the array of Rule objects for this transaction.
242 This routine is only useful on a freshly created transaction object.
243 Rules do not get persisted to the database with transactions.
251 return($self->{'rules'});
258 Delete this transaction. Currently DOES NOT CHECK ACLS
266 $RT::Handle->BeginTransaction();
268 my $attachments = $self->Attachments;
270 while (my $attachment = $attachments->Next) {
271 my ($id, $msg) = $attachment->Delete();
273 $RT::Handle->Rollback();
274 return($id, $self->loc("System Error: [_1]", $msg));
277 my ($id,$msg) = $self->SUPER::Delete();
279 $RT::Handle->Rollback();
280 return($id, $self->loc("System Error: [_1]", $msg));
282 $RT::Handle->Commit();
291 Returns the L<RT::Attachments> object which contains the "top-level" object
292 attachment for this transaction.
299 # XXX: Where is ACL check?
301 unless ( defined $self->{'message'} ) {
303 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
304 $self->{'message'}->Limit(
305 FIELD => 'TransactionId',
308 $self->{'message'}->ChildrenOf(0);
310 $self->{'message'}->GotoFirstItem;
312 return $self->{'message'};
317 =head2 Content PARAMHASH
319 If this transaction has attached mime objects, returns the body of the first
320 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
323 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
324 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
326 If $args{'Type'} is set to C<text/html>, this will return an HTML
327 part of the message, if available. Otherwise it looks for a text/plain
328 part. If $args{'Type'} is missing, it defaults to the value of
329 C<$RT::Transaction::PreferredContentType>, if that's missing too,
337 Type => $PreferredContentType || '',
340 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
345 if ( my $content_obj =
346 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
348 $content = $content_obj->Content ||'';
350 if ( lc $content_obj->ContentType eq 'text/html' ) {
351 $content =~ s/(?:(<\/div>)|<p>|<br\s*\/?>|<div(\s+class="[^"]+")?>)\s*--\s+<br\s*\/?>.*?$/$1/s if $args{'Quote'};
353 if ($args{Type} ne 'text/html') {
354 $content = RT::Interface::Email::ConvertHTMLToText($content);
356 # Scrub out <html>, <head>, <meta>, and <body>, and
357 # leave all else untouched.
358 my $scrubber = HTML::Scrubber->new();
365 $scrubber->default( 1 => { '*' => 1 } );
366 $content = $scrubber->scrub( $content );
370 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
371 if ($args{Type} eq 'text/html') {
372 # Extremely simple text->html converter
373 $content =~ s/&/&/g;
374 $content =~ s/</</g;
375 $content =~ s/>/>/g;
376 $content = qq|<pre style="white-space: pre-wrap; font-family: monospace;">$content</pre>|;
381 # If all else fails, return a message that we couldn't find any content
383 $content = $self->loc('This transaction appears to have no content');
386 if ( $args{'Quote'} ) {
387 if ($args{Type} eq 'text/html') {
388 $content = '<div class="gmail_quote">'
390 . '<br /><blockquote class="gmail_quote" type="cite">'
392 . '</blockquote></div><br /><br />';
394 $content = $self->ApplyQuoteWrap(content => $content,
395 cols => $args{'Wrap'} );
397 $content = $self->QuoteHeader . "\n$content\n\n";
406 Returns text prepended to content when transaction is quoted
407 (see C<Quote> argument in L</Content>). By default returns
408 localized "On <date> <user name> wrote:\n".
414 return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
417 =head2 ApplyQuoteWrap PARAMHASH
419 Wrapper to calculate wrap criteria and apply quote wrapping if needed.
426 my $content = $args{content};
428 # What's the longest line like?
430 foreach ( split ( /\n/, $args{content} ) ) {
431 $max = length if length > $max;
435 require Text::Quoted;
436 require Text::Wrapper;
438 my $structure = Text::Quoted::extract($args{content});
439 $content = $self->QuoteWrap(content_ref => $structure,
444 $content =~ s/^/> /gm; # use regex since string might be multi-line
448 =head2 QuoteWrap PARAMHASH
450 Wrap the contents of transactions based on Wrap settings, maintaining
451 the quote character from the original.
458 my $ref = $args{content_ref};
461 if ( ref $ref eq 'ARRAY' ){
462 foreach my $array (@$ref){
463 $final_string .= $self->QuoteWrap(content_ref => $array,
468 elsif ( ref $ref eq 'HASH' ){
469 return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
471 my $col = $args{cols} - (length $ref->{quoter});
472 my $wrapper = Text::Wrapper->new( columns => $col );
474 # Wrap on individual lines to honor incoming line breaks
475 # Otherwise deliberate separate lines (like a list or a sig)
476 # all get combined incorrectly into single paragraphs.
478 my @lines = split /\n/, $ref->{text};
479 my $wrap = join '', map { $wrapper->wrap($_) } @lines;
480 my $quoter = $ref->{quoter};
482 # Only add the space if actually quoting
483 $quoter .= ' ' if length $quoter;
484 $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line
489 $RT::Logger->warning("Can't apply quoting with $ref");
492 return $final_string;
498 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
505 if (my $attach = $self->Attachments->First) {
506 return $attach->Addresses;
518 Returns the RT::Attachment object which contains the content for this Transaction
525 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
527 # If we don't have any content, return undef now.
528 # Get the set of toplevel attachments to this transaction.
530 my $Attachment = $args{'Attachment'};
532 $Attachment ||= $self->Attachments->First;
534 return undef unless ($Attachment);
536 my $Attachments = $self->Attachments;
537 while ( my $Attachment = $Attachments->Next ) {
538 if ( my $content = _FindPreferredContentObj( %args, Attachment => $Attachment ) ) {
543 # If that fails, return the first top-level textual part which has some content.
544 # We probably really want this to become "recurse, looking for the other type of
545 # displayable". For now, this maintains backcompat
546 my $all_parts = $self->Attachments;
547 while ( my $part = $all_parts->Next ) {
548 next unless _IsDisplayableTextualContentType($part->ContentType)
557 sub _FindPreferredContentObj {
559 my $Attachment = $args{Attachment};
561 # If we don't have any content, return undef now.
562 return undef unless $Attachment;
564 # If it's a textual part, just return the body.
565 if ( _IsDisplayableTextualContentType($Attachment->ContentType) ) {
566 return ($Attachment);
569 # If it's a multipart object, first try returning the first part with preferred
570 # MIME type ('text/plain' by default).
572 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
573 my $kids = $Attachment->Children;
574 while (my $child = $kids->Next) {
575 my $ret = _FindPreferredContentObj(%args, Attachment => $child);
576 return $ret if ($ret);
579 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
581 my $plain_parts = $Attachment->Children;
582 $plain_parts->ContentType( VALUE => $args{Type} );
583 $plain_parts->LimitNotEmpty;
585 # If we actully found a part, return its content
586 if ( my $first = $plain_parts->First ) {
590 my $parts = $Attachment->Children;
591 $parts->LimitNotEmpty;
593 # If we actully found a part, return its content
594 while (my $part = $parts->Next) {
595 next unless _IsDisplayableTextualContentType($part->ContentType);
602 # If this is a message/rfc822 mail, we need to dig into it in order to find
603 # the actual textual content
605 elsif ( $Attachment->ContentType =~ '^message/rfc822' ) {
606 my $children = $Attachment->Children;
607 while ( my $child = $children->Next ) {
608 if ( my $content = _FindPreferredContentObj( %args, Attachment => $child ) ) {
614 # We found no content. suck
618 =head2 _IsDisplayableTextualContentType
620 We may need to pull this out to another module later, but for now, this
621 is better than RT::I18N::IsTextualContentType because that believes that
622 a message/rfc822 email is displayable, despite it having no content
626 sub _IsDisplayableTextualContentType {
628 ($type =~ m{^text/(?:plain|html)\b}i) ? 1 : 0;
634 If this transaction has attached mime objects, returns the first one's subject
635 Otherwise, returns null
641 return undef unless my $first = $self->Attachments->First;
642 return $first->Subject;
649 Returns all the RT::Attachment objects which are attached
650 to this transaction. Takes an optional parameter, which is
651 a ContentType that Attachments should be restricted to.
658 if ( $self->{'attachments'} ) {
659 $self->{'attachments'}->GotoFirstItem;
660 return $self->{'attachments'};
663 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
665 unless ( $self->CurrentUserCanSee ) {
666 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
667 return $self->{'attachments'};
670 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
672 # Get the self->{'attachments'} in the order they're put into
673 # the database. Arguably, we should be returning a tree
674 # of self->{'attachments'}, not a set...but no current app seems to need
677 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
679 return $self->{'attachments'};
686 A private method used to attach a mime object to this transaction.
692 my $MIMEObject = shift;
694 unless ( defined $MIMEObject ) {
695 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
696 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
699 my $Attachment = RT::Attachment->new( $self->CurrentUser );
700 my ($id, $msg) = $Attachment->Create(
701 TransactionId => $self->Id,
702 Attachment => $MIMEObject
704 return ( $Attachment, $msg || $self->loc("Attachment created") );
712 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
713 # since it has less information available without looking to it's parent
714 # transaction. Check ACLs here before we go any further.
715 return unless $self->CurrentUserCanSee;
717 my $attachments = RT::Attachments->new( $self->CurrentUser );
718 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
719 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
720 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
721 $attachments->RowsPerPage(1);
723 my $top = $attachments->First;
726 my $entity = MIME::Entity->build(
727 Type => 'message/rfc822',
728 Description => 'transaction ' . $self->id,
729 Data => $top->ContentAsMIME(Children => 1)->as_string,
739 Returns a text string which describes this transaction
746 unless ( $self->CurrentUserCanSee ) {
747 return ( $self->loc("Permission Denied") );
750 unless ( defined $self->Type ) {
751 return ( $self->loc("No transaction type specified"));
754 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
759 =head2 BriefDescription
761 Returns a text string which briefly describes this transaction
766 my $scrubber = HTML::Scrubber->new(default => 0); # deny everything
768 sub BriefDescription {
770 my $desc = $self->BriefDescriptionAsHTML;
771 $desc = $scrubber->scrub($desc);
772 $desc = HTML::Entities::decode_entities($desc);
777 =head2 BriefDescriptionAsHTML
779 Returns an HTML string which briefly describes this transaction.
783 sub BriefDescriptionAsHTML {
786 unless ( $self->CurrentUserCanSee ) {
787 return ( $self->loc("Permission Denied") );
790 my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field);
792 unless ( defined $type ) {
793 return $self->loc("No transaction type specified");
796 my ($template, @params);
798 my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
800 ? ("$objecttype-$type-$field", "$type-$field")
802 "$objecttype-$type", $type;
805 ($template, @params) = $code[0]->($self);
809 ($template, @params) = (
810 "Default: [_1]/[_2] changed from [_3] to [_4]", #loc
815 ? "'" . $self->OldValue . "'"
816 : $self->loc("(no value)")
820 ? "'" . $self->NewValue . "'"
821 : $self->loc("(no value)")
825 return $self->loc($template, $self->_ProcessReturnValues(@params));
828 sub _ProcessReturnValues {
832 if (ref eq 'ARRAY') { $_ = join "", $self->_ProcessReturnValues(@$_) }
833 elsif (ref eq 'SCALAR') { $_ = $$_ }
834 else { RT::Interface::Web::EscapeHTML(\$_) }
839 sub _FormatPrincipal {
841 my $principal = shift;
842 if ($principal->IsUser) {
843 return $self->_FormatUser( $principal->Object );
845 return $self->loc("group [_1]", $principal->Object->Name);
853 \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
859 %_BriefDescriptions = (
862 return ( "[_1] created", $self->FriendlyObjectType ); #loc()
866 return ( "[_1] enabled", $self->FriendlyObjectType ); #loc()
870 return ( "[_1] disabled", $self->FriendlyObjectType ); #loc()
874 if ( $self->Field eq 'Status' ) {
875 if ( $self->NewValue eq 'deleted' ) {
876 return ( "[_1] deleted", $self->FriendlyObjectType ); #loc()
879 my $canon = $self->Object->DOES("RT::Record::Role::Status")
880 ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
881 : sub { return $_[0] };
883 "Status changed from [_1] to [_2]",
884 "'" . $self->loc( $canon->($self->OldValue) ) . "'",
885 "'" . $self->loc( $canon->($self->NewValue) ) . "'"
891 my $no_value = $self->loc("(no value)");
893 "[_1] changed from [_2] to [_3]",
895 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
896 "'" . $self->NewValue . "'"
901 return $self->Data // ("System error"); #loc()
903 AttachmentTruncate => sub {
905 if ( defined $self->Data ) {
906 return ( "File '[_1]' truncated because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
907 $self->Data, $self->OldValue, $self->NewValue ); #loc()
910 return ( "Content truncated because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
911 $self->OldValue, $self->NewValue ); #loc()
914 AttachmentDrop => sub {
916 if ( defined $self->Data ) {
917 return ( "File '[_1]' dropped because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
918 $self->Data, $self->OldValue, $self->NewValue ); #loc()
921 return ( "Content dropped because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
922 $self->OldValue, $self->NewValue ); #loc()
925 AttachmentError => sub {
927 if ( defined $self->Data ) {
928 return ( "File '[_1]' insert failed. See error log for details.", $self->Data ); #loc()
931 return ( "Content insert failed. See error log for details." ); #loc()
934 "Forward Transaction" => sub {
936 my $recipients = join ", ", map {
937 RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
938 } RT::EmailParser->ParseEmailAddress($self->Data);
940 return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]",
941 $self->Field, $recipients,
942 [\'<a href="#txn-', $self->Field, \'">'], \'</a>'); #loc()
944 "Forward Ticket" => sub {
946 my $recipients = join ", ", map {
947 RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
948 } RT::EmailParser->ParseEmailAddress($self->Data);
950 return ( "Forwarded Ticket to [_1]", $recipients ); #loc()
952 CommentEmailRecord => sub {
954 return ("Outgoing email about a comment recorded"); #loc()
958 return ("Outgoing email recorded"); #loc()
962 return ("Correspondence added"); #loc()
966 return ("Comments added"); #loc()
970 my $field = $self->loc('CustomField');
973 if ( $self->Field ) {
974 $cf = RT::CustomField->new( $self->CurrentUser );
975 $cf->SetContextObject( $self->Object );
976 $cf->Load( $self->Field );
977 $field = $cf->Name();
978 $field = $self->loc('a custom field') if !defined($field);
981 my $new = $self->NewValue;
982 my $old = $self->OldValue;
986 if ( $cf->Type eq 'DateTime' ) {
988 my $date = RT::Date->new( $self->CurrentUser );
989 $date->Set( Format => 'ISO', Value => $old );
990 $old = $date->AsString;
994 my $date = RT::Date->new( $self->CurrentUser );
995 $date->Set( Format => 'ISO', Value => $new );
996 $new = $date->AsString;
999 elsif ( $cf->Type eq 'Date' ) {
1001 my $date = RT::Date->new( $self->CurrentUser );
1003 Format => 'unknown',
1007 $old = $date->AsString( Time => 0, Timezone => 'UTC' );
1011 my $date = RT::Date->new( $self->CurrentUser );
1013 Format => 'unknown',
1017 $new = $date->AsString( Time => 0, Timezone => 'UTC' );
1022 if ( !defined($old) || $old eq '' ) {
1023 return ("[_1] [_2] added", $field, $new); #loc()
1025 elsif ( !defined($new) || $new eq '' ) {
1026 return ("[_1] [_2] deleted", $field, $old); #loc()
1029 return ("[_1] [_2] changed to [_3]", $field, $old, $new); #loc()
1034 return ("Untaken"); #loc()
1038 return ("Taken"); #loc()
1042 my $Old = RT::User->new( $self->CurrentUser );
1043 $Old->Load( $self->OldValue );
1044 my $New = RT::User->new( $self->CurrentUser );
1045 $New->Load( $self->NewValue );
1047 return ("Owner forcibly changed from [_1] to [_2]",
1048 map { $self->_FormatUser($_) } $Old, $New); #loc()
1052 my $Old = RT::User->new( $self->CurrentUser );
1053 $Old->Load( $self->OldValue );
1054 return ("Stolen from [_1]", $self->_FormatUser($Old)); #loc()
1058 my $New = RT::User->new( $self->CurrentUser );
1059 $New->Load( $self->NewValue );
1060 return ( "Given to [_1]", $self->_FormatUser($New)); #loc()
1064 my $principal = RT::Principal->new($self->CurrentUser);
1065 $principal->Load($self->NewValue);
1066 return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
1070 my $principal = RT::Principal->new($self->CurrentUser);
1071 $principal->Load($self->OldValue);
1072 return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
1076 my $principal = RT::Principal->new($self->CurrentUser);
1077 $principal->Load($self->NewValue);
1078 return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
1082 return ( "Subject changed to [_1]", $self->Data ); #loc()
1087 if ( $self->NewValue ) {
1088 my $URI = RT::URI->new( $self->CurrentUser );
1089 if ( $URI->FromURI( $self->NewValue ) ) {
1091 \'<a href="', $URI->AsHREF, \'">',
1097 $value = $self->NewValue;
1100 if ( $self->Field eq 'DependsOn' ) {
1101 return ( "Dependency on [_1] added", $value ); #loc()
1103 elsif ( $self->Field eq 'DependedOnBy' ) {
1104 return ( "Dependency by [_1] added", $value ); #loc()
1106 elsif ( $self->Field eq 'RefersTo' ) {
1107 return ( "Reference to [_1] added", $value ); #loc()
1109 elsif ( $self->Field eq 'ReferredToBy' ) {
1110 return ( "Reference by [_1] added", $value ); #loc()
1112 elsif ( $self->Field eq 'MemberOf' ) {
1113 return ( "Membership in [_1] added", $value ); #loc()
1115 elsif ( $self->Field eq 'HasMember' ) {
1116 return ( "Member [_1] added", $value ); #loc()
1118 elsif ( $self->Field eq 'MergedInto' ) {
1119 return ( "Merged into [_1]", $value ); #loc()
1123 return ( "[_1]", $self->Data ); #loc()
1129 if ( $self->OldValue ) {
1130 my $URI = RT::URI->new( $self->CurrentUser );
1131 if ( $URI->FromURI( $self->OldValue ) ) {
1133 \'<a href="', $URI->AsHREF, \'">',
1139 $value = $self->OldValue;
1142 if ( $self->Field eq 'DependsOn' ) {
1143 return ( "Dependency on [_1] deleted", $value ); #loc()
1145 elsif ( $self->Field eq 'DependedOnBy' ) {
1146 return ( "Dependency by [_1] deleted", $value ); #loc()
1148 elsif ( $self->Field eq 'RefersTo' ) {
1149 return ( "Reference to [_1] deleted", $value ); #loc()
1151 elsif ( $self->Field eq 'ReferredToBy' ) {
1152 return ( "Reference by [_1] deleted", $value ); #loc()
1154 elsif ( $self->Field eq 'MemberOf' ) {
1155 return ( "Membership in [_1] deleted", $value ); #loc()
1157 elsif ( $self->Field eq 'HasMember' ) {
1158 return ( "Member [_1] deleted", $value ); #loc()
1162 return ( "[_1]", $self->Data ); #loc()
1167 if ( $self->Field eq 'Told' ) {
1168 my $t1 = RT::Date->new($self->CurrentUser);
1169 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1170 my $t2 = RT::Date->new($self->CurrentUser);
1171 $t2->Set(Format => 'ISO', Value => $self->OldValue);
1172 return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc()
1175 return ( "[_1] changed from [_2] to [_3]",
1176 $self->loc($self->Field),
1177 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" ); #loc()
1182 if ( $self->Field eq 'Password' ) {
1183 return ('Password changed'); #loc()
1185 elsif ( $self->Field eq 'Queue' ) {
1186 my $q1 = RT::Queue->new( $self->CurrentUser );
1187 $q1->Load( $self->OldValue );
1188 my $q2 = RT::Queue->new( $self->CurrentUser );
1189 $q2->Load( $self->NewValue );
1190 return ("[_1] changed from [_2] to [_3]",
1191 $self->loc($self->Field) , $q1->Name , $q2->Name); #loc()
1194 # Write the date/time change at local time:
1195 elsif ($self->Field =~ /Due|Starts|Started|Told|WillResolve/) {
1196 my $t1 = RT::Date->new($self->CurrentUser);
1197 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1198 my $t2 = RT::Date->new($self->CurrentUser);
1199 $t2->Set(Format => 'ISO', Value => $self->OldValue);
1200 return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc()
1202 elsif ( $self->Field eq 'Owner' ) {
1203 my $Old = RT::User->new( $self->CurrentUser );
1204 $Old->Load( $self->OldValue );
1205 my $New = RT::User->new( $self->CurrentUser );
1206 $New->Load( $self->NewValue );
1208 if ( $Old->id == RT->Nobody->id ) {
1209 if ( $New->id == $self->Creator ) {
1210 return ("Taken"); #loc()
1213 return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
1217 if ( $New->id == $self->Creator ) {
1218 return ("Stolen from [_1]", $self->_FormatUser($Old) ); #loc()
1220 elsif ( $Old->id == $self->Creator ) {
1221 if ( $New->id == RT->Nobody->id ) {
1222 return ("Untaken"); #loc()
1225 return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
1230 "Owner forcibly changed from [_1] to [_2]",
1231 map { $self->_FormatUser($_) } $Old, $New
1237 return ( "[_1] changed from [_2] to [_3]",
1238 $self->loc($self->Field),
1239 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
1240 ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)"))); #loc()
1243 "Set-TimeWorked" => sub {
1245 my $old = $self->OldValue || 0;
1246 my $new = $self->NewValue || 0;
1247 my $duration = $new - $old;
1248 if ($duration < 0) {
1249 return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration); # loc()
1251 elsif ($duration < 60) {
1252 return ("Worked [quant,_1,minute,minutes]", $duration); # loc()
1254 return ("Worked [quant,_1,hour,hours] ([quant,_2,minute,minutes])", sprintf("%.1f", $duration / 60), $duration); # loc()
1257 PurgeTransaction => sub {
1259 return ("Transaction [_1] purged", $self->Data); #loc()
1261 AddReminder => sub {
1263 my $ticket = RT::Ticket->new($self->CurrentUser);
1264 $ticket->Load($self->NewValue);
1266 \'<a href="', RT->Config->Get('WebPath'),
1267 "/Ticket/Reminders.html?id=", $self->ObjectId,
1268 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1270 return ("Reminder '[_1]' added", $subject); #loc()
1272 OpenReminder => sub {
1274 my $ticket = RT::Ticket->new($self->CurrentUser);
1275 $ticket->Load($self->NewValue);
1277 \'<a href="', RT->Config->Get('WebPath'),
1278 "/Ticket/Reminders.html?id=", $self->ObjectId,
1279 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1281 return ("Reminder '[_1]' reopened", $subject); #loc()
1283 ResolveReminder => sub {
1285 my $ticket = RT::Ticket->new($self->CurrentUser);
1286 $ticket->Load($self->NewValue);
1288 \'<a href="', RT->Config->Get('WebPath'),
1289 "/Ticket/Reminders.html?id=", $self->ObjectId,
1290 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1292 return ("Reminder '[_1]' completed", $subject); #loc()
1301 Returns true if the creator of the transaction is a requestor of the ticket.
1302 Returns false otherwise
1308 $self->ObjectType eq 'RT::Ticket' or return undef;
1309 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
1314 sub _OverlayAccessible {
1317 ObjectType => { public => 1},
1318 ObjectId => { public => 1},
1328 return ( 0, $self->loc('Transactions are immutable') );
1335 Takes the name of a table column.
1336 Returns its value as a string, if the user passes an ACL check
1344 #if the field is public, return it.
1345 if ( $self->_Accessible( $field, 'public' ) ) {
1346 return $self->SUPER::_Value( $field );
1349 unless ( $self->CurrentUserCanSee ) {
1353 return $self->SUPER::_Value( $field );
1357 =head2 CurrentUserCanSee
1359 Returns true if current user has rights to see this particular transaction.
1361 This fact depends on type of the transaction, type of an object the transaction
1362 is attached to and may be other conditions, so this method is prefered over
1363 custom implementations.
1365 It always returns true if current user is system user.
1369 sub CurrentUserCanSee {
1372 return 1 if $self->CurrentUser->PrincipalObj->Id == RT->SystemUser->Id;
1374 # Make sure the user can see the custom field before showing that it changed
1375 my $type = $self->__Value('Type');
1376 if ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1377 my $cf = RT::CustomField->new( $self->CurrentUser );
1378 $cf->SetContextObject( $self->Object );
1379 $cf->Load( $cf_id );
1380 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1383 # Transactions that might have changed the ->Object's visibility to
1384 # the current user are marked readable
1385 return 1 if $self->{ _object_is_readable };
1387 # Defer to the object in question
1388 return $self->Object->CurrentUserCanSee("Transaction", $self);
1394 return $self->ObjectId;
1399 return $self->Object;
1404 if ( my $Object = $self->OldReferenceObject ) {
1405 return $Object->Content;
1408 return $self->_Value('OldValue');
1414 if ( my $Object = $self->NewReferenceObject ) {
1415 return $Object->Content;
1418 return $self->_Value('NewValue');
1424 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1425 $Object->Load($self->__Value('ObjectId'));
1429 =head2 NewReferenceObject
1431 =head2 OldReferenceObject
1433 Returns an object of the class specified by the column C<ReferenceType> and
1434 loaded with the id specified by the column C<NewReference> or C<OldReference>.
1435 C<ReferenceType> is assumed to be an L<RT::Record> subclass.
1437 The object may be unloaded (check C<< $object->id >>) if the reference is
1438 corrupt (such as if the referenced record was improperly deleted).
1440 Returns undef if either C<ReferenceType> or C<NewReference>/C<OldReference> is
1445 sub NewReferenceObject { $_[0]->_ReferenceObject("New") }
1446 sub OldReferenceObject { $_[0]->_ReferenceObject("Old") }
1448 sub _ReferenceObject {
1451 my $type = $self->__Value("ReferenceType");
1452 my $id = $self->__Value("${which}Reference");
1453 return unless $type and $id;
1455 my $object = $type->new($self->CurrentUser);
1456 $object->Load( $id );
1460 sub FriendlyObjectType {
1462 return $self->loc( $self->Object->RecordType );
1465 =head2 UpdateCustomFields
1469 CustomField-C<Id> => Value
1473 Object-RT::Transaction-CustomField-C<Id> => Value
1475 parameters to update this transaction's custom fields.
1479 sub UpdateCustomFields {
1483 # This method used to have an API that took a hash of a single
1484 # value "ARGSRef", which was a reference to a hash of arguments.
1485 # This was insane. The next few lines of code preserve that API
1486 # while giving us something saner.
1488 if ($args{'ARGSRef'}) {
1489 RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" );
1490 $args = $args{ARGSRef};
1495 foreach my $arg ( keys %$args ) {
1498 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1499 next if $arg =~ /-Magic$/;
1500 next if $arg =~ /-TimeUnits$/;
1502 my $values = $args->{$arg};
1503 my $cf = $self->LoadCustomFieldByIdentifier($cfid);
1504 next unless $cf->ObjectTypeFromLookupType($cf->__Value('LookupType'))->isa(ref $self);
1506 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1508 next unless (defined($value) && length($value));
1509 $self->_AddCustomFieldValue(
1512 RecordTransaction => 0,
1518 =head2 LoadCustomFieldByIdentifier
1520 Finds and returns the custom field of the given name for the
1521 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1522 look for queue-specific CFs before global ones.
1526 sub LoadCustomFieldByIdentifier {
1530 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1531 if ref $field or $field =~ /^\d+$/;
1533 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1534 unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1536 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1537 $CFs->SetContextObject( $self->Object );
1538 $CFs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
1539 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1540 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1541 return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1544 =head2 CustomFieldLookupType
1546 Returns the RT::Transaction lookup type, which can
1547 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1552 sub CustomFieldLookupType {
1553 "RT::Queue-RT::Ticket-RT::Transaction";
1557 =head2 SquelchMailTo
1559 Similar to Ticket class SquelchMailTo method - returns a list of
1560 transaction's squelched addresses. As transactions are immutable, the
1561 list of squelched recipients cannot be modified after creation.
1567 return () unless $self->CurrentUserCanSee;
1568 return $self->Attributes->Named('SquelchMailTo');
1573 Returns the list of email addresses (as L<Email::Address> objects)
1574 that this transaction would send mail to. There may be duplicates.
1581 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1582 my $action = $scrip->ActionObj->Action;
1583 next unless $action->isa('RT::Action::SendEmail');
1585 foreach my $type (qw(To Cc Bcc)) {
1586 push @recipients, $action->$type();
1590 if ( $self->Rules ) {
1591 for my $rule (@{$self->Rules}) {
1592 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1593 my $data = $rule->{hints}{recipients};
1594 foreach my $type (qw(To Cc Bcc)) {
1595 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1602 =head2 DeferredRecipients($freq, $include_sent )
1604 Takes the following arguments:
1608 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1610 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1614 Returns an array of users who should now receive the notification that
1615 was recorded in this transaction. Returns an empty array if there were
1616 no deferred users, or if $include_sent was not specified and the deferred
1617 notifications have been sent.
1621 sub DeferredRecipients {
1624 my $include_sent = @_? shift : 0;
1626 my $attr = $self->FirstAttribute('DeferredRecipients');
1628 return () unless ($attr);
1630 my $deferred = $attr->Content;
1632 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1636 for my $user (keys %{$deferred->{$freq}}) {
1637 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1638 delete $deferred->{$freq}->{$user}
1641 # Now get our users. Easy.
1643 return keys %{ $deferred->{$freq} };
1648 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1651 'cache_for_sec' => 6000,
1656 =head2 ACLEquivalenceObjects
1658 This method returns a list of objects for which a user's rights also apply
1659 to this Transaction.
1661 This currently only applies to Transaction Custom Fields on Tickets, so we return
1662 the Ticket's Queue and the Ticket.
1664 This method is called from L<RT::Principal/HasRight>.
1668 sub ACLEquivalenceObjects {
1671 return unless $self->ObjectType eq 'RT::Ticket';
1672 my $object = $self->Object;
1673 return $object,$object->QueueObj;
1683 Returns the current value of id.
1684 (In the database, id is stored as int(11).)
1692 Returns the current value of ObjectType.
1693 (In the database, ObjectType is stored as varchar(64).)
1697 =head2 SetObjectType VALUE
1700 Set ObjectType to VALUE.
1701 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1702 (In the database, ObjectType will be stored as a varchar(64).)
1710 Returns the current value of ObjectId.
1711 (In the database, ObjectId is stored as int(11).)
1715 =head2 SetObjectId VALUE
1718 Set ObjectId to VALUE.
1719 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1720 (In the database, ObjectId will be stored as a int(11).)
1728 Returns the current value of TimeTaken.
1729 (In the database, TimeTaken is stored as int(11).)
1733 =head2 SetTimeTaken VALUE
1736 Set TimeTaken to VALUE.
1737 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1738 (In the database, TimeTaken will be stored as a int(11).)
1746 Returns the current value of Type.
1747 (In the database, Type is stored as varchar(20).)
1751 =head2 SetType VALUE
1755 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1756 (In the database, Type will be stored as a varchar(20).)
1764 Returns the current value of Field.
1765 (In the database, Field is stored as varchar(40).)
1769 =head2 SetField VALUE
1773 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1774 (In the database, Field will be stored as a varchar(40).)
1782 Returns the current value of OldValue.
1783 (In the database, OldValue is stored as varchar(255).)
1787 =head2 SetOldValue VALUE
1790 Set OldValue to VALUE.
1791 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1792 (In the database, OldValue will be stored as a varchar(255).)
1800 Returns the current value of NewValue.
1801 (In the database, NewValue is stored as varchar(255).)
1805 =head2 SetNewValue VALUE
1808 Set NewValue to VALUE.
1809 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1810 (In the database, NewValue will be stored as a varchar(255).)
1816 =head2 ReferenceType
1818 Returns the current value of ReferenceType.
1819 (In the database, ReferenceType is stored as varchar(255).)
1823 =head2 SetReferenceType VALUE
1826 Set ReferenceType to VALUE.
1827 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1828 (In the database, ReferenceType will be stored as a varchar(255).)
1836 Returns the current value of OldReference.
1837 (In the database, OldReference is stored as int(11).)
1841 =head2 SetOldReference VALUE
1844 Set OldReference to VALUE.
1845 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1846 (In the database, OldReference will be stored as a int(11).)
1854 Returns the current value of NewReference.
1855 (In the database, NewReference is stored as int(11).)
1859 =head2 SetNewReference VALUE
1862 Set NewReference to VALUE.
1863 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1864 (In the database, NewReference will be stored as a int(11).)
1872 Returns the current value of Data.
1873 (In the database, Data is stored as varchar(255).)
1877 =head2 SetData VALUE
1881 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1882 (In the database, Data will be stored as a varchar(255).)
1890 Returns the current value of Creator.
1891 (In the database, Creator is stored as int(11).)
1899 Returns the current value of Created.
1900 (In the database, Created is stored as datetime.)
1907 sub _CoreAccessible {
1911 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1913 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1915 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1917 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1919 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1921 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1923 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1925 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1927 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1929 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1931 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1933 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1935 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1937 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1942 sub FindDependencies {
1944 my ($walker, $deps) = @_;
1946 $self->SUPER::FindDependencies($walker, $deps);
1948 $deps->Add( out => $self->Object );
1949 $deps->Add( in => $self->Attachments );
1951 my $type = $self->Type;
1952 if ($type eq "CustomField") {
1953 my $cf = RT::CustomField->new( RT->SystemUser );
1954 $cf->Load( $self->Field );
1955 $deps->Add( out => $cf );
1956 } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1957 for my $field (qw/OldValue NewValue/) {
1958 my $user = RT::User->new( RT->SystemUser );
1959 $user->Load( $self->$field );
1960 $deps->Add( out => $user );
1962 } elsif ($type eq "DelWatcher") {
1963 my $principal = RT::Principal->new( RT->SystemUser );
1964 $principal->Load( $self->OldValue );
1965 $deps->Add( out => $principal->Object );
1966 } elsif ($type eq "AddWatcher") {
1967 my $principal = RT::Principal->new( RT->SystemUser );
1968 $principal->Load( $self->NewValue );
1969 $deps->Add( out => $principal->Object );
1970 } elsif ($type eq "DeleteLink") {
1971 if ($self->OldValue) {
1972 my $base = RT::URI->new( $self->CurrentUser );
1973 $base->FromURI( $self->OldValue );
1974 $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1976 } elsif ($type eq "AddLink") {
1977 if ($self->NewValue) {
1978 my $base = RT::URI->new( $self->CurrentUser );
1979 $base->FromURI( $self->NewValue );
1980 $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1982 } elsif ($type eq "Set" and $self->Field eq "Queue") {
1983 for my $field (qw/OldValue NewValue/) {
1984 my $queue = RT::Queue->new( RT->SystemUser );
1985 $queue->Load( $self->$field );
1986 $deps->Add( out => $queue );
1988 } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1989 my $ticket = RT::Ticket->new( RT->SystemUser );
1990 $ticket->Load( $self->NewValue );
1991 $deps->Add( out => $ticket );
1999 Dependencies => undef,
2002 my $deps = $args{'Dependencies'};
2004 $deps->_PushDependencies(
2005 BaseObject => $self,
2006 Flags => RT::Shredder::Constants::DEPENDS_ON,
2007 TargetObjects => $self->Attachments,
2008 Shredder => $args{'Shredder'}
2011 return $self->SUPER::__DependsOn( %args );
2017 my %store = $self->SUPER::Serialize(@_);
2019 my $type = $store{Type};
2020 if ($type eq "CustomField") {
2021 my $cf = RT::CustomField->new( RT->SystemUser );
2022 $cf->Load( $store{Field} );
2023 $store{Field} = \($cf->UID);
2024 } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
2025 for my $field (qw/OldValue NewValue/) {
2026 my $user = RT::User->new( RT->SystemUser );
2027 $user->Load( $store{$field} );
2028 $store{$field} = \($user->UID);
2030 } elsif ($type eq "DelWatcher") {
2031 my $principal = RT::Principal->new( RT->SystemUser );
2032 $principal->Load( $store{OldValue} );
2033 $store{OldValue} = \($principal->UID);
2034 } elsif ($type eq "AddWatcher") {
2035 my $principal = RT::Principal->new( RT->SystemUser );
2036 $principal->Load( $store{NewValue} );
2037 $store{NewValue} = \($principal->UID);
2038 } elsif ($type eq "DeleteLink") {
2039 if ($store{OldValue}) {
2040 my $base = RT::URI->new( $self->CurrentUser );
2041 $base->FromURI( $store{OldValue} );
2042 $store{OldValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
2044 } elsif ($type eq "AddLink") {
2045 if ($store{NewValue}) {
2046 my $base = RT::URI->new( $self->CurrentUser );
2047 $base->FromURI( $store{NewValue} );
2048 $store{NewValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
2050 } elsif ($type eq "Set" and $store{Field} eq "Queue") {
2051 for my $field (qw/OldValue NewValue/) {
2052 my $queue = RT::Queue->new( RT->SystemUser );
2053 $queue->Load( $store{$field} );
2054 $store{$field} = \($queue->UID);
2056 } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
2057 my $ticket = RT::Ticket->new( RT->SystemUser );
2058 $ticket->Load( $store{NewValue} );
2059 $store{NewValue} = \($ticket->UID);
2067 my ($importer, $uid, $data) = @_;
2069 if ($data->{Object} and ref $data->{Object}) {
2070 my $on_uid = ${ $data->{Object} };
2071 return if $importer->ShouldSkipTransaction($on_uid);
2074 if ($data->{Type} eq "DeleteLink" and ref $data->{OldValue}) {
2075 my $uid = ${ $data->{OldValue} };
2076 my $obj = $importer->LookupObj( $uid );
2077 $data->{OldValue} = $obj->URI;
2078 } elsif ($data->{Type} eq "AddLink" and ref $data->{NewValue}) {
2079 my $uid = ${ $data->{NewValue} };
2080 my $obj = $importer->LookupObj( $uid );
2081 $data->{NewValue} = $obj->URI;
2084 return $class->SUPER::PreInflate( $importer, $uid, $data );
2087 RT::Base->_ImportOverlays();