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"));
136 #lets create our transaction
138 Type => $args{'Type'},
139 Data => $args{'Data'},
140 Field => $args{'Field'},
141 OldValue => $args{'OldValue'},
142 NewValue => $args{'NewValue'},
143 Created => $args{'Created'},
144 ObjectType => $args{'ObjectType'},
145 ObjectId => $args{'ObjectId'},
146 ReferenceType => $args{'ReferenceType'},
147 OldReference => $args{'OldReference'},
148 NewReference => $args{'NewReference'},
151 # Parameters passed in during an import that we probably don't want to touch, otherwise
152 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
153 $params{$attr} = $args{$attr} if ($args{$attr});
156 my $id = $self->SUPER::Create(%params);
158 if ( defined $args{'MIMEObj'} ) {
159 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
161 $RT::Logger->error("Couldn't add attachment: $msg");
162 return ( 0, $self->loc("Couldn't add attachment") );
166 # Set up any custom fields passed at creation. Has to happen
169 $self->UpdateCustomFields(%{ $args{'CustomFields'} });
172 Name => 'SquelchMailTo',
173 Content => RT::User->CanonicalizeEmailAddress($_)
174 ) for @{$args{'SquelchMailTo'} || []};
176 #Provide a way to turn off scrips if we need to
177 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
178 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
179 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
181 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
183 $self->{'scrips'}->Prepare(
184 Stage => 'TransactionCreate',
185 Type => $args{'Type'},
186 Ticket => $args{'ObjectId'},
187 Transaction => $self->id,
190 # Entry point of the rule system
191 my $ticket = RT::Ticket->new(RT->SystemUser);
192 $ticket->Load($args{'ObjectId'});
193 my $txn = RT::Transaction->new($RT::SystemUser);
194 $txn->Load($self->id);
196 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
197 Stage => 'TransactionCreate',
198 Type => $args{'Type'},
199 TicketObj => $ticket,
200 TransactionObj => $txn,
203 if ($args{'CommitScrips'} ) {
204 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
205 $self->{'scrips'}->Commit();
206 RT::Ruleset->CommitRules($rules);
210 return ( $id, $self->loc("Transaction Created") );
216 Returns the Scrips object for this transaction.
217 This routine is only useful on a freshly created transaction object.
218 Scrips do not get persisted to the database with transactions.
226 return($self->{'scrips'});
232 Returns the array of Rule objects for this transaction.
233 This routine is only useful on a freshly created transaction object.
234 Rules do not get persisted to the database with transactions.
242 return($self->{'rules'});
249 Delete this transaction. Currently DOES NOT CHECK ACLS
257 $RT::Handle->BeginTransaction();
259 my $attachments = $self->Attachments;
261 while (my $attachment = $attachments->Next) {
262 my ($id, $msg) = $attachment->Delete();
264 $RT::Handle->Rollback();
265 return($id, $self->loc("System Error: [_1]", $msg));
268 my ($id,$msg) = $self->SUPER::Delete();
270 $RT::Handle->Rollback();
271 return($id, $self->loc("System Error: [_1]", $msg));
273 $RT::Handle->Commit();
282 Returns the L<RT::Attachments> object which contains the "top-level" object
283 attachment for this transaction.
290 # XXX: Where is ACL check?
292 unless ( defined $self->{'message'} ) {
294 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
295 $self->{'message'}->Limit(
296 FIELD => 'TransactionId',
299 $self->{'message'}->ChildrenOf(0);
301 $self->{'message'}->GotoFirstItem;
303 return $self->{'message'};
308 =head2 Content PARAMHASH
310 If this transaction has attached mime objects, returns the body of the first
311 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
314 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
315 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
317 If $args{'Type'} is set to C<text/html>, this will return an HTML
318 part of the message, if available. Otherwise it looks for a text/plain
319 part. If $args{'Type'} is missing, it defaults to the value of
320 C<$RT::Transaction::PreferredContentType>, if that's missing too,
328 Type => $PreferredContentType || '',
331 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
336 if ( my $content_obj =
337 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
339 $content = $content_obj->Content ||'';
341 if ( lc $content_obj->ContentType eq 'text/html' ) {
342 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
344 if ($args{Type} ne 'text/html') {
345 my $tree = HTML::TreeBuilder->new_from_content( $content );
346 $content = HTML::FormatText->new(
354 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
355 if ($args{Type} eq 'text/html') {
356 # Extremely simple text->html converter
357 $content =~ s/&/&/g;
358 $content =~ s/</</g;
359 $content =~ s/>/>/g;
360 $content = "<pre>$content</pre>";
365 # If all else fails, return a message that we couldn't find any content
367 $content = $self->loc('This transaction appears to have no content');
370 if ( $args{'Quote'} ) {
372 # What's the longest line like?
374 foreach ( split ( /\n/, $content ) ) {
375 $max = length if length > $max;
378 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
379 require Text::Wrapper;
380 my $wrapper = Text::Wrapper->new(
381 columns => $args{'Wrap'},
382 body_start => ( $max > 70 * 3 ? ' ' : '' ),
385 $content = $wrapper->wrap($content);
388 $content =~ s/^/> /gm;
389 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
400 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
407 if (my $attach = $self->Attachments->First) {
408 return $attach->Addresses;
420 Returns the RT::Attachment object which contains the content for this Transaction
427 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
429 # If we don't have any content, return undef now.
430 # Get the set of toplevel attachments to this transaction.
432 my $Attachment = $args{'Attachment'};
434 $Attachment ||= $self->Attachments->First;
436 return undef unless ($Attachment);
438 # If it's a textual part, just return the body.
439 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
440 return ($Attachment);
443 # If it's a multipart object, first try returning the first part with preferred
444 # MIME type ('text/plain' by default).
446 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
447 my $kids = $Attachment->Children;
448 while (my $child = $kids->Next) {
449 my $ret = $self->ContentObj(%args, Attachment => $child);
450 return $ret if ($ret);
453 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
455 my $plain_parts = $Attachment->Children;
456 $plain_parts->ContentType( VALUE => $args{Type} );
457 $plain_parts->LimitNotEmpty;
459 # If we actully found a part, return its content
460 if ( my $first = $plain_parts->First ) {
465 # If that fails, return the first textual part which has some content.
466 my $all_parts = $self->Attachments;
467 while ( my $part = $all_parts->Next ) {
468 next unless RT::I18N::IsTextualContentType($part->ContentType)
474 # We found no content. suck
482 If this transaction has attached mime objects, returns the first one's subject
483 Otherwise, returns null
489 return undef unless my $first = $self->Attachments->First;
490 return $first->Subject;
497 Returns all the RT::Attachment objects which are attached
498 to this transaction. Takes an optional parameter, which is
499 a ContentType that Attachments should be restricted to.
506 if ( $self->{'attachments'} ) {
507 $self->{'attachments'}->GotoFirstItem;
508 return $self->{'attachments'};
511 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
513 unless ( $self->CurrentUserCanSee ) {
514 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
515 return $self->{'attachments'};
518 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
520 # Get the self->{'attachments'} in the order they're put into
521 # the database. Arguably, we should be returning a tree
522 # of self->{'attachments'}, not a set...but no current app seems to need
525 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
527 return $self->{'attachments'};
534 A private method used to attach a mime object to this transaction.
540 my $MIMEObject = shift;
542 unless ( defined $MIMEObject ) {
543 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
544 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
547 my $Attachment = RT::Attachment->new( $self->CurrentUser );
548 my ($id, $msg) = $Attachment->Create(
549 TransactionId => $self->Id,
550 Attachment => $MIMEObject
552 return ( $Attachment, $msg || $self->loc("Attachment created") );
560 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
561 # since it has less information available without looking to it's parent
562 # transaction. Check ACLs here before we go any further.
563 return unless $self->CurrentUserCanSee;
565 my $attachments = RT::Attachments->new( $self->CurrentUser );
566 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
567 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
568 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
569 $attachments->RowsPerPage(1);
571 my $top = $attachments->First;
574 my $entity = MIME::Entity->build(
575 Type => 'message/rfc822',
576 Description => 'transaction ' . $self->id,
577 Data => $top->ContentAsMIME(Children => 1)->as_string,
587 Returns a text string which describes this transaction
594 unless ( $self->CurrentUserCanSee ) {
595 return ( $self->loc("Permission Denied") );
598 unless ( defined $self->Type ) {
599 return ( $self->loc("No transaction type specified"));
602 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
607 =head2 BriefDescription
609 Returns a text string which briefly describes this transaction
613 sub BriefDescription {
616 unless ( $self->CurrentUserCanSee ) {
617 return ( $self->loc("Permission Denied") );
620 my $type = $self->Type; #cache this, rather than calling it 30 times
622 unless ( defined $type ) {
623 return $self->loc("No transaction type specified");
626 my $obj_type = $self->FriendlyObjectType;
628 if ( $type eq 'Create' ) {
629 return ( $self->loc( "[_1] created", $obj_type ) );
631 elsif ( $type eq 'Enabled' ) {
632 return ( $self->loc( "[_1] enabled", $obj_type ) );
634 elsif ( $type eq 'Disabled' ) {
635 return ( $self->loc( "[_1] disabled", $obj_type ) );
637 elsif ( $type =~ /Status/ ) {
638 if ( $self->Field eq 'Status' ) {
639 if ( $self->NewValue eq 'deleted' ) {
640 return ( $self->loc( "[_1] deleted", $obj_type ) );
645 "Status changed from [_1] to [_2]",
646 "'" . $self->loc( $self->OldValue ) . "'",
647 "'" . $self->loc( $self->NewValue ) . "'"
655 my $no_value = $self->loc("(no value)");
658 "[_1] changed from [_2] to [_3]",
660 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
661 "'" . $self->NewValue . "'"
665 elsif ( $type =~ /SystemError/ ) {
666 return $self->loc("System error");
668 elsif ( $type =~ /Forward Transaction/ ) {
669 return $self->loc( "Forwarded Transaction #[_1] to [_2]",
670 $self->Field, $self->Data );
672 elsif ( $type =~ /Forward Ticket/ ) {
673 return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
676 if ( my $code = $_BriefDescriptions{$type} ) {
677 return $code->($self);
681 "Default: [_1]/[_2] changed from [_3] to [_4]",
686 ? "'" . $self->OldValue . "'"
687 : $self->loc("(no value)")
689 "'" . $self->NewValue . "'"
693 %_BriefDescriptions = (
694 CommentEmailRecord => sub {
696 return $self->loc("Outgoing email about a comment recorded");
700 return $self->loc("Outgoing email recorded");
704 return $self->loc("Correspondence added");
708 return $self->loc("Comments added");
712 my $field = $self->loc('CustomField');
714 if ( $self->Field ) {
715 my $cf = RT::CustomField->new( $self->CurrentUser );
716 $cf->SetContextObject( $self->Object );
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|WillResolve/) {
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 # Defer to the object in question
1076 return $self->Object->CurrentUserCanSee("Transaction");
1082 return $self->ObjectId;
1087 return $self->Object;
1092 if ( my $type = $self->__Value('ReferenceType')
1093 and my $id = $self->__Value('OldReference') )
1095 my $Object = $type->new($self->CurrentUser);
1096 $Object->Load( $id );
1097 return $Object->Content;
1100 return $self->_Value('OldValue');
1106 if ( my $type = $self->__Value('ReferenceType')
1107 and my $id = $self->__Value('NewReference') )
1109 my $Object = $type->new($self->CurrentUser);
1110 $Object->Load( $id );
1111 return $Object->Content;
1114 return $self->_Value('NewValue');
1120 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1121 $Object->Load($self->__Value('ObjectId'));
1125 sub FriendlyObjectType {
1127 my $type = $self->ObjectType or return undef;
1129 return $self->loc($type);
1132 =head2 UpdateCustomFields
1136 CustomField-<<Id>> => Value
1139 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1140 this transaction's custom fields
1144 sub UpdateCustomFields {
1148 # This method used to have an API that took a hash of a single
1149 # value "ARGSRef", which was a reference to a hash of arguments.
1150 # This was insane. The next few lines of code preserve that API
1151 # while giving us something saner.
1153 # TODO: 3.6: DEPRECATE OLD API
1157 if ($args{'ARGSRef'}) {
1158 $args = $args{ARGSRef};
1163 foreach my $arg ( keys %$args ) {
1166 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1167 next if $arg =~ /-Magic$/;
1168 next if $arg =~ /-TimeUnits$/;
1170 my $values = $args->{$arg};
1172 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1174 next unless (defined($value) && length($value));
1175 $self->_AddCustomFieldValue(
1178 RecordTransaction => 0,
1186 =head2 CustomFieldValues
1188 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1194 sub CustomFieldValues {
1198 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1200 # XXX: $field could be undef when we want fetch values for all CFs
1201 # do we want to cover this situation somehow here?
1202 unless ( defined $field && $field =~ /^\d+$/o ) {
1203 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1204 $CFs->SetContextObject( $self->Object );
1205 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1206 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1207 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1208 $field = $CFs->First->id if $CFs->First;
1211 return $self->SUPER::CustomFieldValues($field);
1216 =head2 CustomFieldLookupType
1218 Returns the RT::Transaction lookup type, which can
1219 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1224 sub CustomFieldLookupType {
1225 "RT::Queue-RT::Ticket-RT::Transaction";
1229 =head2 SquelchMailTo
1231 Similar to Ticket class SquelchMailTo method - returns a list of
1232 transaction's squelched addresses. As transactions are immutable, the
1233 list of squelched recipients cannot be modified after creation.
1239 return () unless $self->CurrentUserCanSee;
1240 return $self->Attributes->Named('SquelchMailTo');
1245 Returns the list of email addresses (as L<Email::Address> objects)
1246 that this transaction would send mail to. There may be duplicates.
1253 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1254 my $action = $scrip->ActionObj->Action;
1255 next unless $action->isa('RT::Action::SendEmail');
1257 foreach my $type (qw(To Cc Bcc)) {
1258 push @recipients, $action->$type();
1262 if ( $self->Rules ) {
1263 for my $rule (@{$self->Rules}) {
1264 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1265 my $data = $rule->{hints}{recipients};
1266 foreach my $type (qw(To Cc Bcc)) {
1267 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1274 =head2 DeferredRecipients($freq, $include_sent )
1276 Takes the following arguments:
1280 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1282 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1286 Returns an array of users who should now receive the notification that
1287 was recorded in this transaction. Returns an empty array if there were
1288 no deferred users, or if $include_sent was not specified and the deferred
1289 notifications have been sent.
1293 sub DeferredRecipients {
1296 my $include_sent = @_? shift : 0;
1298 my $attr = $self->FirstAttribute('DeferredRecipients');
1300 return () unless ($attr);
1302 my $deferred = $attr->Content;
1304 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1308 for my $user (keys %{$deferred->{$freq}}) {
1309 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1310 delete $deferred->{$freq}->{$user}
1313 # Now get our users. Easy.
1315 return keys %{ $deferred->{$freq} };
1320 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1324 'fast_update_p' => 1,
1325 'cache_for_sec' => 6000,
1330 =head2 ACLEquivalenceObjects
1332 This method returns a list of objects for which a user's rights also apply
1333 to this Transaction.
1335 This currently only applies to Transaction Custom Fields on Tickets, so we return
1336 the Ticket's Queue and the Ticket.
1338 This method is called from L<RT::Principal/HasRight>.
1342 sub ACLEquivalenceObjects {
1345 return unless $self->ObjectType eq 'RT::Ticket';
1346 my $object = $self->Object;
1347 return $object,$object->QueueObj;
1357 Returns the current value of id.
1358 (In the database, id is stored as int(11).)
1366 Returns the current value of ObjectType.
1367 (In the database, ObjectType is stored as varchar(64).)
1371 =head2 SetObjectType VALUE
1374 Set ObjectType to VALUE.
1375 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1376 (In the database, ObjectType will be stored as a varchar(64).)
1384 Returns the current value of ObjectId.
1385 (In the database, ObjectId is stored as int(11).)
1389 =head2 SetObjectId VALUE
1392 Set ObjectId to VALUE.
1393 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1394 (In the database, ObjectId will be stored as a int(11).)
1402 Returns the current value of TimeTaken.
1403 (In the database, TimeTaken is stored as int(11).)
1407 =head2 SetTimeTaken VALUE
1410 Set TimeTaken to VALUE.
1411 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1412 (In the database, TimeTaken will be stored as a int(11).)
1420 Returns the current value of Type.
1421 (In the database, Type is stored as varchar(20).)
1425 =head2 SetType VALUE
1429 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1430 (In the database, Type will be stored as a varchar(20).)
1438 Returns the current value of Field.
1439 (In the database, Field is stored as varchar(40).)
1443 =head2 SetField VALUE
1447 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1448 (In the database, Field will be stored as a varchar(40).)
1456 Returns the current value of OldValue.
1457 (In the database, OldValue is stored as varchar(255).)
1461 =head2 SetOldValue VALUE
1464 Set OldValue to VALUE.
1465 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1466 (In the database, OldValue will be stored as a varchar(255).)
1474 Returns the current value of NewValue.
1475 (In the database, NewValue is stored as varchar(255).)
1479 =head2 SetNewValue VALUE
1482 Set NewValue to VALUE.
1483 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1484 (In the database, NewValue will be stored as a varchar(255).)
1490 =head2 ReferenceType
1492 Returns the current value of ReferenceType.
1493 (In the database, ReferenceType is stored as varchar(255).)
1497 =head2 SetReferenceType VALUE
1500 Set ReferenceType to VALUE.
1501 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1502 (In the database, ReferenceType will be stored as a varchar(255).)
1510 Returns the current value of OldReference.
1511 (In the database, OldReference is stored as int(11).)
1515 =head2 SetOldReference VALUE
1518 Set OldReference to VALUE.
1519 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1520 (In the database, OldReference will be stored as a int(11).)
1528 Returns the current value of NewReference.
1529 (In the database, NewReference is stored as int(11).)
1533 =head2 SetNewReference VALUE
1536 Set NewReference to VALUE.
1537 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1538 (In the database, NewReference will be stored as a int(11).)
1546 Returns the current value of Data.
1547 (In the database, Data is stored as varchar(255).)
1551 =head2 SetData VALUE
1555 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1556 (In the database, Data will be stored as a varchar(255).)
1564 Returns the current value of Creator.
1565 (In the database, Creator is stored as int(11).)
1573 Returns the current value of Created.
1574 (In the database, Created is stored as datetime.)
1581 sub _CoreAccessible {
1585 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1587 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1589 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1591 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1593 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1595 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1597 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1599 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1601 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1603 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1605 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1607 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1609 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1611 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1616 RT::Base->_ImportOverlays();