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,
122 $args{ObjectId} ||= $args{Ticket};
124 #if we didn't specify a ticket, we need to bail
125 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
126 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
131 #lets create our transaction
133 Type => $args{'Type'},
134 Data => $args{'Data'},
135 Field => $args{'Field'},
136 OldValue => $args{'OldValue'},
137 NewValue => $args{'NewValue'},
138 Created => $args{'Created'},
139 ObjectType => $args{'ObjectType'},
140 ObjectId => $args{'ObjectId'},
141 ReferenceType => $args{'ReferenceType'},
142 OldReference => $args{'OldReference'},
143 NewReference => $args{'NewReference'},
146 # Parameters passed in during an import that we probably don't want to touch, otherwise
147 foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
148 $params{$attr} = $args{$attr} if ($args{$attr});
151 my $id = $self->SUPER::Create(%params);
153 if ( defined $args{'MIMEObj'} ) {
154 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
156 $RT::Logger->error("Couldn't add attachment: $msg");
157 return ( 0, $self->loc("Couldn't add attachment") );
162 #Provide a way to turn off scrips if we need to
163 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
164 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
165 $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
167 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
169 $self->{'scrips'}->Prepare(
170 Stage => 'TransactionCreate',
171 Type => $args{'Type'},
172 Ticket => $args{'ObjectId'},
173 Transaction => $self->id,
176 # Entry point of the rule system
177 my $ticket = RT::Ticket->new($RT::SystemUser);
178 $ticket->Load($args{'ObjectId'});
179 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
180 Stage => 'TransactionCreate',
181 Type => $args{'Type'},
182 TicketObj => $ticket,
183 TransactionObj => $self,
186 if ($args{'CommitScrips'} ) {
187 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
188 $self->{'scrips'}->Commit();
189 RT::Ruleset->CommitRules($rules);
193 return ( $id, $self->loc("Transaction Created") );
200 Returns the Scrips object for this transaction.
201 This routine is only useful on a freshly created transaction object.
202 Scrips do not get persisted to the database with transactions.
210 return($self->{'scrips'});
216 Returns the array of Rule objects for this transaction.
217 This routine is only useful on a freshly created transaction object.
218 Rules do not get persisted to the database with transactions.
226 return($self->{'rules'});
234 Delete this transaction. Currently DOES NOT CHECK ACLS
242 $RT::Handle->BeginTransaction();
244 my $attachments = $self->Attachments;
246 while (my $attachment = $attachments->Next) {
247 my ($id, $msg) = $attachment->Delete();
249 $RT::Handle->Rollback();
250 return($id, $self->loc("System Error: [_1]", $msg));
253 my ($id,$msg) = $self->SUPER::Delete();
255 $RT::Handle->Rollback();
256 return($id, $self->loc("System Error: [_1]", $msg));
258 $RT::Handle->Commit();
264 # {{{ Routines dealing with Attachments
270 Returns the L<RT::Attachments> object which contains the "top-level" object
271 attachment for this transaction.
278 # XXX: Where is ACL check?
280 unless ( defined $self->{'message'} ) {
282 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
283 $self->{'message'}->Limit(
284 FIELD => 'TransactionId',
287 $self->{'message'}->ChildrenOf(0);
289 $self->{'message'}->GotoFirstItem;
291 return $self->{'message'};
298 =head2 Content PARAMHASH
300 If this transaction has attached mime objects, returns the body of the first
301 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
304 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
305 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
307 If $args{'Type'} is set to C<text/html>, this will return an HTML
308 part of the message, if available. Otherwise it looks for a text/plain
309 part. If $args{'Type'} is missing, it defaults to the value of
310 C<$RT::Transaction::PreferredContentType>, if that's missing too,
318 Type => $PreferredContentType || '',
325 if ( my $content_obj =
326 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
328 $content = $content_obj->Content ||'';
330 if ( lc $content_obj->ContentType eq 'text/html' ) {
331 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
333 if ($args{Type} ne 'text/html') {
334 my $tree = HTML::TreeBuilder->new_from_content( $content );
335 $content = HTML::FormatText->new(
343 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
344 if ($args{Type} eq 'text/html') {
345 # Extremely simple text->html converter
346 $content =~ s/&/&/g;
347 $content =~ s/</</g;
348 $content =~ s/>/>/g;
349 $content = "<pre>$content</pre>";
354 # If all else fails, return a message that we couldn't find any content
356 $content = $self->loc('This transaction appears to have no content');
359 if ( $args{'Quote'} ) {
361 # What's the longest line like?
363 foreach ( split ( /\n/, $content ) ) {
364 $max = length if length > $max;
368 require Text::Wrapper;
369 my $wrapper = new Text::Wrapper(
370 columns => $args{'Wrap'},
371 body_start => ( $max > 70 * 3 ? ' ' : '' ),
374 $content = $wrapper->wrap($content);
377 $content =~ s/^/> /gm;
378 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
390 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
397 if (my $attach = $self->Attachments->First) {
398 return $attach->Addresses;
411 Returns the RT::Attachment object which contains the content for this Transaction
418 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
420 # If we don't have any content, return undef now.
421 # Get the set of toplevel attachments to this transaction.
423 my $Attachment = $args{'Attachment'};
425 $Attachment ||= $self->Attachments->First;
427 return undef unless ($Attachment);
429 # If it's a textual part, just return the body.
430 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
431 return ($Attachment);
434 # If it's a multipart object, first try returning the first part with preferred
435 # MIME type ('text/plain' by default).
437 elsif ( $Attachment->ContentType =~ qr|^multipart/mixed|i ) {
438 my $kids = $Attachment->Children;
439 while (my $child = $kids->Next) {
440 my $ret = $self->ContentObj(%args, Attachment => $child);
441 return $ret if ($ret);
444 elsif ( $Attachment->ContentType =~ qr|^multipart/|i ) {
446 my $plain_parts = $Attachment->Children;
447 $plain_parts->ContentType( VALUE => $args{Type} );
448 $plain_parts->LimitNotEmpty;
450 # If we actully found a part, return its content
451 if ( my $first = $plain_parts->First ) {
456 # If that fails, return the first textual part which has some content.
457 my $all_parts = $self->Attachments;
458 while ( my $part = $all_parts->Next ) {
459 next unless RT::I18N::IsTextualContentType($part->ContentType)
465 # We found no content. suck
475 If this transaction has attached mime objects, returns the first one's subject
476 Otherwise, returns null
482 return undef unless my $first = $self->Attachments->First;
483 return $first->Subject;
488 # {{{ sub Attachments
492 Returns all the RT::Attachment objects which are attached
493 to this transaction. Takes an optional parameter, which is
494 a ContentType that Attachments should be restricted to.
501 if ( $self->{'attachments'} ) {
502 $self->{'attachments'}->GotoFirstItem;
503 return $self->{'attachments'};
506 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
508 unless ( $self->CurrentUserCanSee ) {
509 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0');
510 return $self->{'attachments'};
513 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
515 # Get the self->{'attachments'} in the order they're put into
516 # the database. Arguably, we should be returning a tree
517 # of self->{'attachments'}, not a set...but no current app seems to need
520 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
522 return $self->{'attachments'};
531 A private method used to attach a mime object to this transaction.
537 my $MIMEObject = shift;
539 unless ( defined $MIMEObject ) {
540 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
541 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
544 my $Attachment = RT::Attachment->new( $self->CurrentUser );
545 my ($id, $msg) = $Attachment->Create(
546 TransactionId => $self->Id,
547 Attachment => $MIMEObject
549 return ( $Attachment, $msg || $self->loc("Attachment created") );
559 my $main_content = $self->ContentObj;
560 return unless $main_content;
562 my $entity = $main_content->ContentAsMIME;
564 if ( $main_content->Parent ) {
565 # main content is not top most entity, we shouldn't loose
566 # From/To/Cc headers that are on a top part
567 my $attachments = RT::Attachments->new( $self->CurrentUser );
568 $attachments->Columns(qw(id Parent TransactionId Headers));
569 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
570 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
571 $attachments->Limit( FIELD => 'Parent', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0 );
572 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
573 my $tmp = $attachments->First;
574 if ( $tmp && $tmp->id ne $main_content->id ) {
575 $entity->make_multipart;
576 $entity->head->add( split /:/, $_, 2 ) foreach $tmp->SplitHeaders;
577 $entity->make_singlepart;
581 my $attachments = RT::Attachments->new( $self->CurrentUser );
582 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
586 VALUE => $main_content->id,
589 FIELD => 'ContentType',
590 OPERATOR => 'NOT STARTSWITH',
591 VALUE => 'multipart/',
593 $attachments->LimitNotEmpty;
594 while ( my $a = $attachments->Next ) {
595 $entity->make_multipart unless $entity->is_multipart;
596 $entity->add_part( $a->ContentAsMIME );
601 # {{{ Routines dealing with Transaction Attributes
603 # {{{ sub Description
607 Returns a text string which describes this transaction
614 unless ( $self->CurrentUserCanSee ) {
615 return ( $self->loc("Permission Denied") );
618 unless ( defined $self->Type ) {
619 return ( $self->loc("No transaction type specified"));
622 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
627 # {{{ sub BriefDescription
629 =head2 BriefDescription
631 Returns a text string which briefly describes this transaction
635 sub BriefDescription {
638 unless ( $self->CurrentUserCanSee ) {
639 return ( $self->loc("Permission Denied") );
642 my $type = $self->Type; #cache this, rather than calling it 30 times
644 unless ( defined $type ) {
645 return $self->loc("No transaction type specified");
648 my $obj_type = $self->FriendlyObjectType;
650 if ( $type eq 'Create' ) {
651 return ( $self->loc( "[_1] created", $obj_type ) );
653 elsif ( $type eq 'Enabled' ) {
654 return ( $self->loc( "[_1] enabled", $obj_type ) );
656 elsif ( $type eq 'Disabled' ) {
657 return ( $self->loc( "[_1] disabled", $obj_type ) );
659 elsif ( $type =~ /Status/ ) {
660 if ( $self->Field eq 'Status' ) {
661 if ( $self->NewValue eq 'deleted' ) {
662 return ( $self->loc( "[_1] deleted", $obj_type ) );
667 "Status changed from [_1] to [_2]",
668 "'" . $self->loc( $self->OldValue ) . "'",
669 "'" . $self->loc( $self->NewValue ) . "'"
677 my $no_value = $self->loc("(no value)");
680 "[_1] changed from [_2] to [_3]",
682 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
683 "'" . $self->NewValue . "'"
687 elsif ( $type =~ /SystemError/ ) {
688 return $self->loc("System error");
691 if ( my $code = $_BriefDescriptions{$type} ) {
692 return $code->($self);
696 "Default: [_1]/[_2] changed from [_3] to [_4]",
701 ? "'" . $self->OldValue . "'"
702 : $self->loc("(no value)")
704 "'" . $self->NewValue . "'"
708 %_BriefDescriptions = (
709 CommentEmailRecord => sub {
711 return $self->loc("Outgoing email about a comment recorded");
715 return $self->loc("Outgoing email recorded");
719 return $self->loc("Correspondence added");
723 return $self->loc("Comments added");
727 my $field = $self->loc('CustomField');
729 if ( $self->Field ) {
730 my $cf = RT::CustomField->new( $self->CurrentUser );
731 $cf->Load( $self->Field );
732 $field = $cf->Name();
735 if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
736 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
738 elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
739 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
743 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
748 return $self->loc("Untaken");
752 return $self->loc("Taken");
756 my $Old = RT::User->new( $self->CurrentUser );
757 $Old->Load( $self->OldValue );
758 my $New = RT::User->new( $self->CurrentUser );
759 $New->Load( $self->NewValue );
761 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
765 my $Old = RT::User->new( $self->CurrentUser );
766 $Old->Load( $self->OldValue );
767 return $self->loc("Stolen from [_1]", $Old->Name);
771 my $New = RT::User->new( $self->CurrentUser );
772 $New->Load( $self->NewValue );
773 return $self->loc( "Given to [_1]", $New->Name );
777 my $principal = RT::Principal->new($self->CurrentUser);
778 $principal->Load($self->NewValue);
779 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
783 my $principal = RT::Principal->new($self->CurrentUser);
784 $principal->Load($self->OldValue);
785 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
789 return $self->loc( "Subject changed to [_1]", $self->Data );
794 if ( $self->NewValue ) {
795 my $URI = RT::URI->new( $self->CurrentUser );
796 $URI->FromURI( $self->NewValue );
797 if ( $URI->Resolver ) {
798 $value = $URI->Resolver->AsString;
801 $value = $self->NewValue;
803 if ( $self->Field eq 'DependsOn' ) {
804 return $self->loc( "Dependency on [_1] added", $value );
806 elsif ( $self->Field eq 'DependedOnBy' ) {
807 return $self->loc( "Dependency by [_1] added", $value );
810 elsif ( $self->Field eq 'RefersTo' ) {
811 return $self->loc( "Reference to [_1] added", $value );
813 elsif ( $self->Field eq 'ReferredToBy' ) {
814 return $self->loc( "Reference by [_1] added", $value );
816 elsif ( $self->Field eq 'MemberOf' ) {
817 return $self->loc( "Membership in [_1] added", $value );
819 elsif ( $self->Field eq 'HasMember' ) {
820 return $self->loc( "Member [_1] added", $value );
822 elsif ( $self->Field eq 'MergedInto' ) {
823 return $self->loc( "Merged into [_1]", $value );
827 return ( $self->Data );
833 if ( $self->OldValue ) {
834 my $URI = RT::URI->new( $self->CurrentUser );
835 $URI->FromURI( $self->OldValue );
836 if ( $URI->Resolver ) {
837 $value = $URI->Resolver->AsString;
840 $value = $self->OldValue;
843 if ( $self->Field eq 'DependsOn' ) {
844 return $self->loc( "Dependency on [_1] deleted", $value );
846 elsif ( $self->Field eq 'DependedOnBy' ) {
847 return $self->loc( "Dependency by [_1] deleted", $value );
850 elsif ( $self->Field eq 'RefersTo' ) {
851 return $self->loc( "Reference to [_1] deleted", $value );
853 elsif ( $self->Field eq 'ReferredToBy' ) {
854 return $self->loc( "Reference by [_1] deleted", $value );
856 elsif ( $self->Field eq 'MemberOf' ) {
857 return $self->loc( "Membership in [_1] deleted", $value );
859 elsif ( $self->Field eq 'HasMember' ) {
860 return $self->loc( "Member [_1] deleted", $value );
864 return ( $self->Data );
869 if ( $self->Field eq 'Told' ) {
870 my $t1 = new RT::Date($self->CurrentUser);
871 $t1->Set(Format => 'ISO', Value => $self->NewValue);
872 my $t2 = new RT::Date($self->CurrentUser);
873 $t2->Set(Format => 'ISO', Value => $self->OldValue);
874 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
877 return $self->loc( "[_1] changed from [_2] to [_3]",
878 $self->loc($self->Field),
879 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
884 if ( $self->Field eq 'Password' ) {
885 return $self->loc('Password changed');
887 elsif ( $self->Field eq 'Queue' ) {
888 my $q1 = new RT::Queue( $self->CurrentUser );
889 $q1->Load( $self->OldValue );
890 my $q2 = new RT::Queue( $self->CurrentUser );
891 $q2->Load( $self->NewValue );
892 return $self->loc("[_1] changed from [_2] to [_3]",
893 $self->loc($self->Field) , $q1->Name , $q2->Name);
896 # Write the date/time change at local time:
897 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
898 my $t1 = new RT::Date($self->CurrentUser);
899 $t1->Set(Format => 'ISO', Value => $self->NewValue);
900 my $t2 = new RT::Date($self->CurrentUser);
901 $t2->Set(Format => 'ISO', Value => $self->OldValue);
902 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
905 return $self->loc( "[_1] changed from [_2] to [_3]",
906 $self->loc($self->Field),
907 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
910 PurgeTransaction => sub {
912 return $self->loc("Transaction [_1] purged", $self->Data);
916 my $ticket = RT::Ticket->new($self->CurrentUser);
917 $ticket->Load($self->NewValue);
918 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
920 OpenReminder => sub {
922 my $ticket = RT::Ticket->new($self->CurrentUser);
923 $ticket->Load($self->NewValue);
924 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
927 ResolveReminder => sub {
929 my $ticket = RT::Ticket->new($self->CurrentUser);
930 $ticket->Load($self->NewValue);
931 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
939 # {{{ Utility methods
945 Returns true if the creator of the transaction is a requestor of the ticket.
946 Returns false otherwise
952 $self->ObjectType eq 'RT::Ticket' or return undef;
953 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
960 sub _OverlayAccessible {
963 ObjectType => { public => 1},
964 ObjectId => { public => 1},
977 return ( 0, $self->loc('Transactions are immutable') );
986 Takes the name of a table column.
987 Returns its value as a string, if the user passes an ACL check
995 #if the field is public, return it.
996 if ( $self->_Accessible( $field, 'public' ) ) {
997 return $self->SUPER::_Value( $field );
1000 unless ( $self->CurrentUserCanSee ) {
1004 return $self->SUPER::_Value( $field );
1009 # {{{ sub CurrentUserHasRight
1011 =head2 CurrentUserHasRight RIGHT
1013 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1018 sub CurrentUserHasRight {
1021 return $self->CurrentUser->HasRight(
1023 Object => $self->Object
1027 =head2 CurrentUserCanSee
1029 Returns true if current user has rights to see this particular transaction.
1031 This fact depends on type of the transaction, type of an object the transaction
1032 is attached to and may be other conditions, so this method is prefered over
1033 custom implementations.
1037 sub CurrentUserCanSee {
1040 # If it's a comment, we need to be extra special careful
1041 my $type = $self->__Value('Type');
1042 if ( $type eq 'Comment' ) {
1043 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1047 elsif ( $type eq 'CommentEmailRecord' ) {
1048 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1049 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1053 elsif ( $type eq 'EmailRecord' ) {
1054 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1058 # Make sure the user can see the custom field before showing that it changed
1059 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1060 my $cf = RT::CustomField->new( $self->CurrentUser );
1061 $cf->SetContextObject( $self->Object );
1062 $cf->Load( $cf_id );
1063 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1065 #if they ain't got rights to see, don't let em
1066 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1067 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1079 return $self->ObjectId;
1084 return $self->Object;
1089 if ( my $type = $self->__Value('ReferenceType')
1090 and my $id = $self->__Value('OldReference') )
1092 my $Object = $type->new($self->CurrentUser);
1093 $Object->Load( $id );
1094 return $Object->Content;
1097 return $self->__Value('OldValue');
1103 if ( my $type = $self->__Value('ReferenceType')
1104 and my $id = $self->__Value('NewReference') )
1106 my $Object = $type->new($self->CurrentUser);
1107 $Object->Load( $id );
1108 return $Object->Content;
1111 return $self->__Value('NewValue');
1117 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1118 $Object->Load($self->__Value('ObjectId'));
1122 sub FriendlyObjectType {
1124 my $type = $self->ObjectType or return undef;
1126 return $self->loc($type);
1129 =head2 UpdateCustomFields
1133 CustomField-<<Id>> => Value
1136 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1137 this transaction's custom fields
1141 sub UpdateCustomFields {
1145 # This method used to have an API that took a hash of a single
1146 # value "ARGSRef", which was a reference to a hash of arguments.
1147 # This was insane. The next few lines of code preserve that API
1148 # while giving us something saner.
1150 # TODO: 3.6: DEPRECATE OLD API
1154 if ($args{'ARGSRef'}) {
1155 $args = $args{ARGSRef};
1160 foreach my $arg ( keys %$args ) {
1163 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1164 next if $arg =~ /-Magic$/;
1166 my $values = $args->{$arg};
1168 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1170 next unless length($value);
1171 $self->_AddCustomFieldValue(
1174 RecordTransaction => 0,
1182 =head2 CustomFieldValues
1184 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1190 sub CustomFieldValues {
1194 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1196 # XXX: $field could be undef when we want fetch values for all CFs
1197 # do we want to cover this situation somehow here?
1198 unless ( defined $field && $field =~ /^\d+$/o ) {
1199 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1200 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1201 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1202 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1203 $field = $CFs->First->id if $CFs->First;
1206 return $self->SUPER::CustomFieldValues($field);
1211 # {{{ sub CustomFieldLookupType
1213 =head2 CustomFieldLookupType
1215 Returns the RT::Transaction lookup type, which can
1216 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1222 sub CustomFieldLookupType {
1223 "RT::Queue-RT::Ticket-RT::Transaction";
1227 =head2 DeferredRecipients($freq, $include_sent )
1229 Takes the following arguments:
1233 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1235 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1239 Returns an array of users who should now receive the notification that
1240 was recorded in this transaction. Returns an empty array if there were
1241 no deferred users, or if $include_sent was not specified and the deferred
1242 notifications have been sent.
1246 sub DeferredRecipients {
1249 my $include_sent = @_? shift : 0;
1251 my $attr = $self->FirstAttribute('DeferredRecipients');
1253 return () unless ($attr);
1255 my $deferred = $attr->Content;
1257 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1261 for my $user (keys %{$deferred->{$freq}}) {
1262 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1263 delete $deferred->{$freq}->{$user}
1266 # Now get our users. Easy.
1268 return keys %{ $deferred->{$freq} };
1273 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1277 'fast_update_p' => 1,
1278 'cache_for_sec' => 6000,
1283 =head2 ACLEquivalenceObjects
1285 This method returns a list of objects for which a user's rights also apply
1286 to this Transaction.
1288 This currently only applies to Transaction Custom Fields on Tickets, so we return
1289 the Ticket's Queue and the Ticket.
1291 This method is called from L<RT::Principal/HasRight>.
1295 sub ACLEquivalenceObjects {
1298 return unless $self->ObjectType eq 'RT::Ticket';
1299 my $object = $self->Object;
1300 return $object,$object->QueueObj;