1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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;
74 use base 'RT::Record';
79 use vars qw( %_BriefDescriptions $PreferredContentType );
86 use HTML::TreeBuilder;
89 sub Table {'Transactions'}
95 Create a new transaction.
97 This routine should _never_ be called by anything other than RT::Ticket.
98 It should not be called
99 from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
100 Then the unpleasant stuff will start.
102 TODO: Document what gets passed to this
119 ObjectType => 'RT::Ticket',
121 ReferenceType => undef,
122 OldReference => undef,
123 NewReference => undef,
124 SquelchMailTo => undef,
129 $args{ObjectId} ||= $args{Ticket};
131 #if we didn't specify a ticket, we need to bail
132 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
133 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
137 # Set up any custom fields passed at creation. Has to happen
140 $self->UpdateCustomFields(%{ $args{'CustomFields'} });
142 #lets create our transaction
144 Type => $args{'Type'},
145 Data => $args{'Data'},
146 Field => $args{'Field'},
147 OldValue => $args{'OldValue'},
148 NewValue => $args{'NewValue'},
149 Created => $args{'Created'},
150 ObjectType => $args{'ObjectType'},
151 ObjectId => $args{'ObjectId'},
152 ReferenceType => $args{'ReferenceType'},
153 OldReference => $args{'OldReference'},
154 NewReference => $args{'NewReference'},
157 # Parameters passed in during an import that we probably don't want to touch, otherwise
158 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
159 $params{$attr} = $args{$attr} if ($args{$attr});
162 my $id = $self->SUPER::Create(%params);
164 if ( defined $args{'MIMEObj'} ) {
165 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
167 $RT::Logger->error("Couldn't add attachment: $msg");
168 return ( 0, $self->loc("Couldn't add attachment") );
173 Name => 'SquelchMailTo',
174 Content => RT::User->CanonicalizeEmailAddress($_)
175 ) for @{$args{'SquelchMailTo'} || []};
177 #Provide a way to turn off scrips if we need to
178 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
179 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
180 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
182 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
184 $self->{'scrips'}->Prepare(
185 Stage => 'TransactionCreate',
186 Type => $args{'Type'},
187 Ticket => $args{'ObjectId'},
188 Transaction => $self->id,
191 # Entry point of the rule system
192 my $ticket = RT::Ticket->new(RT->SystemUser);
193 $ticket->Load($args{'ObjectId'});
194 my $txn = RT::Transaction->new($RT::SystemUser);
195 $txn->Load($self->id);
197 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
198 Stage => 'TransactionCreate',
199 Type => $args{'Type'},
200 TicketObj => $ticket,
201 TransactionObj => $txn,
204 if ($args{'CommitScrips'} ) {
205 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
206 $self->{'scrips'}->Commit();
207 RT::Ruleset->CommitRules($rules);
211 return ( $id, $self->loc("Transaction Created") );
217 Returns the Scrips object for this transaction.
218 This routine is only useful on a freshly created transaction object.
219 Scrips do not get persisted to the database with transactions.
227 return($self->{'scrips'});
233 Returns the array of Rule objects for this transaction.
234 This routine is only useful on a freshly created transaction object.
235 Rules do not get persisted to the database with transactions.
243 return($self->{'rules'});
250 Delete this transaction. Currently DOES NOT CHECK ACLS
258 $RT::Handle->BeginTransaction();
260 my $attachments = $self->Attachments;
262 while (my $attachment = $attachments->Next) {
263 my ($id, $msg) = $attachment->Delete();
265 $RT::Handle->Rollback();
266 return($id, $self->loc("System Error: [_1]", $msg));
269 my ($id,$msg) = $self->SUPER::Delete();
271 $RT::Handle->Rollback();
272 return($id, $self->loc("System Error: [_1]", $msg));
274 $RT::Handle->Commit();
283 Returns the L<RT::Attachments> object which contains the "top-level" object
284 attachment for this transaction.
291 # XXX: Where is ACL check?
293 unless ( defined $self->{'message'} ) {
295 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
296 $self->{'message'}->Limit(
297 FIELD => 'TransactionId',
300 $self->{'message'}->ChildrenOf(0);
302 $self->{'message'}->GotoFirstItem;
304 return $self->{'message'};
309 =head2 Content PARAMHASH
311 If this transaction has attached mime objects, returns the body of the first
312 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
315 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
316 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
318 If $args{'Type'} is set to C<text/html>, this will return an HTML
319 part of the message, if available. Otherwise it looks for a text/plain
320 part. If $args{'Type'} is missing, it defaults to the value of
321 C<$RT::Transaction::PreferredContentType>, if that's missing too,
329 Type => $PreferredContentType || '',
332 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
337 if ( my $content_obj =
338 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
340 $content = $content_obj->Content ||'';
342 if ( lc $content_obj->ContentType eq 'text/html' ) {
343 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
345 if ($args{Type} ne 'text/html') {
346 my $tree = HTML::TreeBuilder->new_from_content( $content );
347 $content = HTML::FormatText->new(
355 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
356 if ($args{Type} eq 'text/html') {
357 # Extremely simple text->html converter
358 $content =~ s/&/&/g;
359 $content =~ s/</</g;
360 $content =~ s/>/>/g;
361 $content = "<pre>$content</pre>";
366 # If all else fails, return a message that we couldn't find any content
368 $content = $self->loc('This transaction appears to have no content');
371 if ( $args{'Quote'} ) {
373 # What's the longest line like?
375 foreach ( split ( /\n/, $content ) ) {
376 $max = length if length > $max;
379 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
380 require Text::Wrapper;
381 my $wrapper = Text::Wrapper->new(
382 columns => $args{'Wrap'},
383 body_start => ( $max > 70 * 3 ? ' ' : '' ),
386 $content = $wrapper->wrap($content);
389 $content =~ s/^/> /gm;
390 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
401 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
408 if (my $attach = $self->Attachments->First) {
409 return $attach->Addresses;
421 Returns the RT::Attachment object which contains the content for this Transaction
428 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
430 # If we don't have any content, return undef now.
431 # Get the set of toplevel attachments to this transaction.
433 my $Attachment = $args{'Attachment'};
435 $Attachment ||= $self->Attachments->First;
437 return undef unless ($Attachment);
439 # If it's a textual part, just return the body.
440 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
441 return ($Attachment);
444 # If it's a multipart object, first try returning the first part with preferred
445 # MIME type ('text/plain' by default).
447 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
448 my $kids = $Attachment->Children;
449 while (my $child = $kids->Next) {
450 my $ret = $self->ContentObj(%args, Attachment => $child);
451 return $ret if ($ret);
454 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
456 my $plain_parts = $Attachment->Children;
457 $plain_parts->ContentType( VALUE => $args{Type} );
458 $plain_parts->LimitNotEmpty;
460 # If we actully found a part, return its content
461 if ( my $first = $plain_parts->First ) {
466 # If that fails, return the first textual part which has some content.
467 my $all_parts = $self->Attachments;
468 while ( my $part = $all_parts->Next ) {
469 next unless RT::I18N::IsTextualContentType($part->ContentType)
475 # We found no content. suck
483 If this transaction has attached mime objects, returns the first one's subject
484 Otherwise, returns null
490 return undef unless my $first = $self->Attachments->First;
491 return $first->Subject;
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'};
535 A private method used to attach a mime object to this transaction.
541 my $MIMEObject = shift;
543 unless ( defined $MIMEObject ) {
544 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
545 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
548 my $Attachment = RT::Attachment->new( $self->CurrentUser );
549 my ($id, $msg) = $Attachment->Create(
550 TransactionId => $self->Id,
551 Attachment => $MIMEObject
553 return ( $Attachment, $msg || $self->loc("Attachment created") );
561 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
562 # since it has less information available without looking to it's parent
563 # transaction. Check ACLs here before we go any further.
564 return unless $self->CurrentUserCanSee;
566 my $attachments = RT::Attachments->new( $self->CurrentUser );
567 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
568 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
569 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
570 $attachments->RowsPerPage(1);
572 my $top = $attachments->First;
575 my $entity = MIME::Entity->build(
576 Type => 'message/rfc822',
577 Description => 'transaction ' . $self->id,
578 Data => $top->ContentAsMIME(Children => 1)->as_string,
588 Returns a text string which describes this transaction
595 unless ( $self->CurrentUserCanSee ) {
596 return ( $self->loc("Permission Denied") );
599 unless ( defined $self->Type ) {
600 return ( $self->loc("No transaction type specified"));
603 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
608 =head2 BriefDescription
610 Returns a text string which briefly describes this transaction
614 sub BriefDescription {
617 unless ( $self->CurrentUserCanSee ) {
618 return ( $self->loc("Permission Denied") );
621 my $type = $self->Type; #cache this, rather than calling it 30 times
623 unless ( defined $type ) {
624 return $self->loc("No transaction type specified");
627 my $obj_type = $self->FriendlyObjectType;
629 if ( $type eq 'Create' ) {
630 return ( $self->loc( "[_1] created", $obj_type ) );
632 elsif ( $type eq 'Enabled' ) {
633 return ( $self->loc( "[_1] enabled", $obj_type ) );
635 elsif ( $type eq 'Disabled' ) {
636 return ( $self->loc( "[_1] disabled", $obj_type ) );
638 elsif ( $type =~ /Status/ ) {
639 if ( $self->Field eq 'Status' ) {
640 if ( $self->NewValue eq 'deleted' ) {
641 return ( $self->loc( "[_1] deleted", $obj_type ) );
646 "Status changed from [_1] to [_2]",
647 "'" . $self->loc( $self->OldValue ) . "'",
648 "'" . $self->loc( $self->NewValue ) . "'"
656 my $no_value = $self->loc("(no value)");
659 "[_1] changed from [_2] to [_3]",
661 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
662 "'" . $self->NewValue . "'"
666 elsif ( $type =~ /SystemError/ ) {
667 return $self->loc("System error");
669 elsif ( $type =~ /Forward Transaction/ ) {
670 return $self->loc( "Forwarded Transaction #[_1] to [_2]",
671 $self->Field, $self->Data );
673 elsif ( $type =~ /Forward Ticket/ ) {
674 return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
677 if ( my $code = $_BriefDescriptions{$type} ) {
678 return $code->($self);
682 "Default: [_1]/[_2] changed from [_3] to [_4]",
687 ? "'" . $self->OldValue . "'"
688 : $self->loc("(no value)")
690 "'" . $self->NewValue . "'"
694 %_BriefDescriptions = (
695 CommentEmailRecord => sub {
697 return $self->loc("Outgoing email about a comment recorded");
701 return $self->loc("Outgoing email recorded");
705 return $self->loc("Correspondence added");
709 return $self->loc("Comments added");
713 my $field = $self->loc('CustomField');
715 if ( $self->Field ) {
716 my $cf = RT::CustomField->new( $self->CurrentUser );
717 $cf->Load( $self->Field );
718 $field = $cf->Name();
719 $field = $self->loc('a custom field') if !defined($field);
722 my $new = $self->NewValue;
723 my $old = $self->OldValue;
725 if ( !defined($old) || $old eq '' ) {
726 return $self->loc("[_1] [_2] added", $field, $new);
728 elsif ( !defined($new) || $new eq '' ) {
729 return $self->loc("[_1] [_2] deleted", $field, $old);
732 return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
737 return $self->loc("Untaken");
741 return $self->loc("Taken");
745 my $Old = RT::User->new( $self->CurrentUser );
746 $Old->Load( $self->OldValue );
747 my $New = RT::User->new( $self->CurrentUser );
748 $New->Load( $self->NewValue );
750 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
754 my $Old = RT::User->new( $self->CurrentUser );
755 $Old->Load( $self->OldValue );
756 return $self->loc("Stolen from [_1]", $Old->Name);
760 my $New = RT::User->new( $self->CurrentUser );
761 $New->Load( $self->NewValue );
762 return $self->loc( "Given to [_1]", $New->Name );
766 my $principal = RT::Principal->new($self->CurrentUser);
767 $principal->Load($self->NewValue);
768 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
772 my $principal = RT::Principal->new($self->CurrentUser);
773 $principal->Load($self->OldValue);
774 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
778 return $self->loc( "Subject changed to [_1]", $self->Data );
783 if ( $self->NewValue ) {
784 my $URI = RT::URI->new( $self->CurrentUser );
785 $URI->FromURI( $self->NewValue );
786 if ( $URI->Resolver ) {
787 $value = $URI->Resolver->AsString;
790 $value = $self->NewValue;
792 if ( $self->Field eq 'DependsOn' ) {
793 return $self->loc( "Dependency on [_1] added", $value );
795 elsif ( $self->Field eq 'DependedOnBy' ) {
796 return $self->loc( "Dependency by [_1] added", $value );
799 elsif ( $self->Field eq 'RefersTo' ) {
800 return $self->loc( "Reference to [_1] added", $value );
802 elsif ( $self->Field eq 'ReferredToBy' ) {
803 return $self->loc( "Reference by [_1] added", $value );
805 elsif ( $self->Field eq 'MemberOf' ) {
806 return $self->loc( "Membership in [_1] added", $value );
808 elsif ( $self->Field eq 'HasMember' ) {
809 return $self->loc( "Member [_1] added", $value );
811 elsif ( $self->Field eq 'MergedInto' ) {
812 return $self->loc( "Merged into [_1]", $value );
816 return ( $self->Data );
822 if ( $self->OldValue ) {
823 my $URI = RT::URI->new( $self->CurrentUser );
824 $URI->FromURI( $self->OldValue );
825 if ( $URI->Resolver ) {
826 $value = $URI->Resolver->AsString;
829 $value = $self->OldValue;
832 if ( $self->Field eq 'DependsOn' ) {
833 return $self->loc( "Dependency on [_1] deleted", $value );
835 elsif ( $self->Field eq 'DependedOnBy' ) {
836 return $self->loc( "Dependency by [_1] deleted", $value );
839 elsif ( $self->Field eq 'RefersTo' ) {
840 return $self->loc( "Reference to [_1] deleted", $value );
842 elsif ( $self->Field eq 'ReferredToBy' ) {
843 return $self->loc( "Reference by [_1] deleted", $value );
845 elsif ( $self->Field eq 'MemberOf' ) {
846 return $self->loc( "Membership in [_1] deleted", $value );
848 elsif ( $self->Field eq 'HasMember' ) {
849 return $self->loc( "Member [_1] deleted", $value );
853 return ( $self->Data );
858 if ( $self->Field eq 'Told' ) {
859 my $t1 = RT::Date->new($self->CurrentUser);
860 $t1->Set(Format => 'ISO', Value => $self->NewValue);
861 my $t2 = RT::Date->new($self->CurrentUser);
862 $t2->Set(Format => 'ISO', Value => $self->OldValue);
863 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
866 return $self->loc( "[_1] changed from [_2] to [_3]",
867 $self->loc($self->Field),
868 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
873 if ( $self->Field eq 'Password' ) {
874 return $self->loc('Password changed');
876 elsif ( $self->Field eq 'Queue' ) {
877 my $q1 = RT::Queue->new( $self->CurrentUser );
878 $q1->Load( $self->OldValue );
879 my $q2 = RT::Queue->new( $self->CurrentUser );
880 $q2->Load( $self->NewValue );
881 return $self->loc("[_1] changed from [_2] to [_3]",
882 $self->loc($self->Field) , $q1->Name , $q2->Name);
885 # Write the date/time change at local time:
886 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
887 my $t1 = RT::Date->new($self->CurrentUser);
888 $t1->Set(Format => 'ISO', Value => $self->NewValue);
889 my $t2 = RT::Date->new($self->CurrentUser);
890 $t2->Set(Format => 'ISO', Value => $self->OldValue);
891 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
893 elsif ( $self->Field eq 'Owner' ) {
894 my $Old = RT::User->new( $self->CurrentUser );
895 $Old->Load( $self->OldValue );
896 my $New = RT::User->new( $self->CurrentUser );
897 $New->Load( $self->NewValue );
899 if ( $Old->id == RT->Nobody->id ) {
900 if ( $New->id == $self->Creator ) {
901 return $self->loc("Taken");
904 return $self->loc( "Given to [_1]", $New->Name );
908 if ( $New->id == $self->Creator ) {
909 return $self->loc("Stolen from [_1]", $Old->Name);
911 elsif ( $Old->id == $self->Creator ) {
912 if ( $New->id == RT->Nobody->id ) {
913 return $self->loc("Untaken");
916 return $self->loc( "Given to [_1]", $New->Name );
921 "Owner forcibly changed from [_1] to [_2]",
922 $Old->Name, $New->Name );
927 return $self->loc( "[_1] changed from [_2] to [_3]",
928 $self->loc($self->Field),
929 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
932 PurgeTransaction => sub {
934 return $self->loc("Transaction [_1] purged", $self->Data);
938 my $ticket = RT::Ticket->new($self->CurrentUser);
939 $ticket->Load($self->NewValue);
940 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
942 OpenReminder => sub {
944 my $ticket = RT::Ticket->new($self->CurrentUser);
945 $ticket->Load($self->NewValue);
946 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
949 ResolveReminder => sub {
951 my $ticket = RT::Ticket->new($self->CurrentUser);
952 $ticket->Load($self->NewValue);
953 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
964 Returns true if the creator of the transaction is a requestor of the ticket.
965 Returns false otherwise
971 $self->ObjectType eq 'RT::Ticket' or return undef;
972 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
977 sub _OverlayAccessible {
980 ObjectType => { public => 1},
981 ObjectId => { public => 1},
991 return ( 0, $self->loc('Transactions are immutable') );
998 Takes the name of a table column.
999 Returns its value as a string, if the user passes an ACL check
1007 #if the field is public, return it.
1008 if ( $self->_Accessible( $field, 'public' ) ) {
1009 return $self->SUPER::_Value( $field );
1012 unless ( $self->CurrentUserCanSee ) {
1016 return $self->SUPER::_Value( $field );
1021 =head2 CurrentUserHasRight RIGHT
1023 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1028 sub CurrentUserHasRight {
1031 return $self->CurrentUser->HasRight(
1033 Object => $self->Object
1037 =head2 CurrentUserCanSee
1039 Returns true if current user has rights to see this particular transaction.
1041 This fact depends on type of the transaction, type of an object the transaction
1042 is attached to and may be other conditions, so this method is prefered over
1043 custom implementations.
1047 sub CurrentUserCanSee {
1050 # If it's a comment, we need to be extra special careful
1051 my $type = $self->__Value('Type');
1052 if ( $type eq 'Comment' ) {
1053 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1057 elsif ( $type eq 'CommentEmailRecord' ) {
1058 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1059 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1063 elsif ( $type eq 'EmailRecord' ) {
1064 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1068 # Make sure the user can see the custom field before showing that it changed
1069 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1070 my $cf = RT::CustomField->new( $self->CurrentUser );
1071 $cf->SetContextObject( $self->Object );
1072 $cf->Load( $cf_id );
1073 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1075 #if they ain't got rights to see, don't let em
1076 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1077 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1088 return $self->ObjectId;
1093 return $self->Object;
1098 if ( my $type = $self->__Value('ReferenceType')
1099 and my $id = $self->__Value('OldReference') )
1101 my $Object = $type->new($self->CurrentUser);
1102 $Object->Load( $id );
1103 return $Object->Content;
1106 return $self->__Value('OldValue');
1112 if ( my $type = $self->__Value('ReferenceType')
1113 and my $id = $self->__Value('NewReference') )
1115 my $Object = $type->new($self->CurrentUser);
1116 $Object->Load( $id );
1117 return $Object->Content;
1120 return $self->__Value('NewValue');
1126 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1127 $Object->Load($self->__Value('ObjectId'));
1131 sub FriendlyObjectType {
1133 my $type = $self->ObjectType or return undef;
1135 return $self->loc($type);
1138 =head2 UpdateCustomFields
1142 CustomField-<<Id>> => Value
1145 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1146 this transaction's custom fields
1150 sub UpdateCustomFields {
1154 # This method used to have an API that took a hash of a single
1155 # value "ARGSRef", which was a reference to a hash of arguments.
1156 # This was insane. The next few lines of code preserve that API
1157 # while giving us something saner.
1159 # TODO: 3.6: DEPRECATE OLD API
1163 if ($args{'ARGSRef'}) {
1164 $args = $args{ARGSRef};
1169 foreach my $arg ( keys %$args ) {
1172 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1173 next if $arg =~ /-Magic$/;
1174 next if $arg =~ /-TimeUnits$/;
1176 my $values = $args->{$arg};
1178 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1180 next unless (defined($value) && length($value));
1181 $self->_AddCustomFieldValue(
1184 RecordTransaction => 0,
1192 =head2 CustomFieldValues
1194 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1200 sub CustomFieldValues {
1204 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1206 # XXX: $field could be undef when we want fetch values for all CFs
1207 # do we want to cover this situation somehow here?
1208 unless ( defined $field && $field =~ /^\d+$/o ) {
1209 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1210 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1211 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1212 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1213 $field = $CFs->First->id if $CFs->First;
1216 return $self->SUPER::CustomFieldValues($field);
1221 =head2 CustomFieldLookupType
1223 Returns the RT::Transaction lookup type, which can
1224 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1229 sub CustomFieldLookupType {
1230 "RT::Queue-RT::Ticket-RT::Transaction";
1234 =head2 SquelchMailTo
1236 Similar to Ticket class SquelchMailTo method - returns a list of
1237 transaction's squelched addresses. As transactions are immutable, the
1238 list of squelched recipients cannot be modified after creation.
1244 return () unless $self->CurrentUserCanSee;
1245 return $self->Attributes->Named('SquelchMailTo');
1250 Returns the list of email addresses (as L<Email::Address> objects)
1251 that this transaction would send mail to. There may be duplicates.
1258 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1259 my $action = $scrip->ActionObj->Action;
1260 next unless $action->isa('RT::Action::SendEmail');
1262 foreach my $type (qw(To Cc Bcc)) {
1263 push @recipients, $action->$type();
1267 if ( $self->Rules ) {
1268 for my $rule (@{$self->Rules}) {
1269 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1270 my $data = $rule->{hints}{recipients};
1271 foreach my $type (qw(To Cc Bcc)) {
1272 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1279 =head2 DeferredRecipients($freq, $include_sent )
1281 Takes the following arguments:
1285 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1287 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1291 Returns an array of users who should now receive the notification that
1292 was recorded in this transaction. Returns an empty array if there were
1293 no deferred users, or if $include_sent was not specified and the deferred
1294 notifications have been sent.
1298 sub DeferredRecipients {
1301 my $include_sent = @_? shift : 0;
1303 my $attr = $self->FirstAttribute('DeferredRecipients');
1305 return () unless ($attr);
1307 my $deferred = $attr->Content;
1309 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1313 for my $user (keys %{$deferred->{$freq}}) {
1314 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1315 delete $deferred->{$freq}->{$user}
1318 # Now get our users. Easy.
1320 return keys %{ $deferred->{$freq} };
1325 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1329 'fast_update_p' => 1,
1330 'cache_for_sec' => 6000,
1335 =head2 ACLEquivalenceObjects
1337 This method returns a list of objects for which a user's rights also apply
1338 to this Transaction.
1340 This currently only applies to Transaction Custom Fields on Tickets, so we return
1341 the Ticket's Queue and the Ticket.
1343 This method is called from L<RT::Principal/HasRight>.
1347 sub ACLEquivalenceObjects {
1350 return unless $self->ObjectType eq 'RT::Ticket';
1351 my $object = $self->Object;
1352 return $object,$object->QueueObj;
1362 Returns the current value of id.
1363 (In the database, id is stored as int(11).)
1371 Returns the current value of ObjectType.
1372 (In the database, ObjectType is stored as varchar(64).)
1376 =head2 SetObjectType VALUE
1379 Set ObjectType to VALUE.
1380 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1381 (In the database, ObjectType will be stored as a varchar(64).)
1389 Returns the current value of ObjectId.
1390 (In the database, ObjectId is stored as int(11).)
1394 =head2 SetObjectId VALUE
1397 Set ObjectId to VALUE.
1398 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1399 (In the database, ObjectId will be stored as a int(11).)
1407 Returns the current value of TimeTaken.
1408 (In the database, TimeTaken is stored as int(11).)
1412 =head2 SetTimeTaken VALUE
1415 Set TimeTaken to VALUE.
1416 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1417 (In the database, TimeTaken will be stored as a int(11).)
1425 Returns the current value of Type.
1426 (In the database, Type is stored as varchar(20).)
1430 =head2 SetType VALUE
1434 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1435 (In the database, Type will be stored as a varchar(20).)
1443 Returns the current value of Field.
1444 (In the database, Field is stored as varchar(40).)
1448 =head2 SetField VALUE
1452 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1453 (In the database, Field will be stored as a varchar(40).)
1461 Returns the current value of OldValue.
1462 (In the database, OldValue is stored as varchar(255).)
1466 =head2 SetOldValue VALUE
1469 Set OldValue to VALUE.
1470 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1471 (In the database, OldValue will be stored as a varchar(255).)
1479 Returns the current value of NewValue.
1480 (In the database, NewValue is stored as varchar(255).)
1484 =head2 SetNewValue VALUE
1487 Set NewValue to VALUE.
1488 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1489 (In the database, NewValue will be stored as a varchar(255).)
1495 =head2 ReferenceType
1497 Returns the current value of ReferenceType.
1498 (In the database, ReferenceType is stored as varchar(255).)
1502 =head2 SetReferenceType VALUE
1505 Set ReferenceType to VALUE.
1506 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1507 (In the database, ReferenceType will be stored as a varchar(255).)
1515 Returns the current value of OldReference.
1516 (In the database, OldReference is stored as int(11).)
1520 =head2 SetOldReference VALUE
1523 Set OldReference to VALUE.
1524 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1525 (In the database, OldReference will be stored as a int(11).)
1533 Returns the current value of NewReference.
1534 (In the database, NewReference is stored as int(11).)
1538 =head2 SetNewReference VALUE
1541 Set NewReference to VALUE.
1542 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1543 (In the database, NewReference will be stored as a int(11).)
1551 Returns the current value of Data.
1552 (In the database, Data is stored as varchar(255).)
1556 =head2 SetData VALUE
1560 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1561 (In the database, Data will be stored as a varchar(255).)
1569 Returns the current value of Creator.
1570 (In the database, Creator is stored as int(11).)
1578 Returns the current value of Created.
1579 (In the database, Created is stored as datetime.)
1586 sub _CoreAccessible {
1590 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1592 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1594 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1596 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1598 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1600 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1602 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1604 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1606 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1608 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1610 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1612 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1614 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1616 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1621 RT::Base->_ImportOverlays();