1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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'} ) {
372 # What's the longest line like?
374 foreach ( split ( /\n/, $content ) ) {
375 $max = length if length > $max;
378 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
379 require Text::Wrapper;
380 my $wrapper = Text::Wrapper->new(
381 columns => $args{'Wrap'},
382 body_start => ( $max > 70 * 3 ? ' ' : '' ),
385 $content = $wrapper->wrap($content);
388 $content =~ s/^/> /gm;
389 $content = $self->QuoteHeader . "\n$content\n\n";
397 Returns text prepended to content when transaction is quoted
398 (see C<Quote> argument in L</Content>). By default returns
399 localized "On <date> <user name> wrote:\n".
405 return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
411 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
418 if (my $attach = $self->Attachments->First) {
419 return $attach->Addresses;
431 Returns the RT::Attachment object which contains the content for this Transaction
438 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
440 # If we don't have any content, return undef now.
441 # Get the set of toplevel attachments to this transaction.
443 my $Attachment = $args{'Attachment'};
445 $Attachment ||= $self->Attachments->First;
447 return undef unless ($Attachment);
449 # If it's a textual part, just return the body.
450 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
451 return ($Attachment);
454 # If it's a multipart object, first try returning the first part with preferred
455 # MIME type ('text/plain' by default).
457 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
458 my $kids = $Attachment->Children;
459 while (my $child = $kids->Next) {
460 my $ret = $self->ContentObj(%args, Attachment => $child);
461 return $ret if ($ret);
464 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
466 my $plain_parts = $Attachment->Children;
467 $plain_parts->ContentType( VALUE => $args{Type} );
468 $plain_parts->LimitNotEmpty;
470 # If we actully found a part, return its content
471 if ( my $first = $plain_parts->First ) {
476 # If that fails, return the first textual part which has some content.
477 my $all_parts = $self->Attachments;
478 while ( my $part = $all_parts->Next ) {
479 next unless RT::I18N::IsTextualContentType($part->ContentType)
485 # We found no content. suck
493 If this transaction has attached mime objects, returns the first one's subject
494 Otherwise, returns null
500 return undef unless my $first = $self->Attachments->First;
501 return $first->Subject;
508 Returns all the RT::Attachment objects which are attached
509 to this transaction. Takes an optional parameter, which is
510 a ContentType that Attachments should be restricted to.
517 if ( $self->{'attachments'} ) {
518 $self->{'attachments'}->GotoFirstItem;
519 return $self->{'attachments'};
522 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
524 unless ( $self->CurrentUserCanSee ) {
525 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
526 return $self->{'attachments'};
529 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
531 # Get the self->{'attachments'} in the order they're put into
532 # the database. Arguably, we should be returning a tree
533 # of self->{'attachments'}, not a set...but no current app seems to need
536 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
538 return $self->{'attachments'};
545 A private method used to attach a mime object to this transaction.
551 my $MIMEObject = shift;
553 unless ( defined $MIMEObject ) {
554 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
555 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
558 my $Attachment = RT::Attachment->new( $self->CurrentUser );
559 my ($id, $msg) = $Attachment->Create(
560 TransactionId => $self->Id,
561 Attachment => $MIMEObject
563 return ( $Attachment, $msg || $self->loc("Attachment created") );
571 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
572 # since it has less information available without looking to it's parent
573 # transaction. Check ACLs here before we go any further.
574 return unless $self->CurrentUserCanSee;
576 my $attachments = RT::Attachments->new( $self->CurrentUser );
577 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
578 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
579 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
580 $attachments->RowsPerPage(1);
582 my $top = $attachments->First;
585 my $entity = MIME::Entity->build(
586 Type => 'message/rfc822',
587 Description => 'transaction ' . $self->id,
588 Data => $top->ContentAsMIME(Children => 1)->as_string,
598 Returns a text string which describes this transaction
605 unless ( $self->CurrentUserCanSee ) {
606 return ( $self->loc("Permission Denied") );
609 unless ( defined $self->Type ) {
610 return ( $self->loc("No transaction type specified"));
613 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
618 =head2 BriefDescription
620 Returns a text string which briefly describes this transaction
624 sub BriefDescription {
627 unless ( $self->CurrentUserCanSee ) {
628 return ( $self->loc("Permission Denied") );
631 my $type = $self->Type; #cache this, rather than calling it 30 times
633 unless ( defined $type ) {
634 return $self->loc("No transaction type specified");
637 my $obj_type = $self->FriendlyObjectType;
639 if ( $type eq 'Create' ) {
640 return ( $self->loc( "[_1] created", $obj_type ) );
642 elsif ( $type eq 'Enabled' ) {
643 return ( $self->loc( "[_1] enabled", $obj_type ) );
645 elsif ( $type eq 'Disabled' ) {
646 return ( $self->loc( "[_1] disabled", $obj_type ) );
648 elsif ( $type =~ /Status/ ) {
649 if ( $self->Field eq 'Status' ) {
650 if ( $self->NewValue eq 'deleted' ) {
651 return ( $self->loc( "[_1] deleted", $obj_type ) );
654 my $canon = $self->Object->can("QueueObj")
655 ? sub { $self->Object->QueueObj->Lifecycle->CanonicalCase(@_) }
656 : sub { return $_[0] };
659 "Status changed from [_1] to [_2]",
660 "'" . $self->loc( $canon->($self->OldValue) ) . "'",
661 "'" . $self->loc( $canon->($self->NewValue) ) . "'"
669 my $no_value = $self->loc("(no value)");
672 "[_1] changed from [_2] to [_3]",
674 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
675 "'" . $self->NewValue . "'"
679 elsif ( $type =~ /SystemError/ ) {
680 return $self->loc("System error");
682 elsif ( $type =~ /Forward Transaction/ ) {
683 return $self->loc( "Forwarded Transaction #[_1] to [_2]",
684 $self->Field, $self->Data );
686 elsif ( $type =~ /Forward Ticket/ ) {
687 return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
690 if ( my $code = $_BriefDescriptions{$type} ) {
691 return $code->($self);
695 "Default: [_1]/[_2] changed from [_3] to [_4]",
700 ? "'" . $self->OldValue . "'"
701 : $self->loc("(no value)")
703 "'" . $self->NewValue . "'"
707 %_BriefDescriptions = (
708 CommentEmailRecord => sub {
710 return $self->loc("Outgoing email about a comment recorded");
714 return $self->loc("Outgoing email recorded");
718 return $self->loc("Correspondence added");
722 return $self->loc("Comments added");
726 my $field = $self->loc('CustomField');
728 if ( $self->Field ) {
729 my $cf = RT::CustomField->new( $self->CurrentUser );
730 $cf->SetContextObject( $self->Object );
731 $cf->Load( $self->Field );
732 $field = $cf->Name();
733 $field = $self->loc('a custom field') if !defined($field);
736 my $new = $self->NewValue;
737 my $old = $self->OldValue;
739 if ( !defined($old) || $old eq '' ) {
740 return $self->loc("[_1] [_2] added", $field, $new);
742 elsif ( !defined($new) || $new eq '' ) {
743 return $self->loc("[_1] [_2] deleted", $field, $old);
746 return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
751 return $self->loc("Untaken");
755 return $self->loc("Taken");
759 my $Old = RT::User->new( $self->CurrentUser );
760 $Old->Load( $self->OldValue );
761 my $New = RT::User->new( $self->CurrentUser );
762 $New->Load( $self->NewValue );
764 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
768 my $Old = RT::User->new( $self->CurrentUser );
769 $Old->Load( $self->OldValue );
770 return $self->loc("Stolen from [_1]", $Old->Name);
774 my $New = RT::User->new( $self->CurrentUser );
775 $New->Load( $self->NewValue );
776 return $self->loc( "Given to [_1]", $New->Name );
780 my $principal = RT::Principal->new($self->CurrentUser);
781 $principal->Load($self->NewValue);
782 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
786 my $principal = RT::Principal->new($self->CurrentUser);
787 $principal->Load($self->OldValue);
788 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
792 return $self->loc( "Subject changed to [_1]", $self->Data );
797 if ( $self->NewValue ) {
798 my $URI = RT::URI->new( $self->CurrentUser );
799 if ( $URI->FromURI( $self->NewValue ) ) {
800 $value = $URI->Resolver->AsString;
803 $value = $self->NewValue;
805 if ( $self->Field eq 'DependsOn' ) {
806 return $self->loc( "Dependency on [_1] added", $value );
808 elsif ( $self->Field eq 'DependedOnBy' ) {
809 return $self->loc( "Dependency by [_1] added", $value );
812 elsif ( $self->Field eq 'RefersTo' ) {
813 return $self->loc( "Reference to [_1] added", $value );
815 elsif ( $self->Field eq 'ReferredToBy' ) {
816 return $self->loc( "Reference by [_1] added", $value );
818 elsif ( $self->Field eq 'MemberOf' ) {
819 return $self->loc( "Membership in [_1] added", $value );
821 elsif ( $self->Field eq 'HasMember' ) {
822 return $self->loc( "Member [_1] added", $value );
824 elsif ( $self->Field eq 'MergedInto' ) {
825 return $self->loc( "Merged into [_1]", $value );
829 return ( $self->Data );
835 if ( $self->OldValue ) {
836 my $URI = RT::URI->new( $self->CurrentUser );
837 if ( $URI->FromURI( $self->OldValue ) ){
838 $value = $URI->Resolver->AsString;
841 $value = $self->OldValue;
844 if ( $self->Field eq 'DependsOn' ) {
845 return $self->loc( "Dependency on [_1] deleted", $value );
847 elsif ( $self->Field eq 'DependedOnBy' ) {
848 return $self->loc( "Dependency by [_1] deleted", $value );
851 elsif ( $self->Field eq 'RefersTo' ) {
852 return $self->loc( "Reference to [_1] deleted", $value );
854 elsif ( $self->Field eq 'ReferredToBy' ) {
855 return $self->loc( "Reference by [_1] deleted", $value );
857 elsif ( $self->Field eq 'MemberOf' ) {
858 return $self->loc( "Membership in [_1] deleted", $value );
860 elsif ( $self->Field eq 'HasMember' ) {
861 return $self->loc( "Member [_1] deleted", $value );
865 return ( $self->Data );
870 if ( $self->Field eq 'Told' ) {
871 my $t1 = RT::Date->new($self->CurrentUser);
872 $t1->Set(Format => 'ISO', Value => $self->NewValue);
873 my $t2 = RT::Date->new($self->CurrentUser);
874 $t2->Set(Format => 'ISO', Value => $self->OldValue);
875 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
878 return $self->loc( "[_1] changed from [_2] to [_3]",
879 $self->loc($self->Field),
880 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
885 if ( $self->Field eq 'Password' ) {
886 return $self->loc('Password changed');
888 elsif ( $self->Field eq 'Queue' ) {
889 my $q1 = RT::Queue->new( $self->CurrentUser );
890 $q1->Load( $self->OldValue );
891 my $q2 = RT::Queue->new( $self->CurrentUser );
892 $q2->Load( $self->NewValue );
893 return $self->loc("[_1] changed from [_2] to [_3]",
894 $self->loc($self->Field) , $q1->Name , $q2->Name);
897 # Write the date/time change at local time:
898 elsif ($self->Field =~ /Due|Starts|Started|Told|WillResolve/) {
899 my $t1 = RT::Date->new($self->CurrentUser);
900 $t1->Set(Format => 'ISO', Value => $self->NewValue);
901 my $t2 = RT::Date->new($self->CurrentUser);
902 $t2->Set(Format => 'ISO', Value => $self->OldValue);
903 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
905 elsif ( $self->Field eq 'Owner' ) {
906 my $Old = RT::User->new( $self->CurrentUser );
907 $Old->Load( $self->OldValue );
908 my $New = RT::User->new( $self->CurrentUser );
909 $New->Load( $self->NewValue );
911 if ( $Old->id == RT->Nobody->id ) {
912 if ( $New->id == $self->Creator ) {
913 return $self->loc("Taken");
916 return $self->loc( "Given to [_1]", $New->Name );
920 if ( $New->id == $self->Creator ) {
921 return $self->loc("Stolen from [_1]", $Old->Name);
923 elsif ( $Old->id == $self->Creator ) {
924 if ( $New->id == RT->Nobody->id ) {
925 return $self->loc("Untaken");
928 return $self->loc( "Given to [_1]", $New->Name );
933 "Owner forcibly changed from [_1] to [_2]",
934 $Old->Name, $New->Name );
939 return $self->loc( "[_1] changed from [_2] to [_3]",
940 $self->loc($self->Field),
941 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
944 PurgeTransaction => sub {
946 return $self->loc("Transaction [_1] purged", $self->Data);
950 my $ticket = RT::Ticket->new($self->CurrentUser);
951 $ticket->Load($self->NewValue);
952 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
954 OpenReminder => sub {
956 my $ticket = RT::Ticket->new($self->CurrentUser);
957 $ticket->Load($self->NewValue);
958 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
961 ResolveReminder => sub {
963 my $ticket = RT::Ticket->new($self->CurrentUser);
964 $ticket->Load($self->NewValue);
965 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
976 Returns true if the creator of the transaction is a requestor of the ticket.
977 Returns false otherwise
983 $self->ObjectType eq 'RT::Ticket' or return undef;
984 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
989 sub _OverlayAccessible {
992 ObjectType => { public => 1},
993 ObjectId => { public => 1},
1003 return ( 0, $self->loc('Transactions are immutable') );
1010 Takes the name of a table column.
1011 Returns its value as a string, if the user passes an ACL check
1019 #if the field is public, return it.
1020 if ( $self->_Accessible( $field, 'public' ) ) {
1021 return $self->SUPER::_Value( $field );
1024 unless ( $self->CurrentUserCanSee ) {
1028 return $self->SUPER::_Value( $field );
1033 =head2 CurrentUserHasRight RIGHT
1035 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1040 sub CurrentUserHasRight {
1043 return $self->CurrentUser->HasRight(
1045 Object => $self->Object
1049 =head2 CurrentUserCanSee
1051 Returns true if current user has rights to see this particular transaction.
1053 This fact depends on type of the transaction, type of an object the transaction
1054 is attached to and may be other conditions, so this method is prefered over
1055 custom implementations.
1059 sub CurrentUserCanSee {
1062 # If it's a comment, we need to be extra special careful
1063 my $type = $self->__Value('Type');
1064 if ( $type eq 'Comment' ) {
1065 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1069 elsif ( $type eq 'CommentEmailRecord' ) {
1070 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1071 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1075 elsif ( $type eq 'EmailRecord' ) {
1076 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1080 # Make sure the user can see the custom field before showing that it changed
1081 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1082 my $cf = RT::CustomField->new( $self->CurrentUser );
1083 $cf->SetContextObject( $self->Object );
1084 $cf->Load( $cf_id );
1085 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1088 # Transactions that might have changed the ->Object's visibility to
1089 # the current user are marked readable
1090 return 1 if $self->{ _object_is_readable };
1092 # Defer to the object in question
1093 return $self->Object->CurrentUserCanSee("Transaction");
1099 return $self->ObjectId;
1104 return $self->Object;
1109 if ( my $type = $self->__Value('ReferenceType')
1110 and my $id = $self->__Value('OldReference') )
1112 my $Object = $type->new($self->CurrentUser);
1113 $Object->Load( $id );
1114 return $Object->Content;
1117 return $self->_Value('OldValue');
1123 if ( my $type = $self->__Value('ReferenceType')
1124 and my $id = $self->__Value('NewReference') )
1126 my $Object = $type->new($self->CurrentUser);
1127 $Object->Load( $id );
1128 return $Object->Content;
1131 return $self->_Value('NewValue');
1137 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1138 $Object->Load($self->__Value('ObjectId'));
1142 sub FriendlyObjectType {
1144 my $type = $self->ObjectType or return undef;
1146 return $self->loc($type);
1149 =head2 UpdateCustomFields
1153 CustomField-<<Id>> => Value
1156 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1157 this transaction's custom fields
1161 sub UpdateCustomFields {
1165 # This method used to have an API that took a hash of a single
1166 # value "ARGSRef", which was a reference to a hash of arguments.
1167 # This was insane. The next few lines of code preserve that API
1168 # while giving us something saner.
1170 # TODO: 3.6: DEPRECATE OLD API
1174 if ($args{'ARGSRef'}) {
1175 $args = $args{ARGSRef};
1180 foreach my $arg ( keys %$args ) {
1183 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1184 next if $arg =~ /-Magic$/;
1185 next if $arg =~ /-TimeUnits$/;
1187 my $values = $args->{$arg};
1189 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1191 next unless (defined($value) && length($value));
1192 $self->_AddCustomFieldValue(
1195 RecordTransaction => 0,
1201 =head2 LoadCustomFieldByIdentifier
1203 Finds and returns the custom field of the given name for the
1204 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1205 look for queue-specific CFs before global ones.
1209 sub LoadCustomFieldByIdentifier {
1213 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1214 if ref $field or $field =~ /^\d+$/;
1216 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1217 unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1219 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1220 $CFs->SetContextObject( $self->Object );
1221 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1222 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1223 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1224 return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1227 =head2 CustomFieldLookupType
1229 Returns the RT::Transaction lookup type, which can
1230 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1235 sub CustomFieldLookupType {
1236 "RT::Queue-RT::Ticket-RT::Transaction";
1240 =head2 SquelchMailTo
1242 Similar to Ticket class SquelchMailTo method - returns a list of
1243 transaction's squelched addresses. As transactions are immutable, the
1244 list of squelched recipients cannot be modified after creation.
1250 return () unless $self->CurrentUserCanSee;
1251 return $self->Attributes->Named('SquelchMailTo');
1256 Returns the list of email addresses (as L<Email::Address> objects)
1257 that this transaction would send mail to. There may be duplicates.
1264 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1265 my $action = $scrip->ActionObj->Action;
1266 next unless $action->isa('RT::Action::SendEmail');
1268 foreach my $type (qw(To Cc Bcc)) {
1269 push @recipients, $action->$type();
1273 if ( $self->Rules ) {
1274 for my $rule (@{$self->Rules}) {
1275 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1276 my $data = $rule->{hints}{recipients};
1277 foreach my $type (qw(To Cc Bcc)) {
1278 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1285 =head2 DeferredRecipients($freq, $include_sent )
1287 Takes the following arguments:
1291 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1293 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1297 Returns an array of users who should now receive the notification that
1298 was recorded in this transaction. Returns an empty array if there were
1299 no deferred users, or if $include_sent was not specified and the deferred
1300 notifications have been sent.
1304 sub DeferredRecipients {
1307 my $include_sent = @_? shift : 0;
1309 my $attr = $self->FirstAttribute('DeferredRecipients');
1311 return () unless ($attr);
1313 my $deferred = $attr->Content;
1315 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1319 for my $user (keys %{$deferred->{$freq}}) {
1320 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1321 delete $deferred->{$freq}->{$user}
1324 # Now get our users. Easy.
1326 return keys %{ $deferred->{$freq} };
1331 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1335 'fast_update_p' => 1,
1336 'cache_for_sec' => 6000,
1341 =head2 ACLEquivalenceObjects
1343 This method returns a list of objects for which a user's rights also apply
1344 to this Transaction.
1346 This currently only applies to Transaction Custom Fields on Tickets, so we return
1347 the Ticket's Queue and the Ticket.
1349 This method is called from L<RT::Principal/HasRight>.
1353 sub ACLEquivalenceObjects {
1356 return unless $self->ObjectType eq 'RT::Ticket';
1357 my $object = $self->Object;
1358 return $object,$object->QueueObj;
1368 Returns the current value of id.
1369 (In the database, id is stored as int(11).)
1377 Returns the current value of ObjectType.
1378 (In the database, ObjectType is stored as varchar(64).)
1382 =head2 SetObjectType VALUE
1385 Set ObjectType to VALUE.
1386 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1387 (In the database, ObjectType will be stored as a varchar(64).)
1395 Returns the current value of ObjectId.
1396 (In the database, ObjectId is stored as int(11).)
1400 =head2 SetObjectId VALUE
1403 Set ObjectId to VALUE.
1404 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1405 (In the database, ObjectId will be stored as a int(11).)
1413 Returns the current value of TimeTaken.
1414 (In the database, TimeTaken is stored as int(11).)
1418 =head2 SetTimeTaken VALUE
1421 Set TimeTaken to VALUE.
1422 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1423 (In the database, TimeTaken will be stored as a int(11).)
1431 Returns the current value of Type.
1432 (In the database, Type is stored as varchar(20).)
1436 =head2 SetType VALUE
1440 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1441 (In the database, Type will be stored as a varchar(20).)
1449 Returns the current value of Field.
1450 (In the database, Field is stored as varchar(40).)
1454 =head2 SetField VALUE
1458 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1459 (In the database, Field will be stored as a varchar(40).)
1467 Returns the current value of OldValue.
1468 (In the database, OldValue is stored as varchar(255).)
1472 =head2 SetOldValue VALUE
1475 Set OldValue to VALUE.
1476 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1477 (In the database, OldValue will be stored as a varchar(255).)
1485 Returns the current value of NewValue.
1486 (In the database, NewValue is stored as varchar(255).)
1490 =head2 SetNewValue VALUE
1493 Set NewValue to VALUE.
1494 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1495 (In the database, NewValue will be stored as a varchar(255).)
1501 =head2 ReferenceType
1503 Returns the current value of ReferenceType.
1504 (In the database, ReferenceType is stored as varchar(255).)
1508 =head2 SetReferenceType VALUE
1511 Set ReferenceType to VALUE.
1512 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1513 (In the database, ReferenceType will be stored as a varchar(255).)
1521 Returns the current value of OldReference.
1522 (In the database, OldReference is stored as int(11).)
1526 =head2 SetOldReference VALUE
1529 Set OldReference to VALUE.
1530 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1531 (In the database, OldReference will be stored as a int(11).)
1539 Returns the current value of NewReference.
1540 (In the database, NewReference is stored as int(11).)
1544 =head2 SetNewReference VALUE
1547 Set NewReference to VALUE.
1548 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1549 (In the database, NewReference will be stored as a int(11).)
1557 Returns the current value of Data.
1558 (In the database, Data is stored as varchar(255).)
1562 =head2 SetData VALUE
1566 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1567 (In the database, Data will be stored as a varchar(255).)
1575 Returns the current value of Creator.
1576 (In the database, Creator is stored as int(11).)
1584 Returns the current value of Created.
1585 (In the database, Created is stored as datetime.)
1592 sub _CoreAccessible {
1596 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1598 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1600 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1602 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1604 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1606 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1608 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1610 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1612 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1614 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1616 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1618 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1620 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1622 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1627 RT::Base->_ImportOverlays();