1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 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');
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->Load( $self->Field );
738 $field = $cf->Name();
741 if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
742 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
744 elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
745 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
749 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
754 return $self->loc("Untaken");
758 return $self->loc("Taken");
762 my $Old = RT::User->new( $self->CurrentUser );
763 $Old->Load( $self->OldValue );
764 my $New = RT::User->new( $self->CurrentUser );
765 $New->Load( $self->NewValue );
767 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
771 my $Old = RT::User->new( $self->CurrentUser );
772 $Old->Load( $self->OldValue );
773 return $self->loc("Stolen from [_1]", $Old->Name);
777 my $New = RT::User->new( $self->CurrentUser );
778 $New->Load( $self->NewValue );
779 return $self->loc( "Given to [_1]", $New->Name );
783 my $principal = RT::Principal->new($self->CurrentUser);
784 $principal->Load($self->NewValue);
785 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
789 my $principal = RT::Principal->new($self->CurrentUser);
790 $principal->Load($self->OldValue);
791 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
795 return $self->loc( "Subject changed to [_1]", $self->Data );
800 if ( $self->NewValue ) {
801 my $URI = RT::URI->new( $self->CurrentUser );
802 $URI->FromURI( $self->NewValue );
803 if ( $URI->Resolver ) {
804 $value = $URI->Resolver->AsString;
807 $value = $self->NewValue;
809 if ( $self->Field eq 'DependsOn' ) {
810 return $self->loc( "Dependency on [_1] added", $value );
812 elsif ( $self->Field eq 'DependedOnBy' ) {
813 return $self->loc( "Dependency by [_1] added", $value );
816 elsif ( $self->Field eq 'RefersTo' ) {
817 return $self->loc( "Reference to [_1] added", $value );
819 elsif ( $self->Field eq 'ReferredToBy' ) {
820 return $self->loc( "Reference by [_1] added", $value );
822 elsif ( $self->Field eq 'MemberOf' ) {
823 return $self->loc( "Membership in [_1] added", $value );
825 elsif ( $self->Field eq 'HasMember' ) {
826 return $self->loc( "Member [_1] added", $value );
828 elsif ( $self->Field eq 'MergedInto' ) {
829 return $self->loc( "Merged into [_1]", $value );
833 return ( $self->Data );
839 if ( $self->OldValue ) {
840 my $URI = RT::URI->new( $self->CurrentUser );
841 $URI->FromURI( $self->OldValue );
842 if ( $URI->Resolver ) {
843 $value = $URI->Resolver->AsString;
846 $value = $self->OldValue;
849 if ( $self->Field eq 'DependsOn' ) {
850 return $self->loc( "Dependency on [_1] deleted", $value );
852 elsif ( $self->Field eq 'DependedOnBy' ) {
853 return $self->loc( "Dependency by [_1] deleted", $value );
856 elsif ( $self->Field eq 'RefersTo' ) {
857 return $self->loc( "Reference to [_1] deleted", $value );
859 elsif ( $self->Field eq 'ReferredToBy' ) {
860 return $self->loc( "Reference by [_1] deleted", $value );
862 elsif ( $self->Field eq 'MemberOf' ) {
863 return $self->loc( "Membership in [_1] deleted", $value );
865 elsif ( $self->Field eq 'HasMember' ) {
866 return $self->loc( "Member [_1] deleted", $value );
870 return ( $self->Data );
875 if ( $self->Field eq 'Told' ) {
876 my $t1 = new RT::Date($self->CurrentUser);
877 $t1->Set(Format => 'ISO', Value => $self->NewValue);
878 my $t2 = new RT::Date($self->CurrentUser);
879 $t2->Set(Format => 'ISO', Value => $self->OldValue);
880 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
883 return $self->loc( "[_1] changed from [_2] to [_3]",
884 $self->loc($self->Field),
885 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
890 if ( $self->Field eq 'Password' ) {
891 return $self->loc('Password changed');
893 elsif ( $self->Field eq 'Queue' ) {
894 my $q1 = new RT::Queue( $self->CurrentUser );
895 $q1->Load( $self->OldValue );
896 my $q2 = new RT::Queue( $self->CurrentUser );
897 $q2->Load( $self->NewValue );
898 return $self->loc("[_1] changed from [_2] to [_3]",
899 $self->loc($self->Field) , $q1->Name , $q2->Name);
902 # Write the date/time change at local time:
903 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
904 my $t1 = new RT::Date($self->CurrentUser);
905 $t1->Set(Format => 'ISO', Value => $self->NewValue);
906 my $t2 = new RT::Date($self->CurrentUser);
907 $t2->Set(Format => 'ISO', Value => $self->OldValue);
908 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
911 return $self->loc( "[_1] changed from [_2] to [_3]",
912 $self->loc($self->Field),
913 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
916 PurgeTransaction => sub {
918 return $self->loc("Transaction [_1] purged", $self->Data);
922 my $ticket = RT::Ticket->new($self->CurrentUser);
923 $ticket->Load($self->NewValue);
924 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
926 OpenReminder => sub {
928 my $ticket = RT::Ticket->new($self->CurrentUser);
929 $ticket->Load($self->NewValue);
930 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
933 ResolveReminder => sub {
935 my $ticket = RT::Ticket->new($self->CurrentUser);
936 $ticket->Load($self->NewValue);
937 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
945 # {{{ Utility methods
951 Returns true if the creator of the transaction is a requestor of the ticket.
952 Returns false otherwise
958 $self->ObjectType eq 'RT::Ticket' or return undef;
959 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
966 sub _OverlayAccessible {
969 ObjectType => { public => 1},
970 ObjectId => { public => 1},
983 return ( 0, $self->loc('Transactions are immutable') );
992 Takes the name of a table column.
993 Returns its value as a string, if the user passes an ACL check
1001 #if the field is public, return it.
1002 if ( $self->_Accessible( $field, 'public' ) ) {
1003 return $self->SUPER::_Value( $field );
1006 unless ( $self->CurrentUserCanSee ) {
1010 return $self->SUPER::_Value( $field );
1015 # {{{ sub CurrentUserHasRight
1017 =head2 CurrentUserHasRight RIGHT
1019 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1024 sub CurrentUserHasRight {
1027 return $self->CurrentUser->HasRight(
1029 Object => $self->Object
1033 =head2 CurrentUserCanSee
1035 Returns true if current user has rights to see this particular transaction.
1037 This fact depends on type of the transaction, type of an object the transaction
1038 is attached to and may be other conditions, so this method is prefered over
1039 custom implementations.
1043 sub CurrentUserCanSee {
1046 # If it's a comment, we need to be extra special careful
1047 my $type = $self->__Value('Type');
1048 if ( $type eq 'Comment' ) {
1049 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1053 elsif ( $type eq 'CommentEmailRecord' ) {
1054 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1055 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1059 elsif ( $type eq 'EmailRecord' ) {
1060 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1064 # Make sure the user can see the custom field before showing that it changed
1065 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1066 my $cf = RT::CustomField->new( $self->CurrentUser );
1067 $cf->SetContextObject( $self->Object );
1068 $cf->Load( $cf_id );
1069 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1071 #if they ain't got rights to see, don't let em
1072 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1073 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1085 return $self->ObjectId;
1090 return $self->Object;
1095 if ( my $type = $self->__Value('ReferenceType')
1096 and my $id = $self->__Value('OldReference') )
1098 my $Object = $type->new($self->CurrentUser);
1099 $Object->Load( $id );
1100 return $Object->Content;
1103 return $self->__Value('OldValue');
1109 if ( my $type = $self->__Value('ReferenceType')
1110 and my $id = $self->__Value('NewReference') )
1112 my $Object = $type->new($self->CurrentUser);
1113 $Object->Load( $id );
1114 return $Object->Content;
1117 return $self->__Value('NewValue');
1123 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1124 $Object->Load($self->__Value('ObjectId'));
1128 sub FriendlyObjectType {
1130 my $type = $self->ObjectType or return undef;
1132 return $self->loc($type);
1135 =head2 UpdateCustomFields
1139 CustomField-<<Id>> => Value
1142 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1143 this transaction's custom fields
1147 sub UpdateCustomFields {
1151 # This method used to have an API that took a hash of a single
1152 # value "ARGSRef", which was a reference to a hash of arguments.
1153 # This was insane. The next few lines of code preserve that API
1154 # while giving us something saner.
1156 # TODO: 3.6: DEPRECATE OLD API
1160 if ($args{'ARGSRef'}) {
1161 $args = $args{ARGSRef};
1166 foreach my $arg ( keys %$args ) {
1169 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1170 next if $arg =~ /-Magic$/;
1171 next if $arg =~ /-TimeUnits$/;
1173 my $values = $args->{$arg};
1175 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1177 next unless length($value);
1178 $self->_AddCustomFieldValue(
1181 RecordTransaction => 0,
1189 =head2 CustomFieldValues
1191 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1197 sub CustomFieldValues {
1201 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1203 # XXX: $field could be undef when we want fetch values for all CFs
1204 # do we want to cover this situation somehow here?
1205 unless ( defined $field && $field =~ /^\d+$/o ) {
1206 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1207 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1208 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1209 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1210 $field = $CFs->First->id if $CFs->First;
1213 return $self->SUPER::CustomFieldValues($field);
1218 # {{{ sub CustomFieldLookupType
1220 =head2 CustomFieldLookupType
1222 Returns the RT::Transaction lookup type, which can
1223 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1229 sub CustomFieldLookupType {
1230 "RT::Queue-RT::Ticket-RT::Transaction";
1234 =head2 DeferredRecipients($freq, $include_sent )
1236 Takes the following arguments:
1240 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1242 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1246 Returns an array of users who should now receive the notification that
1247 was recorded in this transaction. Returns an empty array if there were
1248 no deferred users, or if $include_sent was not specified and the deferred
1249 notifications have been sent.
1253 sub DeferredRecipients {
1256 my $include_sent = @_? shift : 0;
1258 my $attr = $self->FirstAttribute('DeferredRecipients');
1260 return () unless ($attr);
1262 my $deferred = $attr->Content;
1264 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1268 for my $user (keys %{$deferred->{$freq}}) {
1269 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1270 delete $deferred->{$freq}->{$user}
1273 # Now get our users. Easy.
1275 return keys %{ $deferred->{$freq} };
1280 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1284 'fast_update_p' => 1,
1285 'cache_for_sec' => 6000,
1290 =head2 ACLEquivalenceObjects
1292 This method returns a list of objects for which a user's rights also apply
1293 to this Transaction.
1295 This currently only applies to Transaction Custom Fields on Tickets, so we return
1296 the Ticket's Queue and the Ticket.
1298 This method is called from L<RT::Principal/HasRight>.
1302 sub ACLEquivalenceObjects {
1305 return unless $self->ObjectType eq 'RT::Ticket';
1306 my $object = $self->Object;
1307 return $object,$object->QueueObj;