1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@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 = 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'});
218 Delete this transaction. Currently DOES NOT CHECK ACLS
226 $RT::Handle->BeginTransaction();
228 my $attachments = $self->Attachments;
230 while (my $attachment = $attachments->Next) {
231 my ($id, $msg) = $attachment->Delete();
233 $RT::Handle->Rollback();
234 return($id, $self->loc("System Error: [_1]", $msg));
237 my ($id,$msg) = $self->SUPER::Delete();
239 $RT::Handle->Rollback();
240 return($id, $self->loc("System Error: [_1]", $msg));
242 $RT::Handle->Commit();
248 # {{{ Routines dealing with Attachments
254 Returns the L<RT::Attachments> object which contains the "top-level" object
255 attachment for this transaction.
262 # XXX: Where is ACL check?
264 unless ( defined $self->{'message'} ) {
266 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
267 $self->{'message'}->Limit(
268 FIELD => 'TransactionId',
271 $self->{'message'}->ChildrenOf(0);
273 $self->{'message'}->GotoFirstItem;
275 return $self->{'message'};
282 =head2 Content PARAMHASH
284 If this transaction has attached mime objects, returns the body of the first
285 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
288 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
289 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
291 If $args{'Type'} is set to C<text/html>, this will return an HTML
292 part of the message, if available. Otherwise it looks for a text/plain
293 part. If $args{'Type'} is missing, it defaults to the value of
294 C<$RT::Transaction::PreferredContentType>, if that's missing too,
295 defaults to 'text/plain'.
302 Type => $PreferredContentType || 'text/plain',
305 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
310 if ( my $content_obj = $self->ContentObj( Type => $args{Type} ) ) {
311 $content = $content_obj->Content ||'';
313 if ( lc $content_obj->ContentType eq 'text/html' ) {
314 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
316 if ($args{Type} ne 'text/html') {
317 my $tree = HTML::TreeBuilder->new_from_content( $content );
318 $content = HTML::FormatText->new(
326 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
327 if ($args{Type} eq 'text/html') {
328 # Extremely simple text->html converter
329 $content =~ s/&/&/g;
330 $content =~ s/</</g;
331 $content =~ s/>/>/g;
332 $content = "<pre>$content</pre>";
337 # If all else fails, return a message that we couldn't find any content
339 $content = $self->loc('This transaction appears to have no content');
342 if ( $args{'Quote'} ) {
344 # What's the longest line like?
346 foreach ( split ( /\n/, $content ) ) {
347 $max = length if length > $max;
350 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
351 require Text::Wrapper;
352 my $wrapper = new Text::Wrapper(
353 columns => $args{'Wrap'},
354 body_start => ( $max > 70 * 3 ? ' ' : '' ),
357 $content = $wrapper->wrap($content);
360 $content =~ s/^/> /gm;
361 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
373 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
380 if (my $attach = $self->Attachments->First) {
381 return $attach->Addresses;
394 Returns the RT::Attachment object which contains the content for this Transaction
401 my %args = ( Type => $PreferredContentType || 'text/plain',
404 # If we don't have any content, return undef now.
405 # Get the set of toplevel attachments to this transaction.
406 return undef unless my $Attachment = $self->Attachments->First;
408 # If it's a textual part, just return the body.
409 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
410 return ($Attachment);
413 # If it's a multipart object, first try returning the first part with preferred
414 # MIME type ('text/plain' by default).
416 elsif ( $Attachment->ContentType =~ '^multipart/' ) {
417 my $plain_parts = $Attachment->Children;
418 $plain_parts->ContentType( VALUE => $args{Type} );
419 $plain_parts->LimitNotEmpty;
421 # If we actully found a part, return its content
422 if ( my $first = $plain_parts->First ) {
426 # If that fails, return the first textual part which has some content.
427 my $all_parts = $self->Attachments;
428 while ( my $part = $all_parts->Next ) {
429 next unless RT::I18N::IsTextualContentType($part->ContentType)
435 # We found no content. suck
445 If this transaction has attached mime objects, returns the first one's subject
446 Otherwise, returns null
452 return undef unless my $first = $self->Attachments->First;
453 return $first->Subject;
458 # {{{ sub Attachments
462 Returns all the RT::Attachment objects which are attached
463 to this transaction. Takes an optional parameter, which is
464 a ContentType that Attachments should be restricted to.
471 if ( $self->{'attachments'} ) {
472 $self->{'attachments'}->GotoFirstItem;
473 return $self->{'attachments'};
476 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
478 unless ( $self->CurrentUserCanSee ) {
479 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0');
480 return $self->{'attachments'};
483 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
485 # Get the self->{'attachments'} in the order they're put into
486 # the database. Arguably, we should be returning a tree
487 # of self->{'attachments'}, not a set...but no current app seems to need
490 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
492 return $self->{'attachments'};
501 A private method used to attach a mime object to this transaction.
507 my $MIMEObject = shift;
509 unless ( defined $MIMEObject ) {
510 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
511 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
514 my $Attachment = RT::Attachment->new( $self->CurrentUser );
515 my ($id, $msg) = $Attachment->Create(
516 TransactionId => $self->Id,
517 Attachment => $MIMEObject
519 return ( $Attachment, $msg || $self->loc("Attachment created") );
529 my $main_content = $self->ContentObj;
530 my $entity = $main_content->ContentAsMIME;
532 if ( $main_content->Parent ) {
533 # main content is not top most entity, we shouldn't loose
534 # From/To/Cc headers that are on a top part
535 my $attachments = RT::Attachments->new( $self->CurrentUser );
536 $attachments->Columns(qw(id Parent TransactionId Headers));
537 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
538 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
539 $attachments->Limit( FIELD => 'Parent', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0 );
540 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
541 my $tmp = $attachments->First;
542 if ( $tmp && $tmp->id ne $main_content->id ) {
543 $entity->make_multipart;
544 $entity->head->add( split /:/, $_, 2 ) foreach $tmp->SplitHeaders;
545 $entity->make_singlepart;
549 my $attachments = RT::Attachments->new( $self->CurrentUser );
550 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
554 VALUE => $main_content->id,
557 FIELD => 'ContentType',
558 OPERATOR => 'NOT STARTSWITH',
559 VALUE => 'multipart/',
566 while ( my $a = $attachments->Next ) {
567 $entity->make_multipart unless $entity->is_multipart;
568 $entity->add_part( $a->ContentAsMIME );
573 # {{{ Routines dealing with Transaction Attributes
575 # {{{ sub Description
579 Returns a text string which describes this transaction
586 unless ( $self->CurrentUserCanSee ) {
587 return ( $self->loc("Permission Denied") );
590 unless ( defined $self->Type ) {
591 return ( $self->loc("No transaction type specified"));
594 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
599 # {{{ sub BriefDescription
601 =head2 BriefDescription
603 Returns a text string which briefly describes this transaction
607 sub BriefDescription {
610 unless ( $self->CurrentUserCanSee ) {
611 return ( $self->loc("Permission Denied") );
614 my $type = $self->Type; #cache this, rather than calling it 30 times
616 unless ( defined $type ) {
617 return $self->loc("No transaction type specified");
620 my $obj_type = $self->FriendlyObjectType;
622 if ( $type eq 'Create' ) {
623 return ( $self->loc( "[_1] created", $obj_type ) );
625 elsif ( $type eq 'Enabled' ) {
626 return ( $self->loc( "[_1] enabled", $obj_type ) );
628 elsif ( $type eq 'Disabled' ) {
629 return ( $self->loc( "[_1] disabled", $obj_type ) );
631 elsif ( $type =~ /Status/ ) {
632 if ( $self->Field eq 'Status' ) {
633 if ( $self->NewValue eq 'deleted' ) {
634 return ( $self->loc( "[_1] deleted", $obj_type ) );
639 "Status changed from [_1] to [_2]",
640 "'" . $self->loc( $self->OldValue ) . "'",
641 "'" . $self->loc( $self->NewValue ) . "'"
649 my $no_value = $self->loc("(no value)");
652 "[_1] changed from [_2] to [_3]",
654 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
655 "'" . $self->NewValue . "'"
660 if ( my $code = $_BriefDescriptions{$type} ) {
661 return $code->($self);
665 "Default: [_1]/[_2] changed from [_3] to [_4]",
670 ? "'" . $self->OldValue . "'"
671 : $self->loc("(no value)")
673 "'" . $self->NewValue . "'"
677 %_BriefDescriptions = (
678 CommentEmailRecord => sub {
680 return $self->loc("Outgoing email about a comment recorded");
684 return $self->loc("Outgoing email recorded");
688 return $self->loc("Correspondence added");
692 return $self->loc("Comments added");
696 my $field = $self->loc('CustomField');
698 if ( $self->Field ) {
699 my $cf = RT::CustomField->new( $self->CurrentUser );
700 $cf->Load( $self->Field );
701 $field = $cf->Name();
704 if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
705 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
707 elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
708 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
712 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
717 return $self->loc("Untaken");
721 return $self->loc("Taken");
725 my $Old = RT::User->new( $self->CurrentUser );
726 $Old->Load( $self->OldValue );
727 my $New = RT::User->new( $self->CurrentUser );
728 $New->Load( $self->NewValue );
730 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
734 my $Old = RT::User->new( $self->CurrentUser );
735 $Old->Load( $self->OldValue );
736 return $self->loc("Stolen from [_1]", $Old->Name);
740 my $New = RT::User->new( $self->CurrentUser );
741 $New->Load( $self->NewValue );
742 return $self->loc( "Given to [_1]", $New->Name );
746 my $principal = RT::Principal->new($self->CurrentUser);
747 $principal->Load($self->NewValue);
748 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
752 my $principal = RT::Principal->new($self->CurrentUser);
753 $principal->Load($self->OldValue);
754 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
758 return $self->loc( "Subject changed to [_1]", $self->Data );
763 if ( $self->NewValue ) {
764 my $URI = RT::URI->new( $self->CurrentUser );
765 $URI->FromURI( $self->NewValue );
766 if ( $URI->Resolver ) {
767 $value = $URI->Resolver->AsString;
770 $value = $self->NewValue;
772 if ( $self->Field eq 'DependsOn' ) {
773 return $self->loc( "Dependency on [_1] added", $value );
775 elsif ( $self->Field eq 'DependedOnBy' ) {
776 return $self->loc( "Dependency by [_1] added", $value );
779 elsif ( $self->Field eq 'RefersTo' ) {
780 return $self->loc( "Reference to [_1] added", $value );
782 elsif ( $self->Field eq 'ReferredToBy' ) {
783 return $self->loc( "Reference by [_1] added", $value );
785 elsif ( $self->Field eq 'MemberOf' ) {
786 return $self->loc( "Membership in [_1] added", $value );
788 elsif ( $self->Field eq 'HasMember' ) {
789 return $self->loc( "Member [_1] added", $value );
791 elsif ( $self->Field eq 'MergedInto' ) {
792 return $self->loc( "Merged into [_1]", $value );
796 return ( $self->Data );
802 if ( $self->OldValue ) {
803 my $URI = RT::URI->new( $self->CurrentUser );
804 $URI->FromURI( $self->OldValue );
805 if ( $URI->Resolver ) {
806 $value = $URI->Resolver->AsString;
809 $value = $self->OldValue;
812 if ( $self->Field eq 'DependsOn' ) {
813 return $self->loc( "Dependency on [_1] deleted", $value );
815 elsif ( $self->Field eq 'DependedOnBy' ) {
816 return $self->loc( "Dependency by [_1] deleted", $value );
819 elsif ( $self->Field eq 'RefersTo' ) {
820 return $self->loc( "Reference to [_1] deleted", $value );
822 elsif ( $self->Field eq 'ReferredToBy' ) {
823 return $self->loc( "Reference by [_1] deleted", $value );
825 elsif ( $self->Field eq 'MemberOf' ) {
826 return $self->loc( "Membership in [_1] deleted", $value );
828 elsif ( $self->Field eq 'HasMember' ) {
829 return $self->loc( "Member [_1] deleted", $value );
833 return ( $self->Data );
838 if ( $self->Field eq 'Told' ) {
839 my $t1 = new RT::Date($self->CurrentUser);
840 $t1->Set(Format => 'ISO', Value => $self->NewValue);
841 my $t2 = new RT::Date($self->CurrentUser);
842 $t2->Set(Format => 'ISO', Value => $self->OldValue);
843 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
846 return $self->loc( "[_1] changed from [_2] to [_3]",
847 $self->loc($self->Field),
848 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
853 if ( $self->Field eq 'Password' ) {
854 return $self->loc('Password changed');
856 elsif ( $self->Field eq 'Queue' ) {
857 my $q1 = new RT::Queue( $self->CurrentUser );
858 $q1->Load( $self->OldValue );
859 my $q2 = new RT::Queue( $self->CurrentUser );
860 $q2->Load( $self->NewValue );
861 return $self->loc("[_1] changed from [_2] to [_3]",
862 $self->loc($self->Field) , $q1->Name , $q2->Name);
865 # Write the date/time change at local time:
866 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
867 my $t1 = new RT::Date($self->CurrentUser);
868 $t1->Set(Format => 'ISO', Value => $self->NewValue);
869 my $t2 = new RT::Date($self->CurrentUser);
870 $t2->Set(Format => 'ISO', Value => $self->OldValue);
871 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
874 return $self->loc( "[_1] changed from [_2] to [_3]",
875 $self->loc($self->Field),
876 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
879 PurgeTransaction => sub {
881 return $self->loc("Transaction [_1] purged", $self->Data);
885 my $ticket = RT::Ticket->new($self->CurrentUser);
886 $ticket->Load($self->NewValue);
887 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
889 OpenReminder => sub {
891 my $ticket = RT::Ticket->new($self->CurrentUser);
892 $ticket->Load($self->NewValue);
893 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
896 ResolveReminder => sub {
898 my $ticket = RT::Ticket->new($self->CurrentUser);
899 $ticket->Load($self->NewValue);
900 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
908 # {{{ Utility methods
914 Returns true if the creator of the transaction is a requestor of the ticket.
915 Returns false otherwise
921 $self->ObjectType eq 'RT::Ticket' or return undef;
922 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
929 sub _OverlayAccessible {
932 ObjectType => { public => 1},
933 ObjectId => { public => 1},
946 return ( 0, $self->loc('Transactions are immutable') );
955 Takes the name of a table column.
956 Returns its value as a string, if the user passes an ACL check
964 #if the field is public, return it.
965 if ( $self->_Accessible( $field, 'public' ) ) {
966 return $self->SUPER::_Value( $field );
969 unless ( $self->CurrentUserCanSee ) {
973 return $self->SUPER::_Value( $field );
978 # {{{ sub CurrentUserHasRight
980 =head2 CurrentUserHasRight RIGHT
982 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
987 sub CurrentUserHasRight {
990 return $self->CurrentUser->HasRight(
992 Object => $self->Object
996 =head2 CurrentUserCanSee
998 Returns true if current user has rights to see this particular transaction.
1000 This fact depends on type of the transaction, type of an object the transaction
1001 is attached to and may be other conditions, so this method is prefered over
1002 custom implementations.
1006 sub CurrentUserCanSee {
1009 # If it's a comment, we need to be extra special careful
1010 my $type = $self->__Value('Type');
1011 if ( $type eq 'Comment' ) {
1012 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1016 elsif ( $type eq 'CommentEmailRecord' ) {
1017 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1018 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1022 elsif ( $type eq 'EmailRecord' ) {
1023 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1027 # Make sure the user can see the custom field before showing that it changed
1028 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1029 my $cf = RT::CustomField->new( $self->CurrentUser );
1030 $cf->SetContextObject( $self->Object );
1031 $cf->Load( $cf_id );
1032 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1034 #if they ain't got rights to see, don't let em
1035 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1036 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1048 return $self->ObjectId;
1053 return $self->Object;
1058 if ( my $type = $self->__Value('ReferenceType')
1059 and my $id = $self->__Value('OldReference') )
1061 my $Object = $type->new($self->CurrentUser);
1062 $Object->Load( $id );
1063 return $Object->Content;
1066 return $self->__Value('OldValue');
1072 if ( my $type = $self->__Value('ReferenceType')
1073 and my $id = $self->__Value('NewReference') )
1075 my $Object = $type->new($self->CurrentUser);
1076 $Object->Load( $id );
1077 return $Object->Content;
1080 return $self->__Value('NewValue');
1086 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1087 $Object->Load($self->__Value('ObjectId'));
1091 sub FriendlyObjectType {
1093 my $type = $self->ObjectType or return undef;
1095 return $self->loc($type);
1098 =head2 UpdateCustomFields
1102 CustomField-<<Id>> => Value
1105 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1106 this transaction's custom fields
1110 sub UpdateCustomFields {
1114 # This method used to have an API that took a hash of a single
1115 # value "ARGSRef", which was a reference to a hash of arguments.
1116 # This was insane. The next few lines of code preserve that API
1117 # while giving us something saner.
1119 # TODO: 3.6: DEPRECATE OLD API
1123 if ($args{'ARGSRef'}) {
1124 $args = $args{ARGSRef};
1129 foreach my $arg ( keys %$args ) {
1132 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1133 next if $arg =~ /-Magic$/;
1135 my $values = $args->{$arg};
1137 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1139 next unless length($value);
1140 $self->_AddCustomFieldValue(
1143 RecordTransaction => 0,
1151 =head2 CustomFieldValues
1153 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1159 sub CustomFieldValues {
1163 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1165 # XXX: $field could be undef when we want fetch values for all CFs
1166 # do we want to cover this situation somehow here?
1167 unless ( defined $field && $field =~ /^\d+$/o ) {
1168 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1169 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1170 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1171 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1172 $field = $CFs->First->id if $CFs->First;
1175 return $self->SUPER::CustomFieldValues($field);
1180 # {{{ sub CustomFieldLookupType
1182 =head2 CustomFieldLookupType
1184 Returns the RT::Transaction lookup type, which can
1185 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1191 sub CustomFieldLookupType {
1192 "RT::Queue-RT::Ticket-RT::Transaction";
1196 =head2 DeferredRecipients($freq, $include_sent )
1198 Takes the following arguments:
1202 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1204 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1208 Returns an array of users who should now receive the notification that
1209 was recorded in this transaction. Returns an empty array if there were
1210 no deferred users, or if $include_sent was not specified and the deferred
1211 notifications have been sent.
1215 sub DeferredRecipients {
1218 my $include_sent = @_? shift : 0;
1220 my $attr = $self->FirstAttribute('DeferredRecipients');
1222 return () unless ($attr);
1224 my $deferred = $attr->Content;
1226 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1230 for my $user (keys %{$deferred->{$freq}}) {
1231 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1232 delete $deferred->{$freq}->{$user}
1235 # Now get our users. Easy.
1237 return keys %{ $deferred->{$freq} };
1242 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1246 'fast_update_p' => 1,
1247 'cache_for_sec' => 6000,
1252 =head2 ACLEquivalenceObjects
1254 This method returns a list of objects for which a user's rights also apply
1255 to this Transaction.
1257 This currently only applies to Transaction Custom Fields on Tickets, so we return
1258 the Ticket's Queue and the Ticket.
1260 This method is called from L<RT::Principal/HasRight>.
1264 sub ACLEquivalenceObjects {
1267 return unless $self->ObjectType eq 'RT::Ticket';
1268 my $object = $self->Object;
1269 return $object,$object->QueueObj;