1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 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'};
319 Returns whether this transaction has attached mime objects.
325 my $type = $PreferredContentType || '';
326 return !!$self->ContentObj( $type ? ( Type => $type) : () );
331 =head2 Content PARAMHASH
333 If this transaction has attached mime objects, returns the body of the first
334 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
335 returns the message "This transaction appears to have no content".
337 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
338 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
340 If $args{'Type'} is set to C<text/html>, this will return an HTML
341 part of the message, if available. Otherwise it looks for a text/plain
342 part. If $args{'Type'} is missing, it defaults to the value of
343 C<$RT::Transaction::PreferredContentType>, if that's missing too,
351 Type => $PreferredContentType || '',
354 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
359 if ( my $content_obj =
360 $self->ContentObj( $args{Type} ? ( Type => $args{Type}) : () ) )
362 $content = $content_obj->Content ||'';
364 if ( lc $content_obj->ContentType eq 'text/html' ) {
365 $content =~ s/(?:(<\/div>)|<p>|<br\s*\/?>|<div(\s+class="[^"]+")?>)\s*--\s+<br\s*\/?>.*?$/$1/s if $args{'Quote'};
367 if ($args{Type} ne 'text/html') {
368 $content = RT::Interface::Email::ConvertHTMLToText($content);
370 # Scrub out <html>, <head>, <meta>, and <body>, and
371 # leave all else untouched.
372 my $scrubber = HTML::Scrubber->new();
379 $scrubber->default( 1 => { '*' => 1 } );
380 $content = $scrubber->scrub( $content );
384 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
385 if ($args{Type} eq 'text/html') {
386 # Extremely simple text->html converter
387 $content =~ s/&/&/g;
388 $content =~ s/</</g;
389 $content =~ s/>/>/g;
390 $content = qq|<pre style="white-space: pre-wrap; font-family: monospace;">$content</pre>|;
395 # If all else fails, return a message that we couldn't find any content
397 $content = $self->loc('This transaction appears to have no content');
400 if ( $args{'Quote'} ) {
401 if ($args{Type} eq 'text/html') {
402 $content = '<div class="gmail_quote">'
404 . '<br /><blockquote class="gmail_quote" type="cite">'
406 . '</blockquote></div><br /><br />';
408 $content = $self->ApplyQuoteWrap(content => $content,
409 cols => $args{'Wrap'} );
411 $content = $self->QuoteHeader . "\n$content\n\n";
420 Returns text prepended to content when transaction is quoted
421 (see C<Quote> argument in L</Content>). By default returns
422 localized "On <date> <user name> wrote:\n".
428 return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
431 =head2 ApplyQuoteWrap PARAMHASH
433 Wrapper to calculate wrap criteria and apply quote wrapping if needed.
440 my $content = $args{content};
442 # What's the longest line like?
444 foreach ( split ( /\n/, $args{content} ) ) {
445 $max = length if length > $max;
449 require Text::Quoted;
450 require Text::Wrapper;
452 my $structure = Text::Quoted::extract($args{content});
453 $content = $self->QuoteWrap(content_ref => $structure,
458 $content =~ s/^/> /gm; # use regex since string might be multi-line
462 =head2 QuoteWrap PARAMHASH
464 Wrap the contents of transactions based on Wrap settings, maintaining
465 the quote character from the original.
472 my $ref = $args{content_ref};
475 if ( ref $ref eq 'ARRAY' ){
476 foreach my $array (@$ref){
477 $final_string .= $self->QuoteWrap(content_ref => $array,
482 elsif ( ref $ref eq 'HASH' ){
483 return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
485 my $col = $args{cols} - (length $ref->{quoter});
486 my $wrapper = Text::Wrapper->new( columns => $col );
488 # Wrap on individual lines to honor incoming line breaks
489 # Otherwise deliberate separate lines (like a list or a sig)
490 # all get combined incorrectly into single paragraphs.
492 my @lines = split /\n/, $ref->{text};
493 my $wrap = join '', map { $wrapper->wrap($_) } @lines;
494 my $quoter = $ref->{quoter};
496 # Only add the space if actually quoting
497 $quoter .= ' ' if length $quoter;
498 $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line
503 $RT::Logger->warning("Can't apply quoting with $ref");
506 return $final_string;
512 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
519 if (my $attach = $self->Attachments->First) {
520 return $attach->Addresses;
532 Returns the RT::Attachment object which contains the content for this Transaction
539 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
541 # If we don't have any content, return undef now.
542 # Get the set of toplevel attachments to this transaction.
544 my $Attachment = $args{'Attachment'};
546 $Attachment ||= $self->Attachments->First;
548 return undef unless ($Attachment);
550 my $Attachments = $self->Attachments;
551 while ( my $Attachment = $Attachments->Next ) {
552 if ( my $content = _FindPreferredContentObj( %args, Attachment => $Attachment ) ) {
557 # If that fails, return the first top-level textual part which has some content.
558 # We probably really want this to become "recurse, looking for the other type of
559 # displayable". For now, this maintains backcompat
560 my $all_parts = $self->Attachments;
561 while ( my $part = $all_parts->Next ) {
562 next unless _IsDisplayableTextualContentType($part->ContentType)
571 sub _FindPreferredContentObj {
573 my $Attachment = $args{Attachment};
575 # If we don't have any content, return undef now.
576 return undef unless $Attachment;
578 # If it's a textual part, just return the body.
579 if ( _IsDisplayableTextualContentType($Attachment->ContentType) ) {
580 return ($Attachment);
583 # If it's a multipart object, first try returning the first part with preferred
584 # MIME type ('text/plain' by default).
586 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
587 my $kids = $Attachment->Children;
588 while (my $child = $kids->Next) {
589 my $ret = _FindPreferredContentObj(%args, Attachment => $child);
590 return $ret if ($ret);
593 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
595 my $plain_parts = $Attachment->Children;
596 $plain_parts->ContentType( VALUE => $args{Type} );
597 $plain_parts->LimitNotEmpty;
599 # If we actully found a part, return its content
600 if ( my $first = $plain_parts->First ) {
604 my $parts = $Attachment->Children;
605 $parts->LimitNotEmpty;
607 # If we actully found a part, return its content
608 while (my $part = $parts->Next) {
609 next unless _IsDisplayableTextualContentType($part->ContentType);
616 # If this is a message/rfc822 mail, we need to dig into it in order to find
617 # the actual textual content
619 elsif ( $Attachment->ContentType =~ '^message/rfc822' ) {
620 my $children = $Attachment->Children;
621 while ( my $child = $children->Next ) {
622 if ( my $content = _FindPreferredContentObj( %args, Attachment => $child ) ) {
628 # We found no content. suck
632 =head2 _IsDisplayableTextualContentType
634 We may need to pull this out to another module later, but for now, this
635 is better than RT::I18N::IsTextualContentType because that believes that
636 a message/rfc822 email is displayable, despite it having no content
640 sub _IsDisplayableTextualContentType {
642 ($type =~ m{^text/(?:plain|html)\b}i) ? 1 : 0;
648 If this transaction has attached mime objects, returns the first one's subject
649 Otherwise, returns null
655 return undef unless my $first = $self->Attachments->First;
656 return $first->Subject;
663 Returns all the RT::Attachment objects which are attached
664 to this transaction. Takes an optional parameter, which is
665 a ContentType that Attachments should be restricted to.
672 if ( $self->{'attachments'} ) {
673 $self->{'attachments'}->GotoFirstItem;
674 return $self->{'attachments'};
677 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
679 unless ( $self->CurrentUserCanSee ) {
680 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
681 return $self->{'attachments'};
684 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
686 # Get the self->{'attachments'} in the order they're put into
687 # the database. Arguably, we should be returning a tree
688 # of self->{'attachments'}, not a set...but no current app seems to need
691 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
693 return $self->{'attachments'};
700 A private method used to attach a mime object to this transaction.
706 my $MIMEObject = shift;
708 unless ( defined $MIMEObject ) {
709 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
710 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
713 my $Attachment = RT::Attachment->new( $self->CurrentUser );
714 my ($id, $msg) = $Attachment->Create(
715 TransactionId => $self->Id,
716 Attachment => $MIMEObject
718 return ( $Attachment, $msg || $self->loc("Attachment created") );
726 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
727 # since it has less information available without looking to it's parent
728 # transaction. Check ACLs here before we go any further.
729 return unless $self->CurrentUserCanSee;
731 my $attachments = RT::Attachments->new( $self->CurrentUser );
732 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
733 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
734 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
735 $attachments->RowsPerPage(1);
737 my $top = $attachments->First;
740 my $entity = MIME::Entity->build(
741 Type => 'message/rfc822',
742 Description => 'transaction ' . $self->id,
743 Data => $top->ContentAsMIME(Children => 1)->as_string,
753 Returns a text string which describes this transaction
760 unless ( $self->CurrentUserCanSee ) {
761 return ( $self->loc("Permission Denied") );
764 unless ( defined $self->Type ) {
765 return ( $self->loc("No transaction type specified"));
768 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
773 =head2 BriefDescription
775 Returns a text string which briefly describes this transaction
780 my $scrubber = HTML::Scrubber->new(default => 0); # deny everything
782 sub BriefDescription {
784 my $desc = $self->BriefDescriptionAsHTML;
785 $desc = $scrubber->scrub($desc);
786 $desc = HTML::Entities::decode_entities($desc);
791 =head2 BriefDescriptionAsHTML
793 Returns an HTML string which briefly describes this transaction.
797 sub BriefDescriptionAsHTML {
800 unless ( $self->CurrentUserCanSee ) {
801 return ( $self->loc("Permission Denied") );
804 my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field);
806 unless ( defined $type ) {
807 return $self->loc("No transaction type specified");
810 my ($template, @params);
812 my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
814 ? ("$objecttype-$type-$field", "$type-$field")
816 "$objecttype-$type", $type;
819 ($template, @params) = $code[0]->($self);
823 ($template, @params) = (
824 "Default: [_1]/[_2] changed from [_3] to [_4]", #loc
829 ? "'" . $self->OldValue . "'"
830 : $self->loc("(no value)")
834 ? "'" . $self->NewValue . "'"
835 : $self->loc("(no value)")
839 return $self->loc($template, $self->_ProcessReturnValues(@params));
842 sub _ProcessReturnValues {
846 if (ref eq 'ARRAY') { $_ = join "", $self->_ProcessReturnValues(@$_) }
847 elsif (ref eq 'SCALAR') { $_ = $$_ }
848 else { RT::Interface::Web::EscapeHTML(\$_) }
853 sub _FormatPrincipal {
855 my $principal = shift;
856 if ($principal->IsUser) {
857 return $self->_FormatUser( $principal->Object );
859 return $self->loc("group [_1]", $principal->Object->Name);
867 \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
873 %_BriefDescriptions = (
876 return ( "[_1] created", $self->FriendlyObjectType ); #loc()
880 return ( "[_1] enabled", $self->Field ? $self->loc($self->Field) : $self->FriendlyObjectType ); #loc()
884 return ( "[_1] disabled", $self->Field ? $self->loc($self->Field) : $self->FriendlyObjectType ); #loc()
888 if ( $self->Field eq 'Status' ) {
889 if ( $self->NewValue eq 'deleted' ) {
890 return ( "[_1] deleted", $self->FriendlyObjectType ); #loc()
893 my $canon = $self->Object->DOES("RT::Record::Role::Status")
894 ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
895 : sub { return $_[0] };
897 "Status changed from [_1] to [_2]",
898 "'" . $self->loc( $canon->($self->OldValue) ) . "'",
899 "'" . $self->loc( $canon->($self->NewValue) ) . "'"
905 my $no_value = $self->loc("(no value)");
907 "[_1] changed from [_2] to [_3]",
909 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
910 "'" . $self->NewValue . "'"
915 return $self->Data // ("System error"); #loc()
917 AttachmentTruncate => sub {
919 if ( defined $self->Data ) {
920 return ( "File '[_1]' truncated because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
921 $self->Data, $self->OldValue, $self->NewValue ); #loc()
924 return ( "Content truncated because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
925 $self->OldValue, $self->NewValue ); #loc()
928 AttachmentDrop => sub {
930 if ( defined $self->Data ) {
931 return ( "File '[_1]' dropped because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
932 $self->Data, $self->OldValue, $self->NewValue ); #loc()
935 return ( "Content dropped because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
936 $self->OldValue, $self->NewValue ); #loc()
939 AttachmentError => sub {
941 if ( defined $self->Data ) {
942 return ( "File '[_1]' insert failed. See error log for details.", $self->Data ); #loc()
945 return ( "Content insert failed. See error log for details." ); #loc()
948 "Forward Transaction" => sub {
950 my $recipients = join ", ", map {
951 RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
952 } RT::EmailParser->ParseEmailAddress($self->Data);
954 return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]",
955 $self->Field, $recipients,
956 [\'<a href="#txn-', $self->Field, \'">'], \'</a>'); #loc()
958 "Forward Ticket" => sub {
960 my $recipients = join ", ", map {
961 RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
962 } RT::EmailParser->ParseEmailAddress($self->Data);
964 return ( "Forwarded Ticket to [_1]", $recipients ); #loc()
966 CommentEmailRecord => sub {
968 return ("Outgoing email about a comment recorded"); #loc()
972 return ("Outgoing email recorded"); #loc()
976 return ("Correspondence added"); #loc()
980 return ("Comments added"); #loc()
984 my $field = $self->loc('CustomField');
987 if ( $self->Field ) {
988 $cf = RT::CustomField->new( $self->CurrentUser );
989 $cf->SetContextObject( $self->Object );
990 $cf->Load( $self->Field );
991 $field = $cf->Name();
992 $field = $self->loc('a custom field') if !defined($field);
995 my $new = $self->NewValue;
996 my $old = $self->OldValue;
1000 if ( $cf->Type eq 'DateTime' ) {
1002 my $date = RT::Date->new( $self->CurrentUser );
1003 $date->Set( Format => 'ISO', Value => $old );
1004 $old = $date->AsString;
1008 my $date = RT::Date->new( $self->CurrentUser );
1009 $date->Set( Format => 'ISO', Value => $new );
1010 $new = $date->AsString;
1013 elsif ( $cf->Type eq 'Date' ) {
1015 my $date = RT::Date->new( $self->CurrentUser );
1017 Format => 'unknown',
1021 $old = $date->AsString( Time => 0, Timezone => 'UTC' );
1025 my $date = RT::Date->new( $self->CurrentUser );
1027 Format => 'unknown',
1031 $new = $date->AsString( Time => 0, Timezone => 'UTC' );
1036 if ( !defined($old) || $old eq '' ) {
1037 return ("[_1] [_2] added", $field, $new); #loc()
1039 elsif ( !defined($new) || $new eq '' ) {
1040 return ("[_1] [_2] deleted", $field, $old); #loc()
1043 return ("[_1] [_2] changed to [_3]", $field, $old, $new); #loc()
1048 return ("Untaken"); #loc()
1052 return ("Taken"); #loc()
1056 my $Old = RT::User->new( $self->CurrentUser );
1057 $Old->Load( $self->OldValue );
1058 my $New = RT::User->new( $self->CurrentUser );
1059 $New->Load( $self->NewValue );
1061 return ("Owner forcibly changed from [_1] to [_2]",
1062 map { $self->_FormatUser($_) } $Old, $New); #loc()
1066 my $Old = RT::User->new( $self->CurrentUser );
1067 $Old->Load( $self->OldValue );
1068 return ("Stolen from [_1]", $self->_FormatUser($Old)); #loc()
1072 my $New = RT::User->new( $self->CurrentUser );
1073 $New->Load( $self->NewValue );
1074 return ( "Given to [_1]", $self->_FormatUser($New)); #loc()
1078 my $principal = RT::Principal->new($self->CurrentUser);
1079 $principal->Load($self->NewValue);
1080 return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
1084 my $principal = RT::Principal->new($self->CurrentUser);
1085 $principal->Load($self->OldValue);
1086 return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
1090 my $principal = RT::Principal->new($self->CurrentUser);
1091 $principal->Load($self->NewValue);
1092 return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc()
1096 return ( "Subject changed to [_1]", $self->Data ); #loc()
1101 if ( $self->NewValue ) {
1102 my $URI = RT::URI->new( $self->CurrentUser );
1103 if ( $URI->FromURI( $self->NewValue ) ) {
1105 \'<a href="', $URI->AsHREF, \'">',
1111 $value = $self->NewValue;
1114 if ( $self->Field eq 'DependsOn' ) {
1115 return ( "Dependency on [_1] added", $value ); #loc()
1117 elsif ( $self->Field eq 'DependedOnBy' ) {
1118 return ( "Dependency by [_1] added", $value ); #loc()
1120 elsif ( $self->Field eq 'RefersTo' ) {
1121 return ( "Reference to [_1] added", $value ); #loc()
1123 elsif ( $self->Field eq 'ReferredToBy' ) {
1124 return ( "Reference by [_1] added", $value ); #loc()
1126 elsif ( $self->Field eq 'MemberOf' ) {
1127 return ( "Membership in [_1] added", $value ); #loc()
1129 elsif ( $self->Field eq 'HasMember' ) {
1130 return ( "Member [_1] added", $value ); #loc()
1132 elsif ( $self->Field eq 'MergedInto' ) {
1133 return ( "Merged into [_1]", $value ); #loc()
1137 return ( "[_1]", $self->Data ); #loc()
1143 if ( $self->OldValue ) {
1144 my $URI = RT::URI->new( $self->CurrentUser );
1145 if ( $URI->FromURI( $self->OldValue ) ) {
1147 \'<a href="', $URI->AsHREF, \'">',
1153 $value = $self->OldValue;
1156 if ( $self->Field eq 'DependsOn' ) {
1157 return ( "Dependency on [_1] deleted", $value ); #loc()
1159 elsif ( $self->Field eq 'DependedOnBy' ) {
1160 return ( "Dependency by [_1] deleted", $value ); #loc()
1162 elsif ( $self->Field eq 'RefersTo' ) {
1163 return ( "Reference to [_1] deleted", $value ); #loc()
1165 elsif ( $self->Field eq 'ReferredToBy' ) {
1166 return ( "Reference by [_1] deleted", $value ); #loc()
1168 elsif ( $self->Field eq 'MemberOf' ) {
1169 return ( "Membership in [_1] deleted", $value ); #loc()
1171 elsif ( $self->Field eq 'HasMember' ) {
1172 return ( "Member [_1] deleted", $value ); #loc()
1176 return ( "[_1]", $self->Data ); #loc()
1181 if ( $self->Field eq 'Told' ) {
1182 my $t1 = RT::Date->new($self->CurrentUser);
1183 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1184 my $t2 = RT::Date->new($self->CurrentUser);
1185 $t2->Set(Format => 'ISO', Value => $self->OldValue);
1186 return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc()
1189 return ( "[_1] changed from [_2] to [_3]",
1190 $self->loc($self->Field),
1191 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" ); #loc()
1196 if ( $self->Field eq 'Password' ) {
1197 return ('Password changed'); #loc()
1199 elsif ( $self->Field eq 'Queue' ) {
1200 my $q1 = RT::Queue->new( $self->CurrentUser );
1201 $q1->Load( $self->OldValue );
1202 my $q2 = RT::Queue->new( $self->CurrentUser );
1203 $q2->Load( $self->NewValue );
1204 return ("[_1] changed from [_2] to [_3]",
1205 $self->loc($self->Field), $q1->Name // '#'.$q1->id, $q2->Name // '#'.$q2->id); #loc()
1208 # Write the date/time change at local time:
1209 elsif ($self->Field =~ /^(?:Due|Starts|Started|Told|WillResolve)$/) {
1210 my $t1 = RT::Date->new($self->CurrentUser);
1211 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1212 my $t2 = RT::Date->new($self->CurrentUser);
1213 $t2->Set(Format => 'ISO', Value => $self->OldValue);
1214 return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc()
1216 elsif ( $self->Field eq 'Owner' ) {
1217 my $Old = RT::User->new( $self->CurrentUser );
1218 $Old->Load( $self->OldValue );
1219 my $New = RT::User->new( $self->CurrentUser );
1220 $New->Load( $self->NewValue );
1222 if ( $Old->id == RT->Nobody->id ) {
1223 if ( $New->id == $self->Creator ) {
1224 return ("Taken"); #loc()
1227 return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
1231 if ( $New->id == $self->Creator ) {
1232 return ("Stolen from [_1]", $self->_FormatUser($Old) ); #loc()
1234 elsif ( $Old->id == $self->Creator ) {
1235 if ( $New->id == RT->Nobody->id ) {
1236 return ("Untaken"); #loc()
1239 return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
1244 "Owner forcibly changed from [_1] to [_2]",
1245 map { $self->_FormatUser($_) } $Old, $New
1251 return ( "[_1] changed from [_2] to [_3]",
1252 $self->loc($self->Field),
1253 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
1254 ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)"))); #loc()
1257 "Set-TimeWorked" => sub {
1259 my $old = $self->OldValue || 0;
1260 my $new = $self->NewValue || 0;
1261 my $duration = $new - $old;
1262 if ($duration < 0) {
1263 return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration); # loc()
1265 elsif ($duration < 60) {
1266 return ("Worked [quant,_1,minute,minutes]", $duration); # loc()
1268 return ("Worked [quant,_1,hour,hours] ([quant,_2,minute,minutes])", sprintf("%.1f", $duration / 60), $duration); # loc()
1271 PurgeTransaction => sub {
1273 return ("Transaction [_1] purged", $self->Data); #loc()
1275 AddReminder => sub {
1277 my $ticket = RT::Ticket->new($self->CurrentUser);
1278 $ticket->Load($self->NewValue);
1279 if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
1281 \'<a href="', RT->Config->Get('WebPath'),
1282 "/Ticket/Reminders.html?id=", $self->ObjectId,
1283 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1285 return ("Reminder '[_1]' added", $subject); #loc()
1287 return ("Reminder added"); #loc()
1290 OpenReminder => sub {
1292 my $ticket = RT::Ticket->new($self->CurrentUser);
1293 $ticket->Load($self->NewValue);
1294 if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
1296 \'<a href="', RT->Config->Get('WebPath'),
1297 "/Ticket/Reminders.html?id=", $self->ObjectId,
1298 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1300 return ("Reminder '[_1]' reopened", $subject); #loc()
1302 return ("Reminder reopened"); #loc()
1305 ResolveReminder => sub {
1307 my $ticket = RT::Ticket->new($self->CurrentUser);
1308 $ticket->Load($self->NewValue);
1309 if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
1311 \'<a href="', RT->Config->Get('WebPath'),
1312 "/Ticket/Reminders.html?id=", $self->ObjectId,
1313 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1315 return ("Reminder '[_1]' completed", $subject); #loc()
1317 return ("Reminder completed"); #loc()
1322 my $principal = RT::Principal->new($self->CurrentUser);
1323 $principal->Load($self->Field);
1325 if ($principal->IsUser) {
1326 return ("Added user '[_1]'", $principal->Object->Name); #loc()
1329 return ("Added group '[_1]'", $principal->Object->Name); #loc()
1332 DeleteMember => sub {
1334 my $principal = RT::Principal->new($self->CurrentUser);
1335 $principal->Load($self->Field);
1337 if ($principal->IsUser) {
1338 return ("Removed user '[_1]'", $principal->Object->Name); #loc()
1341 return ("Removed group '[_1]'", $principal->Object->Name); #loc()
1344 AddMembership => sub {
1346 my $principal = RT::Principal->new($self->CurrentUser);
1347 $principal->Load($self->Field);
1348 return ("Added to group '[_1]'", $principal->Object->Name); #loc()
1350 DeleteMembership => sub {
1352 my $principal = RT::Principal->new($self->CurrentUser);
1353 $principal->Load($self->Field);
1354 return ("Removed from group '[_1]'", $principal->Object->Name); #loc()
1363 Returns true if the creator of the transaction is a requestor of the ticket.
1364 Returns false otherwise
1370 $self->ObjectType eq 'RT::Ticket' or return undef;
1371 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
1376 sub _OverlayAccessible {
1379 ObjectType => { public => 1},
1380 ObjectId => { public => 1},
1390 return ( 0, $self->loc('Transactions are immutable') );
1397 Takes the name of a table column.
1398 Returns its value as a string, if the user passes an ACL check
1406 #if the field is public, return it.
1407 if ( $self->_Accessible( $field, 'public' ) ) {
1408 return $self->SUPER::_Value( $field );
1411 unless ( $self->CurrentUserCanSee ) {
1415 return $self->SUPER::_Value( $field );
1419 =head2 CurrentUserCanSee
1421 Returns true if current user has rights to see this particular transaction.
1423 This fact depends on type of the transaction, type of an object the transaction
1424 is attached to and may be other conditions, so this method is prefered over
1425 custom implementations.
1427 It always returns true if current user is system user.
1431 sub CurrentUserCanSee {
1434 return 1 if $self->CurrentUser->PrincipalObj->Id == RT->SystemUser->Id;
1436 # Make sure the user can see the custom field before showing that it changed
1437 my $type = $self->__Value('Type');
1438 if ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1439 my $cf = RT::CustomField->new( $self->CurrentUser );
1440 $cf->SetContextObject( $self->Object );
1441 $cf->Load( $cf_id );
1442 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1445 # Transactions that might have changed the ->Object's visibility to
1446 # the current user are marked readable
1447 return 1 if $self->{ _object_is_readable };
1449 # Defer to the object in question
1450 return $self->Object->CurrentUserCanSee("Transaction", $self);
1456 return $self->ObjectId;
1461 return $self->Object;
1466 if ( my $Object = $self->OldReferenceObject ) {
1467 return $Object->Content;
1470 return $self->_Value('OldValue');
1476 if ( my $Object = $self->NewReferenceObject ) {
1477 return $Object->Content;
1480 return $self->_Value('NewValue');
1486 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1487 $Object->Load($self->__Value('ObjectId'));
1491 =head2 NewReferenceObject
1493 =head2 OldReferenceObject
1495 Returns an object of the class specified by the column C<ReferenceType> and
1496 loaded with the id specified by the column C<NewReference> or C<OldReference>.
1497 C<ReferenceType> is assumed to be an L<RT::Record> subclass.
1499 The object may be unloaded (check C<< $object->id >>) if the reference is
1500 corrupt (such as if the referenced record was improperly deleted).
1502 Returns undef if either C<ReferenceType> or C<NewReference>/C<OldReference> is
1507 sub NewReferenceObject { $_[0]->_ReferenceObject("New") }
1508 sub OldReferenceObject { $_[0]->_ReferenceObject("Old") }
1510 sub _ReferenceObject {
1513 my $type = $self->__Value("ReferenceType");
1514 my $id = $self->__Value("${which}Reference");
1515 return unless $type and $id;
1517 my $object = $type->new($self->CurrentUser);
1518 $object->Load( $id );
1522 sub FriendlyObjectType {
1524 return $self->loc( $self->Object->RecordType );
1527 =head2 UpdateCustomFields
1531 CustomField-C<Id> => Value
1535 Object-RT::Transaction-CustomField-C<Id> => Value
1537 parameters to update this transaction's custom fields.
1541 sub UpdateCustomFields {
1545 # This method used to have an API that took a hash of a single
1546 # value "ARGSRef", which was a reference to a hash of arguments.
1547 # This was insane. The next few lines of code preserve that API
1548 # while giving us something saner.
1550 if ($args{'ARGSRef'}) {
1551 RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" );
1552 $args = $args{ARGSRef};
1557 foreach my $arg ( keys %$args ) {
1560 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1561 next if $arg =~ /-Magic$/;
1562 next if $arg =~ /-TimeUnits$/;
1564 my $values = $args->{$arg};
1565 my $cf = $self->LoadCustomFieldByIdentifier($cfid);
1566 next unless $cf->ObjectTypeFromLookupType($cf->__Value('LookupType'))->isa(ref $self);
1568 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1570 next unless (defined($value) && length($value));
1571 $self->_AddCustomFieldValue(
1574 RecordTransaction => 0,
1580 =head2 LoadCustomFieldByIdentifier
1582 Finds and returns the custom field of the given name for the
1583 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1584 look for queue-specific CFs before global ones.
1588 sub LoadCustomFieldByIdentifier {
1592 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1593 if ref $field or $field =~ /^\d+$/;
1595 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1596 unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1598 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1599 $CFs->SetContextObject( $self->Object );
1600 $CFs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
1601 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1602 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1603 return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1606 =head2 CustomFieldLookupType
1608 Returns the RT::Transaction lookup type, which can
1609 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1614 sub CustomFieldLookupType {
1615 "RT::Queue-RT::Ticket-RT::Transaction";
1619 =head2 SquelchMailTo
1621 Similar to Ticket class SquelchMailTo method - returns a list of
1622 transaction's squelched addresses. As transactions are immutable, the
1623 list of squelched recipients cannot be modified after creation.
1629 return () unless $self->CurrentUserCanSee;
1630 return $self->Attributes->Named('SquelchMailTo');
1635 Returns the list of email addresses (as L<Email::Address> objects)
1636 that this transaction would send mail to. There may be duplicates.
1643 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1644 my $action = $scrip->ActionObj->Action;
1645 next unless $action->isa('RT::Action::SendEmail');
1647 foreach my $type (qw(To Cc Bcc)) {
1648 push @recipients, $action->$type();
1652 if ( $self->Rules ) {
1653 for my $rule (@{$self->Rules}) {
1654 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1655 my $data = $rule->{hints}{recipients};
1656 foreach my $type (qw(To Cc Bcc)) {
1657 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1664 =head2 DeferredRecipients($freq, $include_sent )
1666 Takes the following arguments:
1670 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1672 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1676 Returns an array of users who should now receive the notification that
1677 was recorded in this transaction. Returns an empty array if there were
1678 no deferred users, or if $include_sent was not specified and the deferred
1679 notifications have been sent.
1683 sub DeferredRecipients {
1686 my $include_sent = @_? shift : 0;
1688 my $attr = $self->FirstAttribute('DeferredRecipients');
1690 return () unless ($attr);
1692 my $deferred = $attr->Content;
1694 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1698 for my $user (keys %{$deferred->{$freq}}) {
1699 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1700 delete $deferred->{$freq}->{$user}
1703 # Now get our users. Easy.
1705 return keys %{ $deferred->{$freq} };
1710 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1713 'cache_for_sec' => 6000,
1718 =head2 ACLEquivalenceObjects
1720 This method returns a list of objects for which a user's rights also apply
1721 to this Transaction.
1723 This currently only applies to Transaction Custom Fields on Tickets, so we return
1724 the Ticket's Queue and the Ticket.
1726 This method is called from L<RT::Principal/HasRight>.
1730 sub ACLEquivalenceObjects {
1733 return unless $self->ObjectType eq 'RT::Ticket';
1734 my $object = $self->Object;
1735 return $object,$object->QueueObj;
1745 Returns the current value of id.
1746 (In the database, id is stored as int(11).)
1754 Returns the current value of ObjectType.
1755 (In the database, ObjectType is stored as varchar(64).)
1759 =head2 SetObjectType VALUE
1762 Set ObjectType to VALUE.
1763 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1764 (In the database, ObjectType will be stored as a varchar(64).)
1772 Returns the current value of ObjectId.
1773 (In the database, ObjectId is stored as int(11).)
1777 =head2 SetObjectId VALUE
1780 Set ObjectId to VALUE.
1781 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1782 (In the database, ObjectId will be stored as a int(11).)
1790 Returns the current value of TimeTaken.
1791 (In the database, TimeTaken is stored as int(11).)
1795 =head2 SetTimeTaken VALUE
1798 Set TimeTaken to VALUE.
1799 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1800 (In the database, TimeTaken will be stored as a int(11).)
1808 Returns the current value of Type.
1809 (In the database, Type is stored as varchar(20).)
1813 =head2 SetType VALUE
1817 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1818 (In the database, Type will be stored as a varchar(20).)
1826 Returns the current value of Field.
1827 (In the database, Field is stored as varchar(40).)
1831 =head2 SetField VALUE
1835 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1836 (In the database, Field will be stored as a varchar(40).)
1844 Returns the current value of OldValue.
1845 (In the database, OldValue is stored as varchar(255).)
1849 =head2 SetOldValue VALUE
1852 Set OldValue to VALUE.
1853 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1854 (In the database, OldValue will be stored as a varchar(255).)
1862 Returns the current value of NewValue.
1863 (In the database, NewValue is stored as varchar(255).)
1867 =head2 SetNewValue VALUE
1870 Set NewValue to VALUE.
1871 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1872 (In the database, NewValue will be stored as a varchar(255).)
1878 =head2 ReferenceType
1880 Returns the current value of ReferenceType.
1881 (In the database, ReferenceType is stored as varchar(255).)
1885 =head2 SetReferenceType VALUE
1888 Set ReferenceType to VALUE.
1889 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1890 (In the database, ReferenceType will be stored as a varchar(255).)
1898 Returns the current value of OldReference.
1899 (In the database, OldReference is stored as int(11).)
1903 =head2 SetOldReference VALUE
1906 Set OldReference to VALUE.
1907 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1908 (In the database, OldReference will be stored as a int(11).)
1916 Returns the current value of NewReference.
1917 (In the database, NewReference is stored as int(11).)
1921 =head2 SetNewReference VALUE
1924 Set NewReference to VALUE.
1925 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1926 (In the database, NewReference will be stored as a int(11).)
1934 Returns the current value of Data.
1935 (In the database, Data is stored as varchar(255).)
1939 =head2 SetData VALUE
1943 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1944 (In the database, Data will be stored as a varchar(255).)
1952 Returns the current value of Creator.
1953 (In the database, Creator is stored as int(11).)
1961 Returns the current value of Created.
1962 (In the database, Created is stored as datetime.)
1969 sub _CoreAccessible {
1973 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1975 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1977 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1979 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1981 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1983 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1985 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1987 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1989 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1991 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1993 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1995 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1997 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1999 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2004 sub FindDependencies {
2006 my ($walker, $deps) = @_;
2008 $self->SUPER::FindDependencies($walker, $deps);
2010 $deps->Add( out => $self->Object );
2011 $deps->Add( in => $self->Attachments );
2013 my $type = $self->Type;
2014 if ($type eq "CustomField") {
2015 my $cf = RT::CustomField->new( RT->SystemUser );
2016 $cf->Load( $self->Field );
2017 $deps->Add( out => $cf );
2018 } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
2019 for my $field (qw/OldValue NewValue/) {
2020 my $user = RT::User->new( RT->SystemUser );
2021 $user->Load( $self->$field );
2022 $deps->Add( out => $user );
2024 } elsif ($type eq "DelWatcher") {
2025 my $principal = RT::Principal->new( RT->SystemUser );
2026 $principal->Load( $self->OldValue );
2027 $deps->Add( out => $principal->Object );
2028 } elsif ($type eq "AddWatcher") {
2029 my $principal = RT::Principal->new( RT->SystemUser );
2030 $principal->Load( $self->NewValue );
2031 $deps->Add( out => $principal->Object );
2032 } elsif ($type eq "DeleteLink") {
2033 if ($self->OldValue) {
2034 my $base = RT::URI->new( $self->CurrentUser );
2035 $base->FromURI( $self->OldValue );
2036 $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
2038 } elsif ($type eq "AddLink") {
2039 if ($self->NewValue) {
2040 my $base = RT::URI->new( $self->CurrentUser );
2041 $base->FromURI( $self->NewValue );
2042 $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
2044 } elsif ($type eq "Set" and $self->Field eq "Queue") {
2045 for my $field (qw/OldValue NewValue/) {
2046 my $queue = RT::Queue->new( RT->SystemUser );
2047 $queue->Load( $self->$field );
2048 $deps->Add( out => $queue );
2050 } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
2051 my $ticket = RT::Ticket->new( RT->SystemUser );
2052 $ticket->Load( $self->NewValue );
2053 $deps->Add( out => $ticket );
2061 Dependencies => undef,
2064 my $deps = $args{'Dependencies'};
2066 $deps->_PushDependencies(
2067 BaseObject => $self,
2068 Flags => RT::Shredder::Constants::DEPENDS_ON,
2069 TargetObjects => $self->Attachments,
2070 Shredder => $args{'Shredder'}
2073 return $self->SUPER::__DependsOn( %args );
2079 my %store = $self->SUPER::Serialize(@_);
2081 my $type = $store{Type};
2082 if ($type eq "CustomField") {
2083 my $cf = RT::CustomField->new( RT->SystemUser );
2084 $cf->Load( $store{Field} );
2085 $store{Field} = \($cf->UID);
2087 $store{OldReference} = \($self->OldReferenceObject->UID) if $self->OldReference;
2088 $store{NewReference} = \($self->NewReferenceObject->UID) if $self->NewReference;
2089 } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
2090 for my $field (qw/OldValue NewValue/) {
2091 my $user = RT::User->new( RT->SystemUser );
2092 $user->Load( $store{$field} );
2093 $store{$field} = \($user->UID);
2095 } elsif ($type eq "DelWatcher") {
2096 my $principal = RT::Principal->new( RT->SystemUser );
2097 $principal->Load( $store{OldValue} );
2098 $store{OldValue} = \($principal->UID);
2099 } elsif ($type eq "AddWatcher") {
2100 my $principal = RT::Principal->new( RT->SystemUser );
2101 $principal->Load( $store{NewValue} );
2102 $store{NewValue} = \($principal->UID);
2103 } elsif ($type eq "DeleteLink") {
2104 if ($store{OldValue}) {
2105 my $base = RT::URI->new( $self->CurrentUser );
2106 $base->FromURI( $store{OldValue} );
2107 if ($base->Resolver && (my $object = $base->Object)) {
2108 if ($args{serializer}->Observe(object => $object)) {
2109 $store{OldValue} = \($object->UID);
2111 elsif ($args{serializer}{HyperlinkUnmigrated}) {
2112 $store{OldValue} = $base->AsHREF;
2115 $store{OldValue} = "(not migrated)";
2119 } elsif ($type eq "AddLink") {
2120 if ($store{NewValue}) {
2121 my $base = RT::URI->new( $self->CurrentUser );
2122 $base->FromURI( $store{NewValue} );
2123 if ($base->Resolver && (my $object = $base->Object)) {
2124 if ($args{serializer}->Observe(object => $object)) {
2125 $store{NewValue} = \($object->UID);
2127 elsif ($args{serializer}{HyperlinkUnmigrated}) {
2128 $store{NewValue} = $base->AsHREF;
2131 $store{NewValue} = "(not migrated)";
2135 } elsif ($type eq "Set" and $store{Field} eq "Queue") {
2136 for my $field (qw/OldValue NewValue/) {
2137 my $queue = RT::Queue->new( RT->SystemUser );
2138 $queue->Load( $store{$field} );
2139 if ($args{serializer}->Observe(object => $queue)) {
2140 $store{$field} = \($queue->UID);
2143 $store{$field} = "$RT::Organization: " . $queue->Name . " (not migrated)";
2147 } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
2148 my $ticket = RT::Ticket->new( RT->SystemUser );
2149 $ticket->Load( $store{NewValue} );
2150 $store{NewValue} = \($ticket->UID);
2158 my ($importer, $uid, $data) = @_;
2160 if ($data->{Object} and ref $data->{Object}) {
2161 my $on_uid = ${ $data->{Object} };
2162 return if $importer->ShouldSkipTransaction($on_uid);
2165 if ($data->{Type} eq "DeleteLink" and ref $data->{OldValue}) {
2166 my $uid = ${ $data->{OldValue} };
2167 my $obj = $importer->LookupObj( $uid );
2168 $data->{OldValue} = $obj->URI;
2169 } elsif ($data->{Type} eq "AddLink" and ref $data->{NewValue}) {
2170 my $uid = ${ $data->{NewValue} };
2171 my $obj = $importer->LookupObj( $uid );
2172 $data->{NewValue} = $obj->URI;
2175 return $class->SUPER::PreInflate( $importer, $uid, $data );
2178 RT::Base->_ImportOverlays();