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;
75 no warnings qw(redefine);
77 use vars qw( %_BriefDescriptions $PreferredContentType );
84 use HTML::TreeBuilder;
90 Create a new transaction.
92 This routine should _never_ be called by anything other than RT::Ticket.
93 It should not be called
94 from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
95 Then the unpleasant stuff will start.
97 TODO: Document what gets passed to this
114 ObjectType => 'RT::Ticket',
116 ReferenceType => undef,
117 OldReference => undef,
118 NewReference => undef,
123 $args{ObjectId} ||= $args{Ticket};
125 #if we didn't specify a ticket, we need to bail
126 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
127 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
132 #lets create our transaction
134 Type => $args{'Type'},
135 Data => $args{'Data'},
136 Field => $args{'Field'},
137 OldValue => $args{'OldValue'},
138 NewValue => $args{'NewValue'},
139 Created => $args{'Created'},
140 ObjectType => $args{'ObjectType'},
141 ObjectId => $args{'ObjectId'},
142 ReferenceType => $args{'ReferenceType'},
143 OldReference => $args{'OldReference'},
144 NewReference => $args{'NewReference'},
147 # Parameters passed in during an import that we probably don't want to touch, otherwise
148 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
149 $params{$attr} = $args{$attr} if ($args{$attr});
152 my $id = $self->SUPER::Create(%params);
154 if ( defined $args{'MIMEObj'} ) {
155 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
157 $RT::Logger->error("Couldn't add attachment: $msg");
158 return ( 0, $self->loc("Couldn't add attachment") );
162 # Set up any custom fields passed at creation. Has to happen
165 $self->UpdateCustomFields(%{ $args{'CustomFields'} });
167 #Provide a way to turn off scrips if we need to
168 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
169 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
170 $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
172 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
174 $self->{'scrips'}->Prepare(
175 Stage => 'TransactionCreate',
176 Type => $args{'Type'},
177 Ticket => $args{'ObjectId'},
178 Transaction => $self->id,
181 # Entry point of the rule system
182 my $ticket = RT::Ticket->new($RT::SystemUser);
183 $ticket->Load($args{'ObjectId'});
184 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
185 Stage => 'TransactionCreate',
186 Type => $args{'Type'},
187 TicketObj => $ticket,
188 TransactionObj => $self,
191 if ($args{'CommitScrips'} ) {
192 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
193 $self->{'scrips'}->Commit();
194 RT::Ruleset->CommitRules($rules);
198 return ( $id, $self->loc("Transaction Created") );
205 Returns the Scrips object for this transaction.
206 This routine is only useful on a freshly created transaction object.
207 Scrips do not get persisted to the database with transactions.
215 return($self->{'scrips'});
221 Returns the array of Rule objects for this transaction.
222 This routine is only useful on a freshly created transaction object.
223 Rules do not get persisted to the database with transactions.
231 return($self->{'rules'});
239 Delete this transaction. Currently DOES NOT CHECK ACLS
247 $RT::Handle->BeginTransaction();
249 my $attachments = $self->Attachments;
251 while (my $attachment = $attachments->Next) {
252 my ($id, $msg) = $attachment->Delete();
254 $RT::Handle->Rollback();
255 return($id, $self->loc("System Error: [_1]", $msg));
258 my ($id,$msg) = $self->SUPER::Delete();
260 $RT::Handle->Rollback();
261 return($id, $self->loc("System Error: [_1]", $msg));
263 $RT::Handle->Commit();
269 # {{{ Routines dealing with Attachments
275 Returns the L<RT::Attachments> object which contains the "top-level" object
276 attachment for this transaction.
283 # XXX: Where is ACL check?
285 unless ( defined $self->{'message'} ) {
287 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
288 $self->{'message'}->Limit(
289 FIELD => 'TransactionId',
292 $self->{'message'}->ChildrenOf(0);
294 $self->{'message'}->GotoFirstItem;
296 return $self->{'message'};
303 =head2 Content PARAMHASH
305 If this transaction has attached mime objects, returns the body of the first
306 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
309 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
310 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
312 If $args{'Type'} is set to C<text/html>, this will return an HTML
313 part of the message, if available. Otherwise it looks for a text/plain
314 part. If $args{'Type'} is missing, it defaults to the value of
315 C<$RT::Transaction::PreferredContentType>, if that's missing too,
323 Type => $PreferredContentType || '',
326 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
331 if ( my $content_obj =
332 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
334 $content = $content_obj->Content ||'';
336 if ( lc $content_obj->ContentType eq 'text/html' ) {
337 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
339 if ($args{Type} ne 'text/html') {
340 my $tree = HTML::TreeBuilder->new_from_content( $content );
341 $content = HTML::FormatText->new(
349 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
350 if ($args{Type} eq 'text/html') {
351 # Extremely simple text->html converter
352 $content =~ s/&/&/g;
353 $content =~ s/</</g;
354 $content =~ s/>/>/g;
355 $content = "<pre>$content</pre>";
360 # If all else fails, return a message that we couldn't find any content
362 $content = $self->loc('This transaction appears to have no content');
365 if ( $args{'Quote'} ) {
367 # What's the longest line like?
369 foreach ( split ( /\n/, $content ) ) {
370 $max = length if length > $max;
373 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
374 require Text::Wrapper;
375 my $wrapper = new Text::Wrapper(
376 columns => $args{'Wrap'},
377 body_start => ( $max > 70 * 3 ? ' ' : '' ),
380 $content = $wrapper->wrap($content);
383 $content =~ s/^/> /gm;
384 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
396 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
403 if (my $attach = $self->Attachments->First) {
404 return $attach->Addresses;
417 Returns the RT::Attachment object which contains the content for this Transaction
424 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
426 # If we don't have any content, return undef now.
427 # Get the set of toplevel attachments to this transaction.
429 my $Attachment = $args{'Attachment'};
431 $Attachment ||= $self->Attachments->First;
433 return undef unless ($Attachment);
435 # If it's a textual part, just return the body.
436 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
437 return ($Attachment);
440 # If it's a multipart object, first try returning the first part with preferred
441 # MIME type ('text/plain' by default).
443 elsif ( $Attachment->ContentType =~ qr|^multipart/mixed|i ) {
444 my $kids = $Attachment->Children;
445 while (my $child = $kids->Next) {
446 my $ret = $self->ContentObj(%args, Attachment => $child);
447 return $ret if ($ret);
450 elsif ( $Attachment->ContentType =~ qr|^multipart/|i ) {
452 my $plain_parts = $Attachment->Children;
453 $plain_parts->ContentType( VALUE => $args{Type} );
454 $plain_parts->LimitNotEmpty;
456 # If we actully found a part, return its content
457 if ( my $first = $plain_parts->First ) {
462 # If that fails, return the first textual part which has some content.
463 my $all_parts = $self->Attachments;
464 while ( my $part = $all_parts->Next ) {
465 next unless RT::I18N::IsTextualContentType($part->ContentType)
471 # We found no content. suck
481 If this transaction has attached mime objects, returns the first one's subject
482 Otherwise, returns null
488 return undef unless my $first = $self->Attachments->First;
489 return $first->Subject;
494 # {{{ sub Attachments
498 Returns all the RT::Attachment objects which are attached
499 to this transaction. Takes an optional parameter, which is
500 a ContentType that Attachments should be restricted to.
507 if ( $self->{'attachments'} ) {
508 $self->{'attachments'}->GotoFirstItem;
509 return $self->{'attachments'};
512 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
514 unless ( $self->CurrentUserCanSee ) {
515 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
516 return $self->{'attachments'};
519 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
521 # Get the self->{'attachments'} in the order they're put into
522 # the database. Arguably, we should be returning a tree
523 # of self->{'attachments'}, not a set...but no current app seems to need
526 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
528 return $self->{'attachments'};
537 A private method used to attach a mime object to this transaction.
543 my $MIMEObject = shift;
545 unless ( defined $MIMEObject ) {
546 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
547 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
550 my $Attachment = RT::Attachment->new( $self->CurrentUser );
551 my ($id, $msg) = $Attachment->Create(
552 TransactionId => $self->Id,
553 Attachment => $MIMEObject
555 return ( $Attachment, $msg || $self->loc("Attachment created") );
565 my $main_content = $self->ContentObj;
566 return unless $main_content;
568 my $entity = $main_content->ContentAsMIME;
570 if ( $main_content->Parent ) {
571 # main content is not top most entity, we shouldn't loose
572 # From/To/Cc headers that are on a top part
573 my $attachments = RT::Attachments->new( $self->CurrentUser );
574 $attachments->Columns(qw(id Parent TransactionId Headers));
575 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
576 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
577 $attachments->Limit( FIELD => 'Parent', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0 );
578 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
579 my $tmp = $attachments->First;
580 if ( $tmp && $tmp->id ne $main_content->id ) {
581 $entity->make_multipart;
582 $entity->head->add( split /:/, $_, 2 ) foreach $tmp->SplitHeaders;
583 $entity->make_singlepart;
587 my $attachments = RT::Attachments->new( $self->CurrentUser );
588 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
592 VALUE => $main_content->id,
595 FIELD => 'ContentType',
596 OPERATOR => 'NOT STARTSWITH',
597 VALUE => 'multipart/',
599 $attachments->LimitNotEmpty;
600 while ( my $a = $attachments->Next ) {
601 $entity->make_multipart unless $entity->is_multipart;
602 $entity->add_part( $a->ContentAsMIME );
607 # {{{ Routines dealing with Transaction Attributes
609 # {{{ sub Description
613 Returns a text string which describes this transaction
620 unless ( $self->CurrentUserCanSee ) {
621 return ( $self->loc("Permission Denied") );
624 unless ( defined $self->Type ) {
625 return ( $self->loc("No transaction type specified"));
628 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
633 # {{{ sub BriefDescription
635 =head2 BriefDescription
637 Returns a text string which briefly describes this transaction
641 sub BriefDescription {
644 unless ( $self->CurrentUserCanSee ) {
645 return ( $self->loc("Permission Denied") );
648 my $type = $self->Type; #cache this, rather than calling it 30 times
650 unless ( defined $type ) {
651 return $self->loc("No transaction type specified");
654 my $obj_type = $self->FriendlyObjectType;
656 if ( $type eq 'Create' ) {
657 return ( $self->loc( "[_1] created", $obj_type ) );
659 elsif ( $type eq 'Enabled' ) {
660 return ( $self->loc( "[_1] enabled", $obj_type ) );
662 elsif ( $type eq 'Disabled' ) {
663 return ( $self->loc( "[_1] disabled", $obj_type ) );
665 elsif ( $type =~ /Status/ ) {
666 if ( $self->Field eq 'Status' ) {
667 if ( $self->NewValue eq 'deleted' ) {
668 return ( $self->loc( "[_1] deleted", $obj_type ) );
673 "Status changed from [_1] to [_2]",
674 "'" . $self->loc( $self->OldValue ) . "'",
675 "'" . $self->loc( $self->NewValue ) . "'"
683 my $no_value = $self->loc("(no value)");
686 "[_1] changed from [_2] to [_3]",
688 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
689 "'" . $self->NewValue . "'"
693 elsif ( $type =~ /SystemError/ ) {
694 return $self->loc("System error");
697 if ( my $code = $_BriefDescriptions{$type} ) {
698 return $code->($self);
702 "Default: [_1]/[_2] changed from [_3] to [_4]",
707 ? "'" . $self->OldValue . "'"
708 : $self->loc("(no value)")
710 "'" . $self->NewValue . "'"
714 %_BriefDescriptions = (
715 CommentEmailRecord => sub {
717 return $self->loc("Outgoing email about a comment recorded");
721 return $self->loc("Outgoing email recorded");
725 return $self->loc("Correspondence added");
729 return $self->loc("Comments added");
733 my $field = $self->loc('CustomField');
735 if ( $self->Field ) {
736 my $cf = RT::CustomField->new( $self->CurrentUser );
737 $cf->SetContextObject( $self->Object );
738 $cf->Load( $self->Field );
739 $field = $cf->Name();
742 if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
743 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
745 elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
746 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
750 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
755 return $self->loc("Untaken");
759 return $self->loc("Taken");
763 my $Old = RT::User->new( $self->CurrentUser );
764 $Old->Load( $self->OldValue );
765 my $New = RT::User->new( $self->CurrentUser );
766 $New->Load( $self->NewValue );
768 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
772 my $Old = RT::User->new( $self->CurrentUser );
773 $Old->Load( $self->OldValue );
774 return $self->loc("Stolen from [_1]", $Old->Name);
778 my $New = RT::User->new( $self->CurrentUser );
779 $New->Load( $self->NewValue );
780 return $self->loc( "Given to [_1]", $New->Name );
784 my $principal = RT::Principal->new($self->CurrentUser);
785 $principal->Load($self->NewValue);
786 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
790 my $principal = RT::Principal->new($self->CurrentUser);
791 $principal->Load($self->OldValue);
792 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
796 return $self->loc( "Subject changed to [_1]", $self->Data );
801 if ( $self->NewValue ) {
802 my $URI = RT::URI->new( $self->CurrentUser );
803 $URI->FromURI( $self->NewValue );
804 if ( $URI->Resolver ) {
805 $value = $URI->Resolver->AsString;
808 $value = $self->NewValue;
810 if ( $self->Field eq 'DependsOn' ) {
811 return $self->loc( "Dependency on [_1] added", $value );
813 elsif ( $self->Field eq 'DependedOnBy' ) {
814 return $self->loc( "Dependency by [_1] added", $value );
817 elsif ( $self->Field eq 'RefersTo' ) {
818 return $self->loc( "Reference to [_1] added", $value );
820 elsif ( $self->Field eq 'ReferredToBy' ) {
821 return $self->loc( "Reference by [_1] added", $value );
823 elsif ( $self->Field eq 'MemberOf' ) {
824 return $self->loc( "Membership in [_1] added", $value );
826 elsif ( $self->Field eq 'HasMember' ) {
827 return $self->loc( "Member [_1] added", $value );
829 elsif ( $self->Field eq 'MergedInto' ) {
830 return $self->loc( "Merged into [_1]", $value );
834 return ( $self->Data );
840 if ( $self->OldValue ) {
841 my $URI = RT::URI->new( $self->CurrentUser );
842 $URI->FromURI( $self->OldValue );
843 if ( $URI->Resolver ) {
844 $value = $URI->Resolver->AsString;
847 $value = $self->OldValue;
850 if ( $self->Field eq 'DependsOn' ) {
851 return $self->loc( "Dependency on [_1] deleted", $value );
853 elsif ( $self->Field eq 'DependedOnBy' ) {
854 return $self->loc( "Dependency by [_1] deleted", $value );
857 elsif ( $self->Field eq 'RefersTo' ) {
858 return $self->loc( "Reference to [_1] deleted", $value );
860 elsif ( $self->Field eq 'ReferredToBy' ) {
861 return $self->loc( "Reference by [_1] deleted", $value );
863 elsif ( $self->Field eq 'MemberOf' ) {
864 return $self->loc( "Membership in [_1] deleted", $value );
866 elsif ( $self->Field eq 'HasMember' ) {
867 return $self->loc( "Member [_1] deleted", $value );
871 return ( $self->Data );
876 if ( $self->Field eq 'Told' ) {
877 my $t1 = new RT::Date($self->CurrentUser);
878 $t1->Set(Format => 'ISO', Value => $self->NewValue);
879 my $t2 = new RT::Date($self->CurrentUser);
880 $t2->Set(Format => 'ISO', Value => $self->OldValue);
881 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
884 return $self->loc( "[_1] changed from [_2] to [_3]",
885 $self->loc($self->Field),
886 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
891 if ( $self->Field eq 'Password' ) {
892 return $self->loc('Password changed');
894 elsif ( $self->Field eq 'Queue' ) {
895 my $q1 = new RT::Queue( $self->CurrentUser );
896 $q1->Load( $self->OldValue );
897 my $q2 = new RT::Queue( $self->CurrentUser );
898 $q2->Load( $self->NewValue );
899 return $self->loc("[_1] changed from [_2] to [_3]",
900 $self->loc($self->Field) , $q1->Name , $q2->Name);
903 # Write the date/time change at local time:
904 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
905 my $t1 = new RT::Date($self->CurrentUser);
906 $t1->Set(Format => 'ISO', Value => $self->NewValue);
907 my $t2 = new RT::Date($self->CurrentUser);
908 $t2->Set(Format => 'ISO', Value => $self->OldValue);
909 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
912 return $self->loc( "[_1] changed from [_2] to [_3]",
913 $self->loc($self->Field),
914 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
917 PurgeTransaction => sub {
919 return $self->loc("Transaction [_1] purged", $self->Data);
923 my $ticket = RT::Ticket->new($self->CurrentUser);
924 $ticket->Load($self->NewValue);
925 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
927 OpenReminder => sub {
929 my $ticket = RT::Ticket->new($self->CurrentUser);
930 $ticket->Load($self->NewValue);
931 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
934 ResolveReminder => sub {
936 my $ticket = RT::Ticket->new($self->CurrentUser);
937 $ticket->Load($self->NewValue);
938 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
946 # {{{ Utility methods
952 Returns true if the creator of the transaction is a requestor of the ticket.
953 Returns false otherwise
959 $self->ObjectType eq 'RT::Ticket' or return undef;
960 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
967 sub _OverlayAccessible {
970 ObjectType => { public => 1},
971 ObjectId => { public => 1},
984 return ( 0, $self->loc('Transactions are immutable') );
993 Takes the name of a table column.
994 Returns its value as a string, if the user passes an ACL check
1002 #if the field is public, return it.
1003 if ( $self->_Accessible( $field, 'public' ) ) {
1004 return $self->SUPER::_Value( $field );
1007 unless ( $self->CurrentUserCanSee ) {
1011 return $self->SUPER::_Value( $field );
1016 # {{{ sub CurrentUserHasRight
1018 =head2 CurrentUserHasRight RIGHT
1020 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1025 sub CurrentUserHasRight {
1028 return $self->CurrentUser->HasRight(
1030 Object => $self->Object
1034 =head2 CurrentUserCanSee
1036 Returns true if current user has rights to see this particular transaction.
1038 This fact depends on type of the transaction, type of an object the transaction
1039 is attached to and may be other conditions, so this method is prefered over
1040 custom implementations.
1044 sub CurrentUserCanSee {
1047 # If it's a comment, we need to be extra special careful
1048 my $type = $self->__Value('Type');
1049 if ( $type eq 'Comment' ) {
1050 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1054 elsif ( $type eq 'CommentEmailRecord' ) {
1055 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1056 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1060 elsif ( $type eq 'EmailRecord' ) {
1061 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1065 # Make sure the user can see the custom field before showing that it changed
1066 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1067 my $cf = RT::CustomField->new( $self->CurrentUser );
1068 $cf->SetContextObject( $self->Object );
1069 $cf->Load( $cf_id );
1070 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1072 # Defer to the object in question
1073 return $self->Object->CurrentUserCanSee("Transaction");
1080 return $self->ObjectId;
1085 return $self->Object;
1090 if ( my $type = $self->__Value('ReferenceType')
1091 and my $id = $self->__Value('OldReference') )
1093 my $Object = $type->new($self->CurrentUser);
1094 $Object->Load( $id );
1095 return $Object->Content;
1098 return $self->_Value('OldValue');
1104 if ( my $type = $self->__Value('ReferenceType')
1105 and my $id = $self->__Value('NewReference') )
1107 my $Object = $type->new($self->CurrentUser);
1108 $Object->Load( $id );
1109 return $Object->Content;
1112 return $self->_Value('NewValue');
1118 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1119 $Object->Load($self->__Value('ObjectId'));
1123 sub FriendlyObjectType {
1125 my $type = $self->ObjectType or return undef;
1127 return $self->loc($type);
1130 =head2 UpdateCustomFields
1134 CustomField-<<Id>> => Value
1137 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1138 this transaction's custom fields
1142 sub UpdateCustomFields {
1146 # This method used to have an API that took a hash of a single
1147 # value "ARGSRef", which was a reference to a hash of arguments.
1148 # This was insane. The next few lines of code preserve that API
1149 # while giving us something saner.
1151 # TODO: 3.6: DEPRECATE OLD API
1155 if ($args{'ARGSRef'}) {
1156 $args = $args{ARGSRef};
1161 foreach my $arg ( keys %$args ) {
1164 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1165 next if $arg =~ /-Magic$/;
1166 next if $arg =~ /-TimeUnits$/;
1168 my $values = $args->{$arg};
1170 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1172 next unless length($value);
1173 $self->_AddCustomFieldValue(
1176 RecordTransaction => 0,
1184 =head2 CustomFieldValues
1186 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1192 sub CustomFieldValues {
1196 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1198 # XXX: $field could be undef when we want fetch values for all CFs
1199 # do we want to cover this situation somehow here?
1200 unless ( defined $field && $field =~ /^\d+$/o ) {
1201 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1202 $CFs->SetContextObject( $self->Object );
1203 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1204 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1205 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1206 $field = $CFs->First->id if $CFs->First;
1209 return $self->SUPER::CustomFieldValues($field);
1214 # {{{ sub CustomFieldLookupType
1216 =head2 CustomFieldLookupType
1218 Returns the RT::Transaction lookup type, which can
1219 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1225 sub CustomFieldLookupType {
1226 "RT::Queue-RT::Ticket-RT::Transaction";
1230 =head2 DeferredRecipients($freq, $include_sent )
1232 Takes the following arguments:
1236 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1238 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1242 Returns an array of users who should now receive the notification that
1243 was recorded in this transaction. Returns an empty array if there were
1244 no deferred users, or if $include_sent was not specified and the deferred
1245 notifications have been sent.
1249 sub DeferredRecipients {
1252 my $include_sent = @_? shift : 0;
1254 my $attr = $self->FirstAttribute('DeferredRecipients');
1256 return () unless ($attr);
1258 my $deferred = $attr->Content;
1260 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1264 for my $user (keys %{$deferred->{$freq}}) {
1265 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1266 delete $deferred->{$freq}->{$user}
1269 # Now get our users. Easy.
1271 return keys %{ $deferred->{$freq} };
1276 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1280 'fast_update_p' => 1,
1281 'cache_for_sec' => 6000,
1286 =head2 ACLEquivalenceObjects
1288 This method returns a list of objects for which a user's rights also apply
1289 to this Transaction.
1291 This currently only applies to Transaction Custom Fields on Tickets, so we return
1292 the Ticket's Queue and the Ticket.
1294 This method is called from L<RT::Principal/HasRight>.
1298 sub ACLEquivalenceObjects {
1301 return unless $self->ObjectType eq 'RT::Ticket';
1302 my $object = $self->Object;
1303 return $object,$object->QueueObj;