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 $RT::MessageBoxWidth - 2 or 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 || '',
321 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
326 if ( my $content_obj =
327 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
329 $content = $content_obj->Content ||'';
331 if ( lc $content_obj->ContentType eq 'text/html' ) {
332 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
334 if ($args{Type} ne 'text/html') {
335 my $tree = HTML::TreeBuilder->new_from_content( $content );
336 $content = HTML::FormatText->new(
344 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
345 if ($args{Type} eq 'text/html') {
346 # Extremely simple text->html converter
347 $content =~ s/&/&/g;
348 $content =~ s/</</g;
349 $content =~ s/>/>/g;
350 $content = "<pre>$content</pre>";
355 # If all else fails, return a message that we couldn't find any content
357 $content = $self->loc('This transaction appears to have no content');
360 if ( $args{'Quote'} ) {
362 # What's the longest line like?
364 foreach ( split ( /\n/, $content ) ) {
365 $max = length if length > $max;
368 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
369 require Text::Wrapper;
370 my $wrapper = new Text::Wrapper(
371 columns => $args{'Wrap'},
372 body_start => ( $max > 70 * 3 ? ' ' : '' ),
375 $content = $wrapper->wrap($content);
378 $content =~ s/^/> /gm;
379 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
391 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
398 if (my $attach = $self->Attachments->First) {
399 return $attach->Addresses;
412 Returns the RT::Attachment object which contains the content for this Transaction
419 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
421 # If we don't have any content, return undef now.
422 # Get the set of toplevel attachments to this transaction.
424 my $Attachment = $args{'Attachment'};
426 $Attachment ||= $self->Attachments->First;
428 return undef unless ($Attachment);
430 # If it's a textual part, just return the body.
431 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
432 return ($Attachment);
435 # If it's a multipart object, first try returning the first part with preferred
436 # MIME type ('text/plain' by default).
438 elsif ( $Attachment->ContentType =~ qr|^multipart/mixed|i ) {
439 my $kids = $Attachment->Children;
440 while (my $child = $kids->Next) {
441 my $ret = $self->ContentObj(%args, Attachment => $child);
442 return $ret if ($ret);
445 elsif ( $Attachment->ContentType =~ qr|^multipart/|i ) {
447 my $plain_parts = $Attachment->Children;
448 $plain_parts->ContentType( VALUE => $args{Type} );
449 $plain_parts->LimitNotEmpty;
451 # If we actully found a part, return its content
452 if ( my $first = $plain_parts->First ) {
457 # If that fails, return the first textual part which has some content.
458 my $all_parts = $self->Attachments;
459 while ( my $part = $all_parts->Next ) {
460 next unless RT::I18N::IsTextualContentType($part->ContentType)
466 # We found no content. suck
476 If this transaction has attached mime objects, returns the first one's subject
477 Otherwise, returns null
483 return undef unless my $first = $self->Attachments->First;
484 return $first->Subject;
489 # {{{ sub Attachments
493 Returns all the RT::Attachment objects which are attached
494 to this transaction. Takes an optional parameter, which is
495 a ContentType that Attachments should be restricted to.
502 if ( $self->{'attachments'} ) {
503 $self->{'attachments'}->GotoFirstItem;
504 return $self->{'attachments'};
507 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
509 unless ( $self->CurrentUserCanSee ) {
510 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0');
511 return $self->{'attachments'};
514 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
516 # Get the self->{'attachments'} in the order they're put into
517 # the database. Arguably, we should be returning a tree
518 # of self->{'attachments'}, not a set...but no current app seems to need
521 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
523 return $self->{'attachments'};
532 A private method used to attach a mime object to this transaction.
538 my $MIMEObject = shift;
540 unless ( defined $MIMEObject ) {
541 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
542 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
545 my $Attachment = RT::Attachment->new( $self->CurrentUser );
546 my ($id, $msg) = $Attachment->Create(
547 TransactionId => $self->Id,
548 Attachment => $MIMEObject
550 return ( $Attachment, $msg || $self->loc("Attachment created") );
560 my $main_content = $self->ContentObj;
561 return unless $main_content;
563 my $entity = $main_content->ContentAsMIME;
565 if ( $main_content->Parent ) {
566 # main content is not top most entity, we shouldn't loose
567 # From/To/Cc headers that are on a top part
568 my $attachments = RT::Attachments->new( $self->CurrentUser );
569 $attachments->Columns(qw(id Parent TransactionId Headers));
570 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
571 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
572 $attachments->Limit( FIELD => 'Parent', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0 );
573 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
574 my $tmp = $attachments->First;
575 if ( $tmp && $tmp->id ne $main_content->id ) {
576 $entity->make_multipart;
577 $entity->head->add( split /:/, $_, 2 ) foreach $tmp->SplitHeaders;
578 $entity->make_singlepart;
582 my $attachments = RT::Attachments->new( $self->CurrentUser );
583 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
587 VALUE => $main_content->id,
590 FIELD => 'ContentType',
591 OPERATOR => 'NOT STARTSWITH',
592 VALUE => 'multipart/',
594 $attachments->LimitNotEmpty;
595 while ( my $a = $attachments->Next ) {
596 $entity->make_multipart unless $entity->is_multipart;
597 $entity->add_part( $a->ContentAsMIME );
602 # {{{ Routines dealing with Transaction Attributes
604 # {{{ sub Description
608 Returns a text string which describes this transaction
615 unless ( $self->CurrentUserCanSee ) {
616 return ( $self->loc("Permission Denied") );
619 unless ( defined $self->Type ) {
620 return ( $self->loc("No transaction type specified"));
623 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
628 # {{{ sub BriefDescription
630 =head2 BriefDescription
632 Returns a text string which briefly describes this transaction
636 sub BriefDescription {
639 unless ( $self->CurrentUserCanSee ) {
640 return ( $self->loc("Permission Denied") );
643 my $type = $self->Type; #cache this, rather than calling it 30 times
645 unless ( defined $type ) {
646 return $self->loc("No transaction type specified");
649 my $obj_type = $self->FriendlyObjectType;
651 if ( $type eq 'Create' ) {
652 return ( $self->loc( "[_1] created", $obj_type ) );
654 elsif ( $type eq 'Enabled' ) {
655 return ( $self->loc( "[_1] enabled", $obj_type ) );
657 elsif ( $type eq 'Disabled' ) {
658 return ( $self->loc( "[_1] disabled", $obj_type ) );
660 elsif ( $type =~ /Status/ ) {
661 if ( $self->Field eq 'Status' ) {
662 if ( $self->NewValue eq 'deleted' ) {
663 return ( $self->loc( "[_1] deleted", $obj_type ) );
668 "Status changed from [_1] to [_2]",
669 "'" . $self->loc( $self->OldValue ) . "'",
670 "'" . $self->loc( $self->NewValue ) . "'"
678 my $no_value = $self->loc("(no value)");
681 "[_1] changed from [_2] to [_3]",
683 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
684 "'" . $self->NewValue . "'"
688 elsif ( $type =~ /SystemError/ ) {
689 return $self->loc("System error");
692 if ( my $code = $_BriefDescriptions{$type} ) {
693 return $code->($self);
697 "Default: [_1]/[_2] changed from [_3] to [_4]",
702 ? "'" . $self->OldValue . "'"
703 : $self->loc("(no value)")
705 "'" . $self->NewValue . "'"
709 %_BriefDescriptions = (
710 CommentEmailRecord => sub {
712 return $self->loc("Outgoing email about a comment recorded");
716 return $self->loc("Outgoing email recorded");
720 return $self->loc("Correspondence added");
724 return $self->loc("Comments added");
728 my $field = $self->loc('CustomField');
730 if ( $self->Field ) {
731 my $cf = RT::CustomField->new( $self->CurrentUser );
732 $cf->Load( $self->Field );
733 $field = $cf->Name();
736 if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
737 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
739 elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
740 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
744 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
749 return $self->loc("Untaken");
753 return $self->loc("Taken");
757 my $Old = RT::User->new( $self->CurrentUser );
758 $Old->Load( $self->OldValue );
759 my $New = RT::User->new( $self->CurrentUser );
760 $New->Load( $self->NewValue );
762 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
766 my $Old = RT::User->new( $self->CurrentUser );
767 $Old->Load( $self->OldValue );
768 return $self->loc("Stolen from [_1]", $Old->Name);
772 my $New = RT::User->new( $self->CurrentUser );
773 $New->Load( $self->NewValue );
774 return $self->loc( "Given to [_1]", $New->Name );
778 my $principal = RT::Principal->new($self->CurrentUser);
779 $principal->Load($self->NewValue);
780 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
784 my $principal = RT::Principal->new($self->CurrentUser);
785 $principal->Load($self->OldValue);
786 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
790 return $self->loc( "Subject changed to [_1]", $self->Data );
795 if ( $self->NewValue ) {
796 my $URI = RT::URI->new( $self->CurrentUser );
797 $URI->FromURI( $self->NewValue );
798 if ( $URI->Resolver ) {
799 $value = $URI->Resolver->AsString;
802 $value = $self->NewValue;
804 if ( $self->Field eq 'DependsOn' ) {
805 return $self->loc( "Dependency on [_1] added", $value );
807 elsif ( $self->Field eq 'DependedOnBy' ) {
808 return $self->loc( "Dependency by [_1] added", $value );
811 elsif ( $self->Field eq 'RefersTo' ) {
812 return $self->loc( "Reference to [_1] added", $value );
814 elsif ( $self->Field eq 'ReferredToBy' ) {
815 return $self->loc( "Reference by [_1] added", $value );
817 elsif ( $self->Field eq 'MemberOf' ) {
818 return $self->loc( "Membership in [_1] added", $value );
820 elsif ( $self->Field eq 'HasMember' ) {
821 return $self->loc( "Member [_1] added", $value );
823 elsif ( $self->Field eq 'MergedInto' ) {
824 return $self->loc( "Merged into [_1]", $value );
828 return ( $self->Data );
834 if ( $self->OldValue ) {
835 my $URI = RT::URI->new( $self->CurrentUser );
836 $URI->FromURI( $self->OldValue );
837 if ( $URI->Resolver ) {
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 = new RT::Date($self->CurrentUser);
872 $t1->Set(Format => 'ISO', Value => $self->NewValue);
873 my $t2 = new RT::Date($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 = new RT::Queue( $self->CurrentUser );
890 $q1->Load( $self->OldValue );
891 my $q2 = new RT::Queue( $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/) {
899 my $t1 = new RT::Date($self->CurrentUser);
900 $t1->Set(Format => 'ISO', Value => $self->NewValue);
901 my $t2 = new RT::Date($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 );
906 return $self->loc( "[_1] changed from [_2] to [_3]",
907 $self->loc($self->Field),
908 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
911 PurgeTransaction => sub {
913 return $self->loc("Transaction [_1] purged", $self->Data);
917 my $ticket = RT::Ticket->new($self->CurrentUser);
918 $ticket->Load($self->NewValue);
919 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
921 OpenReminder => sub {
923 my $ticket = RT::Ticket->new($self->CurrentUser);
924 $ticket->Load($self->NewValue);
925 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
928 ResolveReminder => sub {
930 my $ticket = RT::Ticket->new($self->CurrentUser);
931 $ticket->Load($self->NewValue);
932 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
940 # {{{ Utility methods
946 Returns true if the creator of the transaction is a requestor of the ticket.
947 Returns false otherwise
953 $self->ObjectType eq 'RT::Ticket' or return undef;
954 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
961 sub _OverlayAccessible {
964 ObjectType => { public => 1},
965 ObjectId => { public => 1},
978 return ( 0, $self->loc('Transactions are immutable') );
987 Takes the name of a table column.
988 Returns its value as a string, if the user passes an ACL check
996 #if the field is public, return it.
997 if ( $self->_Accessible( $field, 'public' ) ) {
998 return $self->SUPER::_Value( $field );
1001 unless ( $self->CurrentUserCanSee ) {
1005 return $self->SUPER::_Value( $field );
1010 # {{{ sub CurrentUserHasRight
1012 =head2 CurrentUserHasRight RIGHT
1014 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1019 sub CurrentUserHasRight {
1022 return $self->CurrentUser->HasRight(
1024 Object => $self->Object
1028 =head2 CurrentUserCanSee
1030 Returns true if current user has rights to see this particular transaction.
1032 This fact depends on type of the transaction, type of an object the transaction
1033 is attached to and may be other conditions, so this method is prefered over
1034 custom implementations.
1038 sub CurrentUserCanSee {
1041 # If it's a comment, we need to be extra special careful
1042 my $type = $self->__Value('Type');
1043 if ( $type eq 'Comment' ) {
1044 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1048 elsif ( $type eq 'CommentEmailRecord' ) {
1049 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1050 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1054 elsif ( $type eq 'EmailRecord' ) {
1055 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1059 # Make sure the user can see the custom field before showing that it changed
1060 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1061 my $cf = RT::CustomField->new( $self->CurrentUser );
1062 $cf->SetContextObject( $self->Object );
1063 $cf->Load( $cf_id );
1064 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1066 #if they ain't got rights to see, don't let em
1067 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1068 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
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$/;
1167 my $values = $args->{$arg};
1169 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1171 next unless length($value);
1172 $self->_AddCustomFieldValue(
1175 RecordTransaction => 0,
1183 =head2 CustomFieldValues
1185 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1191 sub CustomFieldValues {
1195 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1197 # XXX: $field could be undef when we want fetch values for all CFs
1198 # do we want to cover this situation somehow here?
1199 unless ( defined $field && $field =~ /^\d+$/o ) {
1200 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1201 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1202 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1203 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1204 $field = $CFs->First->id if $CFs->First;
1207 return $self->SUPER::CustomFieldValues($field);
1212 # {{{ sub CustomFieldLookupType
1214 =head2 CustomFieldLookupType
1216 Returns the RT::Transaction lookup type, which can
1217 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1223 sub CustomFieldLookupType {
1224 "RT::Queue-RT::Ticket-RT::Transaction";
1228 =head2 DeferredRecipients($freq, $include_sent )
1230 Takes the following arguments:
1234 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1236 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1240 Returns an array of users who should now receive the notification that
1241 was recorded in this transaction. Returns an empty array if there were
1242 no deferred users, or if $include_sent was not specified and the deferred
1243 notifications have been sent.
1247 sub DeferredRecipients {
1250 my $include_sent = @_? shift : 0;
1252 my $attr = $self->FirstAttribute('DeferredRecipients');
1254 return () unless ($attr);
1256 my $deferred = $attr->Content;
1258 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1262 for my $user (keys %{$deferred->{$freq}}) {
1263 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1264 delete $deferred->{$freq}->{$user}
1267 # Now get our users. Easy.
1269 return keys %{ $deferred->{$freq} };
1274 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1278 'fast_update_p' => 1,
1279 'cache_for_sec' => 6000,
1284 =head2 ACLEquivalenceObjects
1286 This method returns a list of objects for which a user's rights also apply
1287 to this Transaction.
1289 This currently only applies to Transaction Custom Fields on Tickets, so we return
1290 the Ticket's Queue and the Ticket.
1292 This method is called from L<RT::Principal/HasRight>.
1296 sub ACLEquivalenceObjects {
1299 return unless $self->ObjectType eq 'RT::Ticket';
1300 my $object = $self->Object;
1301 return $object,$object->QueueObj;