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 );
86 use HTML::TreeBuilder;
89 sub Table {'Transactions'}
95 Create a new transaction.
97 This routine should _never_ be called by anything other than RT::Ticket.
98 It should not be called
99 from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
100 Then the unpleasant stuff will start.
102 TODO: Document what gets passed to this
119 ObjectType => 'RT::Ticket',
121 ReferenceType => undef,
122 OldReference => undef,
123 NewReference => undef,
124 SquelchMailTo => undef,
129 $args{ObjectId} ||= $args{Ticket};
131 #if we didn't specify a ticket, we need to bail
132 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
133 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
136 #lets create our transaction
138 Type => $args{'Type'},
139 Data => $args{'Data'},
140 Field => $args{'Field'},
141 OldValue => $args{'OldValue'},
142 NewValue => $args{'NewValue'},
143 Created => $args{'Created'},
144 ObjectType => $args{'ObjectType'},
145 ObjectId => $args{'ObjectId'},
146 ReferenceType => $args{'ReferenceType'},
147 OldReference => $args{'OldReference'},
148 NewReference => $args{'NewReference'},
151 # Parameters passed in during an import that we probably don't want to touch, otherwise
152 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
153 $params{$attr} = $args{$attr} if ($args{$attr});
156 my $id = $self->SUPER::Create(%params);
158 if ( defined $args{'MIMEObj'} ) {
159 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
161 $RT::Logger->error("Couldn't add attachment: $msg");
162 return ( 0, $self->loc("Couldn't add attachment") );
166 # Set up any custom fields passed at creation. Has to happen
169 $self->UpdateCustomFields(%{ $args{'CustomFields'} });
172 Name => 'SquelchMailTo',
173 Content => RT::User->CanonicalizeEmailAddress($_)
174 ) for @{$args{'SquelchMailTo'} || []};
176 #Provide a way to turn off scrips if we need to
177 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
178 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
179 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
181 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
183 $self->{'scrips'}->Prepare(
184 Stage => 'TransactionCreate',
185 Type => $args{'Type'},
186 Ticket => $args{'ObjectId'},
187 Transaction => $self->id,
190 # Entry point of the rule system
191 my $ticket = RT::Ticket->new(RT->SystemUser);
192 $ticket->Load($args{'ObjectId'});
193 my $txn = RT::Transaction->new($RT::SystemUser);
194 $txn->Load($self->id);
196 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
197 Stage => 'TransactionCreate',
198 Type => $args{'Type'},
199 TicketObj => $ticket,
200 TransactionObj => $txn,
203 if ($args{'CommitScrips'} ) {
204 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
205 $self->{'scrips'}->Commit();
206 RT::Ruleset->CommitRules($rules);
210 return ( $id, $self->loc("Transaction Created") );
216 Returns the Scrips object for this transaction.
217 This routine is only useful on a freshly created transaction object.
218 Scrips do not get persisted to the database with transactions.
226 return($self->{'scrips'});
232 Returns the array of Rule objects for this transaction.
233 This routine is only useful on a freshly created transaction object.
234 Rules do not get persisted to the database with transactions.
242 return($self->{'rules'});
249 Delete this transaction. Currently DOES NOT CHECK ACLS
257 $RT::Handle->BeginTransaction();
259 my $attachments = $self->Attachments;
261 while (my $attachment = $attachments->Next) {
262 my ($id, $msg) = $attachment->Delete();
264 $RT::Handle->Rollback();
265 return($id, $self->loc("System Error: [_1]", $msg));
268 my ($id,$msg) = $self->SUPER::Delete();
270 $RT::Handle->Rollback();
271 return($id, $self->loc("System Error: [_1]", $msg));
273 $RT::Handle->Commit();
282 Returns the L<RT::Attachments> object which contains the "top-level" object
283 attachment for this transaction.
290 # XXX: Where is ACL check?
292 unless ( defined $self->{'message'} ) {
294 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
295 $self->{'message'}->Limit(
296 FIELD => 'TransactionId',
299 $self->{'message'}->ChildrenOf(0);
301 $self->{'message'}->GotoFirstItem;
303 return $self->{'message'};
308 =head2 Content PARAMHASH
310 If this transaction has attached mime objects, returns the body of the first
311 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
314 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
315 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
317 If $args{'Type'} is set to C<text/html>, this will return an HTML
318 part of the message, if available. Otherwise it looks for a text/plain
319 part. If $args{'Type'} is missing, it defaults to the value of
320 C<$RT::Transaction::PreferredContentType>, if that's missing too,
328 Type => $PreferredContentType || '',
331 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
336 if ( my $content_obj =
337 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
339 $content = $content_obj->Content ||'';
341 if ( lc $content_obj->ContentType eq 'text/html' ) {
342 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
344 if ($args{Type} ne 'text/html') {
345 my $tree = HTML::TreeBuilder->new_from_content( $content );
346 $content = HTML::FormatText->new(
354 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
355 if ($args{Type} eq 'text/html') {
356 # Extremely simple text->html converter
357 $content =~ s/&/&/g;
358 $content =~ s/</</g;
359 $content =~ s/>/>/g;
360 $content = "<pre>$content</pre>";
365 # If all else fails, return a message that we couldn't find any content
367 $content = $self->loc('This transaction appears to have no content');
370 if ( $args{'Quote'} ) {
371 $content = $self->ApplyQuoteWrap(content => $content,
372 cols => $args{'Wrap'} );
374 $content = $self->QuoteHeader . "\n$content\n\n";
382 Returns text prepended to content when transaction is quoted
383 (see C<Quote> argument in L</Content>). By default returns
384 localized "On <date> <user name> wrote:\n".
390 return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
393 =head2 ApplyQuoteWrap PARAMHASH
395 Wrapper to calculate wrap criteria and apply quote wrapping if needed.
402 my $content = $args{content};
404 # What's the longest line like?
406 foreach ( split ( /\n/, $args{content} ) ) {
407 $max = length if length > $max;
411 require Text::Quoted;
412 require Text::Wrapper;
414 my $structure = Text::Quoted::extract($args{content});
415 $content = $self->QuoteWrap(content_ref => $structure,
420 $content =~ s/^/> /gm; # use regex since string might be multi-line
424 =head2 QuoteWrap PARAMHASH
426 Wrap the contents of transactions based on Wrap settings, maintaining
427 the quote character from the original.
434 my $ref = $args{content_ref};
437 if ( ref $ref eq 'ARRAY' ){
438 foreach my $array (@$ref){
439 $final_string .= $self->QuoteWrap(content_ref => $array,
444 elsif ( ref $ref eq 'HASH' ){
445 return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
447 my $col = $args{cols} - (length $ref->{quoter});
448 my $wrapper = Text::Wrapper->new( columns => $col );
450 # Wrap on individual lines to honor incoming line breaks
451 # Otherwise deliberate separate lines (like a list or a sig)
452 # all get combined incorrectly into single paragraphs.
454 my @lines = split /\n/, $ref->{text};
455 my $wrap = join '', map { $wrapper->wrap($_) } @lines;
456 my $quoter = $ref->{quoter};
458 # Only add the space if actually quoting
459 $quoter .= ' ' if length $quoter;
460 $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line
465 $RT::Logger->warning("Can't apply quoting with $ref");
468 return $final_string;
474 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
481 if (my $attach = $self->Attachments->First) {
482 return $attach->Addresses;
494 Returns the RT::Attachment object which contains the content for this Transaction
501 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
503 # If we don't have any content, return undef now.
504 # Get the set of toplevel attachments to this transaction.
506 my $Attachment = $args{'Attachment'};
508 $Attachment ||= $self->Attachments->First;
510 return undef unless ($Attachment);
512 # If it's a textual part, just return the body.
513 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
514 return ($Attachment);
517 # If it's a multipart object, first try returning the first part with preferred
518 # MIME type ('text/plain' by default).
520 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
521 my $kids = $Attachment->Children;
522 while (my $child = $kids->Next) {
523 my $ret = $self->ContentObj(%args, Attachment => $child);
524 return $ret if ($ret);
527 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
529 my $plain_parts = $Attachment->Children;
530 $plain_parts->ContentType( VALUE => $args{Type} );
531 $plain_parts->LimitNotEmpty;
533 # If we actully found a part, return its content
534 if ( my $first = $plain_parts->First ) {
539 # If that fails, return the first textual part which has some content.
540 my $all_parts = $self->Attachments;
541 while ( my $part = $all_parts->Next ) {
542 next unless RT::I18N::IsTextualContentType($part->ContentType)
548 # We found no content. suck
556 If this transaction has attached mime objects, returns the first one's subject
557 Otherwise, returns null
563 return undef unless my $first = $self->Attachments->First;
564 return $first->Subject;
571 Returns all the RT::Attachment objects which are attached
572 to this transaction. Takes an optional parameter, which is
573 a ContentType that Attachments should be restricted to.
580 if ( $self->{'attachments'} ) {
581 $self->{'attachments'}->GotoFirstItem;
582 return $self->{'attachments'};
585 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
587 unless ( $self->CurrentUserCanSee ) {
588 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
589 return $self->{'attachments'};
592 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
594 # Get the self->{'attachments'} in the order they're put into
595 # the database. Arguably, we should be returning a tree
596 # of self->{'attachments'}, not a set...but no current app seems to need
599 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
601 return $self->{'attachments'};
608 A private method used to attach a mime object to this transaction.
614 my $MIMEObject = shift;
616 unless ( defined $MIMEObject ) {
617 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
618 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
621 my $Attachment = RT::Attachment->new( $self->CurrentUser );
622 my ($id, $msg) = $Attachment->Create(
623 TransactionId => $self->Id,
624 Attachment => $MIMEObject
626 return ( $Attachment, $msg || $self->loc("Attachment created") );
634 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
635 # since it has less information available without looking to it's parent
636 # transaction. Check ACLs here before we go any further.
637 return unless $self->CurrentUserCanSee;
639 my $attachments = RT::Attachments->new( $self->CurrentUser );
640 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
641 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
642 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
643 $attachments->RowsPerPage(1);
645 my $top = $attachments->First;
648 my $entity = MIME::Entity->build(
649 Type => 'message/rfc822',
650 Description => 'transaction ' . $self->id,
651 Data => $top->ContentAsMIME(Children => 1)->as_string,
661 Returns a text string which describes this transaction
668 unless ( $self->CurrentUserCanSee ) {
669 return ( $self->loc("Permission Denied") );
672 unless ( defined $self->Type ) {
673 return ( $self->loc("No transaction type specified"));
676 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
681 =head2 BriefDescription
683 Returns a text string which briefly describes this transaction
687 sub BriefDescription {
690 unless ( $self->CurrentUserCanSee ) {
691 return ( $self->loc("Permission Denied") );
694 my $type = $self->Type; #cache this, rather than calling it 30 times
696 unless ( defined $type ) {
697 return $self->loc("No transaction type specified");
700 my $obj_type = $self->FriendlyObjectType;
702 if ( $type eq 'Create' ) {
703 return ( $self->loc( "[_1] created", $obj_type ) );
705 elsif ( $type eq 'Enabled' ) {
706 return ( $self->loc( "[_1] enabled", $obj_type ) );
708 elsif ( $type eq 'Disabled' ) {
709 return ( $self->loc( "[_1] disabled", $obj_type ) );
711 elsif ( $type =~ /Status/ ) {
712 if ( $self->Field eq 'Status' ) {
713 if ( $self->NewValue eq 'deleted' ) {
714 return ( $self->loc( "[_1] deleted", $obj_type ) );
717 my $canon = $self->Object->can("QueueObj")
718 ? sub { $self->Object->QueueObj->Lifecycle->CanonicalCase(@_) }
719 : sub { return $_[0] };
722 "Status changed from [_1] to [_2]",
723 "'" . $self->loc( $canon->($self->OldValue) ) . "'",
724 "'" . $self->loc( $canon->($self->NewValue) ) . "'"
732 my $no_value = $self->loc("(no value)");
735 "[_1] changed from [_2] to [_3]",
737 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
738 "'" . $self->NewValue . "'"
742 elsif ( $type =~ /SystemError/ ) {
743 return $self->loc("System error");
745 elsif ( $type =~ /Forward Transaction/ ) {
746 return $self->loc( "Forwarded Transaction #[_1] to [_2]",
747 $self->Field, $self->Data );
749 elsif ( $type =~ /Forward Ticket/ ) {
750 return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
753 if ( my $code = $_BriefDescriptions{$type} ) {
754 return $code->($self);
758 "Default: [_1]/[_2] changed from [_3] to [_4]",
763 ? "'" . $self->OldValue . "'"
764 : $self->loc("(no value)")
766 "'" . $self->NewValue . "'"
770 %_BriefDescriptions = (
771 CommentEmailRecord => sub {
773 return $self->loc("Outgoing email about a comment recorded");
777 return $self->loc("Outgoing email recorded");
781 return $self->loc("Correspondence added");
785 return $self->loc("Comments added");
789 my $field = $self->loc('CustomField');
792 if ( $self->Field ) {
793 $cf = RT::CustomField->new( $self->CurrentUser );
794 $cf->SetContextObject( $self->Object );
795 $cf->Load( $self->Field );
796 $field = $cf->Name();
797 $field = $self->loc('a custom field') if !defined($field);
800 my $new = $self->NewValue;
801 my $old = $self->OldValue;
805 if ( $cf->Type eq 'DateTime' ) {
807 my $date = RT::Date->new( $self->CurrentUser );
808 $date->Set( Format => 'ISO', Value => $old );
809 $old = $date->AsString;
813 my $date = RT::Date->new( $self->CurrentUser );
814 $date->Set( Format => 'ISO', Value => $new );
815 $new = $date->AsString;
818 elsif ( $cf->Type eq 'Date' ) {
820 my $date = RT::Date->new( $self->CurrentUser );
826 $old = $date->AsString( Time => 0, Timezone => 'UTC' );
830 my $date = RT::Date->new( $self->CurrentUser );
836 $new = $date->AsString( Time => 0, Timezone => 'UTC' );
841 if ( !defined($old) || $old eq '' ) {
842 return $self->loc("[_1] [_2] added", $field, $new);
844 elsif ( !defined($new) || $new eq '' ) {
845 return $self->loc("[_1] [_2] deleted", $field, $old);
848 return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
853 return $self->loc("Untaken");
857 return $self->loc("Taken");
861 my $Old = RT::User->new( $self->CurrentUser );
862 $Old->Load( $self->OldValue );
863 my $New = RT::User->new( $self->CurrentUser );
864 $New->Load( $self->NewValue );
866 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
870 my $Old = RT::User->new( $self->CurrentUser );
871 $Old->Load( $self->OldValue );
872 return $self->loc("Stolen from [_1]", $Old->Name);
876 my $New = RT::User->new( $self->CurrentUser );
877 $New->Load( $self->NewValue );
878 return $self->loc( "Given to [_1]", $New->Name );
882 my $principal = RT::Principal->new($self->CurrentUser);
883 $principal->Load($self->NewValue);
884 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
888 my $principal = RT::Principal->new($self->CurrentUser);
889 $principal->Load($self->OldValue);
890 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
894 return $self->loc( "Subject changed to [_1]", $self->Data );
899 if ( $self->NewValue ) {
900 my $URI = RT::URI->new( $self->CurrentUser );
901 if ( $URI->FromURI( $self->NewValue ) ) {
902 $value = $URI->Resolver->AsString;
905 $value = $self->NewValue;
907 if ( $self->Field eq 'DependsOn' ) {
908 return $self->loc( "Dependency on [_1] added", $value );
910 elsif ( $self->Field eq 'DependedOnBy' ) {
911 return $self->loc( "Dependency by [_1] added", $value );
914 elsif ( $self->Field eq 'RefersTo' ) {
915 return $self->loc( "Reference to [_1] added", $value );
917 elsif ( $self->Field eq 'ReferredToBy' ) {
918 return $self->loc( "Reference by [_1] added", $value );
920 elsif ( $self->Field eq 'MemberOf' ) {
921 return $self->loc( "Membership in [_1] added", $value );
923 elsif ( $self->Field eq 'HasMember' ) {
924 return $self->loc( "Member [_1] added", $value );
926 elsif ( $self->Field eq 'MergedInto' ) {
927 return $self->loc( "Merged into [_1]", $value );
931 return ( $self->Data );
937 if ( $self->OldValue ) {
938 my $URI = RT::URI->new( $self->CurrentUser );
939 if ( $URI->FromURI( $self->OldValue ) ){
940 $value = $URI->Resolver->AsString;
943 $value = $self->OldValue;
946 if ( $self->Field eq 'DependsOn' ) {
947 return $self->loc( "Dependency on [_1] deleted", $value );
949 elsif ( $self->Field eq 'DependedOnBy' ) {
950 return $self->loc( "Dependency by [_1] deleted", $value );
953 elsif ( $self->Field eq 'RefersTo' ) {
954 return $self->loc( "Reference to [_1] deleted", $value );
956 elsif ( $self->Field eq 'ReferredToBy' ) {
957 return $self->loc( "Reference by [_1] deleted", $value );
959 elsif ( $self->Field eq 'MemberOf' ) {
960 return $self->loc( "Membership in [_1] deleted", $value );
962 elsif ( $self->Field eq 'HasMember' ) {
963 return $self->loc( "Member [_1] deleted", $value );
967 return ( $self->Data );
972 if ( $self->Field eq 'Told' ) {
973 my $t1 = RT::Date->new($self->CurrentUser);
974 $t1->Set(Format => 'ISO', Value => $self->NewValue);
975 my $t2 = RT::Date->new($self->CurrentUser);
976 $t2->Set(Format => 'ISO', Value => $self->OldValue);
977 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
980 return $self->loc( "[_1] changed from [_2] to [_3]",
981 $self->loc($self->Field),
982 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
987 if ( $self->Field eq 'Password' ) {
988 return $self->loc('Password changed');
990 elsif ( $self->Field eq 'Queue' ) {
991 my $q1 = RT::Queue->new( $self->CurrentUser );
992 $q1->Load( $self->OldValue );
993 my $q2 = RT::Queue->new( $self->CurrentUser );
994 $q2->Load( $self->NewValue );
995 return $self->loc("[_1] changed from [_2] to [_3]",
996 $self->loc($self->Field) , $q1->Name , $q2->Name);
999 # Write the date/time change at local time:
1000 elsif ($self->Field =~ /Due|Starts|Started|Told|WillResolve/) {
1001 my $t1 = RT::Date->new($self->CurrentUser);
1002 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1003 my $t2 = RT::Date->new($self->CurrentUser);
1004 $t2->Set(Format => 'ISO', Value => $self->OldValue);
1005 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
1007 elsif ( $self->Field eq 'Owner' ) {
1008 my $Old = RT::User->new( $self->CurrentUser );
1009 $Old->Load( $self->OldValue );
1010 my $New = RT::User->new( $self->CurrentUser );
1011 $New->Load( $self->NewValue );
1013 if ( $Old->id == RT->Nobody->id ) {
1014 if ( $New->id == $self->Creator ) {
1015 return $self->loc("Taken");
1018 return $self->loc( "Given to [_1]", $New->Name );
1022 if ( $New->id == $self->Creator ) {
1023 return $self->loc("Stolen from [_1]", $Old->Name);
1025 elsif ( $Old->id == $self->Creator ) {
1026 if ( $New->id == RT->Nobody->id ) {
1027 return $self->loc("Untaken");
1030 return $self->loc( "Given to [_1]", $New->Name );
1035 "Owner forcibly changed from [_1] to [_2]",
1036 $Old->Name, $New->Name );
1041 return $self->loc( "[_1] changed from [_2] to [_3]",
1042 $self->loc($self->Field),
1043 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
1044 ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)")));
1047 PurgeTransaction => sub {
1049 return $self->loc("Transaction [_1] purged", $self->Data);
1051 AddReminder => sub {
1053 my $ticket = RT::Ticket->new($self->CurrentUser);
1054 $ticket->Load($self->NewValue);
1055 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
1057 OpenReminder => sub {
1059 my $ticket = RT::Ticket->new($self->CurrentUser);
1060 $ticket->Load($self->NewValue);
1061 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
1064 ResolveReminder => sub {
1066 my $ticket = RT::Ticket->new($self->CurrentUser);
1067 $ticket->Load($self->NewValue);
1068 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
1079 Returns true if the creator of the transaction is a requestor of the ticket.
1080 Returns false otherwise
1086 $self->ObjectType eq 'RT::Ticket' or return undef;
1087 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
1092 sub _OverlayAccessible {
1095 ObjectType => { public => 1},
1096 ObjectId => { public => 1},
1106 return ( 0, $self->loc('Transactions are immutable') );
1113 Takes the name of a table column.
1114 Returns its value as a string, if the user passes an ACL check
1122 #if the field is public, return it.
1123 if ( $self->_Accessible( $field, 'public' ) ) {
1124 return $self->SUPER::_Value( $field );
1127 unless ( $self->CurrentUserCanSee ) {
1131 return $self->SUPER::_Value( $field );
1136 =head2 CurrentUserHasRight RIGHT
1138 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1143 sub CurrentUserHasRight {
1146 return $self->CurrentUser->HasRight(
1148 Object => $self->Object
1152 =head2 CurrentUserCanSee
1154 Returns true if current user has rights to see this particular transaction.
1156 This fact depends on type of the transaction, type of an object the transaction
1157 is attached to and may be other conditions, so this method is prefered over
1158 custom implementations.
1162 sub CurrentUserCanSee {
1165 # If it's a comment, we need to be extra special careful
1166 my $type = $self->__Value('Type');
1167 if ( $type eq 'Comment' ) {
1168 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1172 elsif ( $type eq 'CommentEmailRecord' ) {
1173 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1174 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1178 elsif ( $type eq 'EmailRecord' ) {
1179 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1183 # Make sure the user can see the custom field before showing that it changed
1184 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1185 my $cf = RT::CustomField->new( $self->CurrentUser );
1186 $cf->SetContextObject( $self->Object );
1187 $cf->Load( $cf_id );
1188 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1191 # Transactions that might have changed the ->Object's visibility to
1192 # the current user are marked readable
1193 return 1 if $self->{ _object_is_readable };
1195 # Defer to the object in question
1196 return $self->Object->CurrentUserCanSee("Transaction");
1202 return $self->ObjectId;
1207 return $self->Object;
1212 if ( my $type = $self->__Value('ReferenceType')
1213 and my $id = $self->__Value('OldReference') )
1215 my $Object = $type->new($self->CurrentUser);
1216 $Object->Load( $id );
1217 return $Object->Content;
1220 return $self->_Value('OldValue');
1226 if ( my $type = $self->__Value('ReferenceType')
1227 and my $id = $self->__Value('NewReference') )
1229 my $Object = $type->new($self->CurrentUser);
1230 $Object->Load( $id );
1231 return $Object->Content;
1234 return $self->_Value('NewValue');
1240 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1241 $Object->Load($self->__Value('ObjectId'));
1245 sub FriendlyObjectType {
1247 my $type = $self->ObjectType or return undef;
1249 return $self->loc($type);
1252 =head2 UpdateCustomFields
1256 CustomField-<<Id>> => Value
1259 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1260 this transaction's custom fields
1264 sub UpdateCustomFields {
1268 # This method used to have an API that took a hash of a single
1269 # value "ARGSRef", which was a reference to a hash of arguments.
1270 # This was insane. The next few lines of code preserve that API
1271 # while giving us something saner.
1273 # TODO: 3.6: DEPRECATE OLD API
1277 if ($args{'ARGSRef'}) {
1278 $args = $args{ARGSRef};
1283 foreach my $arg ( keys %$args ) {
1286 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1287 next if $arg =~ /-Magic$/;
1288 next if $arg =~ /-TimeUnits$/;
1290 my $values = $args->{$arg};
1292 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1294 next unless (defined($value) && length($value));
1295 $self->_AddCustomFieldValue(
1298 RecordTransaction => 0,
1304 =head2 LoadCustomFieldByIdentifier
1306 Finds and returns the custom field of the given name for the
1307 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1308 look for queue-specific CFs before global ones.
1312 sub LoadCustomFieldByIdentifier {
1316 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1317 if ref $field or $field =~ /^\d+$/;
1319 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1320 unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1322 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1323 $CFs->SetContextObject( $self->Object );
1324 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1325 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1326 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1327 return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1330 =head2 CustomFieldLookupType
1332 Returns the RT::Transaction lookup type, which can
1333 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1338 sub CustomFieldLookupType {
1339 "RT::Queue-RT::Ticket-RT::Transaction";
1343 =head2 SquelchMailTo
1345 Similar to Ticket class SquelchMailTo method - returns a list of
1346 transaction's squelched addresses. As transactions are immutable, the
1347 list of squelched recipients cannot be modified after creation.
1353 return () unless $self->CurrentUserCanSee;
1354 return $self->Attributes->Named('SquelchMailTo');
1359 Returns the list of email addresses (as L<Email::Address> objects)
1360 that this transaction would send mail to. There may be duplicates.
1367 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1368 my $action = $scrip->ActionObj->Action;
1369 next unless $action->isa('RT::Action::SendEmail');
1371 foreach my $type (qw(To Cc Bcc)) {
1372 push @recipients, $action->$type();
1376 if ( $self->Rules ) {
1377 for my $rule (@{$self->Rules}) {
1378 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1379 my $data = $rule->{hints}{recipients};
1380 foreach my $type (qw(To Cc Bcc)) {
1381 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1388 =head2 DeferredRecipients($freq, $include_sent )
1390 Takes the following arguments:
1394 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1396 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1400 Returns an array of users who should now receive the notification that
1401 was recorded in this transaction. Returns an empty array if there were
1402 no deferred users, or if $include_sent was not specified and the deferred
1403 notifications have been sent.
1407 sub DeferredRecipients {
1410 my $include_sent = @_? shift : 0;
1412 my $attr = $self->FirstAttribute('DeferredRecipients');
1414 return () unless ($attr);
1416 my $deferred = $attr->Content;
1418 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1422 for my $user (keys %{$deferred->{$freq}}) {
1423 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1424 delete $deferred->{$freq}->{$user}
1427 # Now get our users. Easy.
1429 return keys %{ $deferred->{$freq} };
1434 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1437 'cache_for_sec' => 6000,
1442 =head2 ACLEquivalenceObjects
1444 This method returns a list of objects for which a user's rights also apply
1445 to this Transaction.
1447 This currently only applies to Transaction Custom Fields on Tickets, so we return
1448 the Ticket's Queue and the Ticket.
1450 This method is called from L<RT::Principal/HasRight>.
1454 sub ACLEquivalenceObjects {
1457 return unless $self->ObjectType eq 'RT::Ticket';
1458 my $object = $self->Object;
1459 return $object,$object->QueueObj;
1469 Returns the current value of id.
1470 (In the database, id is stored as int(11).)
1478 Returns the current value of ObjectType.
1479 (In the database, ObjectType is stored as varchar(64).)
1483 =head2 SetObjectType VALUE
1486 Set ObjectType to VALUE.
1487 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1488 (In the database, ObjectType will be stored as a varchar(64).)
1496 Returns the current value of ObjectId.
1497 (In the database, ObjectId is stored as int(11).)
1501 =head2 SetObjectId VALUE
1504 Set ObjectId to VALUE.
1505 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1506 (In the database, ObjectId will be stored as a int(11).)
1514 Returns the current value of TimeTaken.
1515 (In the database, TimeTaken is stored as int(11).)
1519 =head2 SetTimeTaken VALUE
1522 Set TimeTaken to VALUE.
1523 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1524 (In the database, TimeTaken will be stored as a int(11).)
1532 Returns the current value of Type.
1533 (In the database, Type is stored as varchar(20).)
1537 =head2 SetType VALUE
1541 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1542 (In the database, Type will be stored as a varchar(20).)
1550 Returns the current value of Field.
1551 (In the database, Field is stored as varchar(40).)
1555 =head2 SetField VALUE
1559 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1560 (In the database, Field will be stored as a varchar(40).)
1568 Returns the current value of OldValue.
1569 (In the database, OldValue is stored as varchar(255).)
1573 =head2 SetOldValue VALUE
1576 Set OldValue to VALUE.
1577 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1578 (In the database, OldValue will be stored as a varchar(255).)
1586 Returns the current value of NewValue.
1587 (In the database, NewValue is stored as varchar(255).)
1591 =head2 SetNewValue VALUE
1594 Set NewValue to VALUE.
1595 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1596 (In the database, NewValue will be stored as a varchar(255).)
1602 =head2 ReferenceType
1604 Returns the current value of ReferenceType.
1605 (In the database, ReferenceType is stored as varchar(255).)
1609 =head2 SetReferenceType VALUE
1612 Set ReferenceType to VALUE.
1613 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1614 (In the database, ReferenceType will be stored as a varchar(255).)
1622 Returns the current value of OldReference.
1623 (In the database, OldReference is stored as int(11).)
1627 =head2 SetOldReference VALUE
1630 Set OldReference to VALUE.
1631 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1632 (In the database, OldReference will be stored as a int(11).)
1640 Returns the current value of NewReference.
1641 (In the database, NewReference is stored as int(11).)
1645 =head2 SetNewReference VALUE
1648 Set NewReference to VALUE.
1649 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1650 (In the database, NewReference will be stored as a int(11).)
1658 Returns the current value of Data.
1659 (In the database, Data is stored as varchar(255).)
1663 =head2 SetData VALUE
1667 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1668 (In the database, Data will be stored as a varchar(255).)
1676 Returns the current value of Creator.
1677 (In the database, Creator is stored as int(11).)
1685 Returns the current value of Created.
1686 (In the database, Created is stored as datetime.)
1693 sub _CoreAccessible {
1697 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1699 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1701 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1703 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1705 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1707 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1709 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1711 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1713 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1715 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1717 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1719 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1721 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1723 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1728 RT::Base->_ImportOverlays();