1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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,
128 $args{ObjectId} ||= $args{Ticket};
130 #if we didn't specify a ticket, we need to bail
131 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
132 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
137 #lets create our transaction
139 Type => $args{'Type'},
140 Data => $args{'Data'},
141 Field => $args{'Field'},
142 OldValue => $args{'OldValue'},
143 NewValue => $args{'NewValue'},
144 Created => $args{'Created'},
145 ObjectType => $args{'ObjectType'},
146 ObjectId => $args{'ObjectId'},
147 ReferenceType => $args{'ReferenceType'},
148 OldReference => $args{'OldReference'},
149 NewReference => $args{'NewReference'},
152 # Parameters passed in during an import that we probably don't want to touch, otherwise
153 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
154 $params{$attr} = $args{$attr} if ($args{$attr});
157 my $id = $self->SUPER::Create(%params);
159 if ( defined $args{'MIMEObj'} ) {
160 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
162 $RT::Logger->error("Couldn't add attachment: $msg");
163 return ( 0, $self->loc("Couldn't add attachment") );
168 Name => 'SquelchMailTo',
169 Content => RT::User->CanonicalizeEmailAddress($_)
170 ) for @{$args{'SquelchMailTo'} || []};
172 #Provide a way to turn off scrips if we need to
173 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
174 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
175 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
177 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
179 $self->{'scrips'}->Prepare(
180 Stage => 'TransactionCreate',
181 Type => $args{'Type'},
182 Ticket => $args{'ObjectId'},
183 Transaction => $self->id,
186 # Entry point of the rule system
187 my $ticket = RT::Ticket->new(RT->SystemUser);
188 $ticket->Load($args{'ObjectId'});
189 my $txn = RT::Transaction->new($RT::SystemUser);
190 $txn->Load($self->id);
192 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
193 Stage => 'TransactionCreate',
194 Type => $args{'Type'},
195 TicketObj => $ticket,
196 TransactionObj => $txn,
199 if ($args{'CommitScrips'} ) {
200 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
201 $self->{'scrips'}->Commit();
202 RT::Ruleset->CommitRules($rules);
206 return ( $id, $self->loc("Transaction Created") );
212 Returns the Scrips object for this transaction.
213 This routine is only useful on a freshly created transaction object.
214 Scrips do not get persisted to the database with transactions.
222 return($self->{'scrips'});
228 Returns the array of Rule objects for this transaction.
229 This routine is only useful on a freshly created transaction object.
230 Rules do not get persisted to the database with transactions.
238 return($self->{'rules'});
245 Delete this transaction. Currently DOES NOT CHECK ACLS
253 $RT::Handle->BeginTransaction();
255 my $attachments = $self->Attachments;
257 while (my $attachment = $attachments->Next) {
258 my ($id, $msg) = $attachment->Delete();
260 $RT::Handle->Rollback();
261 return($id, $self->loc("System Error: [_1]", $msg));
264 my ($id,$msg) = $self->SUPER::Delete();
266 $RT::Handle->Rollback();
267 return($id, $self->loc("System Error: [_1]", $msg));
269 $RT::Handle->Commit();
278 Returns the L<RT::Attachments> object which contains the "top-level" object
279 attachment for this transaction.
286 # XXX: Where is ACL check?
288 unless ( defined $self->{'message'} ) {
290 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
291 $self->{'message'}->Limit(
292 FIELD => 'TransactionId',
295 $self->{'message'}->ChildrenOf(0);
297 $self->{'message'}->GotoFirstItem;
299 return $self->{'message'};
304 =head2 Content PARAMHASH
306 If this transaction has attached mime objects, returns the body of the first
307 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
310 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
311 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
313 If $args{'Type'} is set to C<text/html>, this will return an HTML
314 part of the message, if available. Otherwise it looks for a text/plain
315 part. If $args{'Type'} is missing, it defaults to the value of
316 C<$RT::Transaction::PreferredContentType>, if that's missing too,
324 Type => $PreferredContentType || '',
331 if ( my $content_obj =
332 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
334 $content = $content_obj->Content ||'';
336 if ( lc $content_obj->ContentType eq 'text/html' ) {
337 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
339 if ($args{Type} ne 'text/html') {
340 my $tree = HTML::TreeBuilder->new_from_content( $content );
341 $content = HTML::FormatText->new(
349 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
350 if ($args{Type} eq 'text/html') {
351 # Extremely simple text->html converter
352 $content =~ s/&/&/g;
353 $content =~ s/</</g;
354 $content =~ s/>/>/g;
355 $content = "<pre>$content</pre>";
360 # If all else fails, return a message that we couldn't find any content
362 $content = $self->loc('This transaction appears to have no content');
365 if ( $args{'Quote'} ) {
367 # What's the longest line like?
369 foreach ( split ( /\n/, $content ) ) {
370 $max = length if length > $max;
374 require Text::Wrapper;
375 my $wrapper = Text::Wrapper->new(
376 columns => $args{'Wrap'},
377 body_start => ( $max > 70 * 3 ? ' ' : '' ),
380 $content = $wrapper->wrap($content);
383 $content =~ s/^/> /gm;
384 $content = $self->QuoteHeader . "\n$content\n\n";
392 Returns text prepended to content when transaction is quoted
393 (see C<Quote> argument in L</Content>). By default returns
394 localized "On <date> <user name> wrote:\n".
400 return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
406 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
413 if (my $attach = $self->Attachments->First) {
414 return $attach->Addresses;
426 Returns the RT::Attachment object which contains the content for this Transaction
433 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
435 # If we don't have any content, return undef now.
436 # Get the set of toplevel attachments to this transaction.
438 my $Attachment = $args{'Attachment'};
440 $Attachment ||= $self->Attachments->First;
442 return undef unless ($Attachment);
444 # If it's a textual part, just return the body.
445 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
446 return ($Attachment);
449 # If it's a multipart object, first try returning the first part with preferred
450 # MIME type ('text/plain' by default).
452 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
453 my $kids = $Attachment->Children;
454 while (my $child = $kids->Next) {
455 my $ret = $self->ContentObj(%args, Attachment => $child);
456 return $ret if ($ret);
459 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
461 my $plain_parts = $Attachment->Children;
462 $plain_parts->ContentType( VALUE => $args{Type} );
463 $plain_parts->LimitNotEmpty;
465 # If we actully found a part, return its content
466 if ( my $first = $plain_parts->First ) {
471 # If that fails, return the first textual part which has some content.
472 my $all_parts = $self->Attachments;
473 while ( my $part = $all_parts->Next ) {
474 next unless RT::I18N::IsTextualContentType($part->ContentType)
480 # We found no content. suck
488 If this transaction has attached mime objects, returns the first one's subject
489 Otherwise, returns null
495 return undef unless my $first = $self->Attachments->First;
496 return $first->Subject;
503 Returns all the RT::Attachment objects which are attached
504 to this transaction. Takes an optional parameter, which is
505 a ContentType that Attachments should be restricted to.
512 if ( $self->{'attachments'} ) {
513 $self->{'attachments'}->GotoFirstItem;
514 return $self->{'attachments'};
517 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
519 unless ( $self->CurrentUserCanSee ) {
520 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
521 return $self->{'attachments'};
524 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
526 # Get the self->{'attachments'} in the order they're put into
527 # the database. Arguably, we should be returning a tree
528 # of self->{'attachments'}, not a set...but no current app seems to need
531 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
533 return $self->{'attachments'};
540 A private method used to attach a mime object to this transaction.
546 my $MIMEObject = shift;
548 unless ( defined $MIMEObject ) {
549 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
550 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
553 my $Attachment = RT::Attachment->new( $self->CurrentUser );
554 my ($id, $msg) = $Attachment->Create(
555 TransactionId => $self->Id,
556 Attachment => $MIMEObject
558 return ( $Attachment, $msg || $self->loc("Attachment created") );
566 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
567 # since it has less information available without looking to it's parent
568 # transaction. Check ACLs here before we go any further.
569 return unless $self->CurrentUserCanSee;
571 my $attachments = RT::Attachments->new( $self->CurrentUser );
572 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
573 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
574 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
575 $attachments->RowsPerPage(1);
577 my $top = $attachments->First;
580 my $entity = MIME::Entity->build(
581 Type => 'message/rfc822',
582 Description => 'transaction ' . $self->id,
583 Data => $top->ContentAsMIME(Children => 1)->as_string,
593 Returns a text string which describes this transaction
600 unless ( $self->CurrentUserCanSee ) {
601 return ( $self->loc("Permission Denied") );
604 unless ( defined $self->Type ) {
605 return ( $self->loc("No transaction type specified"));
608 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
613 =head2 BriefDescription
615 Returns a text string which briefly describes this transaction
619 sub BriefDescription {
622 unless ( $self->CurrentUserCanSee ) {
623 return ( $self->loc("Permission Denied") );
626 my $type = $self->Type; #cache this, rather than calling it 30 times
628 unless ( defined $type ) {
629 return $self->loc("No transaction type specified");
632 my $obj_type = $self->FriendlyObjectType;
634 if ( $type eq 'Create' ) {
635 return ( $self->loc( "[_1] created", $obj_type ) );
637 elsif ( $type eq 'Enabled' ) {
638 return ( $self->loc( "[_1] enabled", $obj_type ) );
640 elsif ( $type eq 'Disabled' ) {
641 return ( $self->loc( "[_1] disabled", $obj_type ) );
643 elsif ( $type =~ /Status/ ) {
644 if ( $self->Field eq 'Status' ) {
645 if ( $self->NewValue eq 'deleted' ) {
646 return ( $self->loc( "[_1] deleted", $obj_type ) );
651 "Status changed from [_1] to [_2]",
652 "'" . $self->loc( $self->OldValue ) . "'",
653 "'" . $self->loc( $self->NewValue ) . "'"
661 my $no_value = $self->loc("(no value)");
664 "[_1] changed from [_2] to [_3]",
666 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
667 "'" . $self->NewValue . "'"
671 elsif ( $type =~ /SystemError/ ) {
672 return $self->loc("System error");
674 elsif ( $type =~ /Forward Transaction/ ) {
675 return $self->loc( "Forwarded Transaction #[_1] to [_2]",
676 $self->Field, $self->Data );
678 elsif ( $type =~ /Forward Ticket/ ) {
679 return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
682 if ( my $code = $_BriefDescriptions{$type} ) {
683 return $code->($self);
687 "Default: [_1]/[_2] changed from [_3] to [_4]",
692 ? "'" . $self->OldValue . "'"
693 : $self->loc("(no value)")
695 "'" . $self->NewValue . "'"
699 %_BriefDescriptions = (
700 CommentEmailRecord => sub {
702 return $self->loc("Outgoing email about a comment recorded");
706 return $self->loc("Outgoing email recorded");
710 return $self->loc("Correspondence added");
714 return $self->loc("Comments added");
718 my $field = $self->loc('CustomField');
720 if ( $self->Field ) {
721 my $cf = RT::CustomField->new( $self->CurrentUser );
722 $cf->SetContextObject( $self->Object );
723 $cf->Load( $self->Field );
724 $field = $cf->Name();
725 $field = $self->loc('a custom field') if !defined($field);
728 my $new = $self->NewValue;
729 my $old = $self->OldValue;
731 if ( !defined($old) || $old eq '' ) {
732 return $self->loc("[_1] [_2] added", $field, $new);
734 elsif ( !defined($new) || $new eq '' ) {
735 return $self->loc("[_1] [_2] deleted", $field, $old);
738 return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
743 return $self->loc("Untaken");
747 return $self->loc("Taken");
751 my $Old = RT::User->new( $self->CurrentUser );
752 $Old->Load( $self->OldValue );
753 my $New = RT::User->new( $self->CurrentUser );
754 $New->Load( $self->NewValue );
756 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
760 my $Old = RT::User->new( $self->CurrentUser );
761 $Old->Load( $self->OldValue );
762 return $self->loc("Stolen from [_1]", $Old->Name);
766 my $New = RT::User->new( $self->CurrentUser );
767 $New->Load( $self->NewValue );
768 return $self->loc( "Given to [_1]", $New->Name );
772 my $principal = RT::Principal->new($self->CurrentUser);
773 $principal->Load($self->NewValue);
774 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
778 my $principal = RT::Principal->new($self->CurrentUser);
779 $principal->Load($self->OldValue);
780 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
784 return $self->loc( "Subject changed to [_1]", $self->Data );
789 if ( $self->NewValue ) {
790 my $URI = RT::URI->new( $self->CurrentUser );
791 if ( $URI->FromURI( $self->NewValue ) ) {
792 $value = $URI->Resolver->AsString;
795 $value = $self->NewValue;
797 if ( $self->Field eq 'DependsOn' ) {
798 return $self->loc( "Dependency on [_1] added", $value );
800 elsif ( $self->Field eq 'DependedOnBy' ) {
801 return $self->loc( "Dependency by [_1] added", $value );
804 elsif ( $self->Field eq 'RefersTo' ) {
805 return $self->loc( "Reference to [_1] added", $value );
807 elsif ( $self->Field eq 'ReferredToBy' ) {
808 return $self->loc( "Reference by [_1] added", $value );
810 elsif ( $self->Field eq 'MemberOf' ) {
811 return $self->loc( "Membership in [_1] added", $value );
813 elsif ( $self->Field eq 'HasMember' ) {
814 return $self->loc( "Member [_1] added", $value );
816 elsif ( $self->Field eq 'MergedInto' ) {
817 return $self->loc( "Merged into [_1]", $value );
821 return ( $self->Data );
827 if ( $self->OldValue ) {
828 my $URI = RT::URI->new( $self->CurrentUser );
829 if ( $URI->FromURI( $self->OldValue ) ){
830 $value = $URI->Resolver->AsString;
833 $value = $self->OldValue;
836 if ( $self->Field eq 'DependsOn' ) {
837 return $self->loc( "Dependency on [_1] deleted", $value );
839 elsif ( $self->Field eq 'DependedOnBy' ) {
840 return $self->loc( "Dependency by [_1] deleted", $value );
843 elsif ( $self->Field eq 'RefersTo' ) {
844 return $self->loc( "Reference to [_1] deleted", $value );
846 elsif ( $self->Field eq 'ReferredToBy' ) {
847 return $self->loc( "Reference by [_1] deleted", $value );
849 elsif ( $self->Field eq 'MemberOf' ) {
850 return $self->loc( "Membership in [_1] deleted", $value );
852 elsif ( $self->Field eq 'HasMember' ) {
853 return $self->loc( "Member [_1] deleted", $value );
857 return ( $self->Data );
862 if ( $self->Field eq 'Told' ) {
863 my $t1 = RT::Date->new($self->CurrentUser);
864 $t1->Set(Format => 'ISO', Value => $self->NewValue);
865 my $t2 = RT::Date->new($self->CurrentUser);
866 $t2->Set(Format => 'ISO', Value => $self->OldValue);
867 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
870 return $self->loc( "[_1] changed from [_2] to [_3]",
871 $self->loc($self->Field),
872 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
877 if ( $self->Field eq 'Password' ) {
878 return $self->loc('Password changed');
880 elsif ( $self->Field eq 'Queue' ) {
881 my $q1 = RT::Queue->new( $self->CurrentUser );
882 $q1->Load( $self->OldValue );
883 my $q2 = RT::Queue->new( $self->CurrentUser );
884 $q2->Load( $self->NewValue );
885 return $self->loc("[_1] changed from [_2] to [_3]",
886 $self->loc($self->Field) , $q1->Name , $q2->Name);
889 # Write the date/time change at local time:
890 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
891 my $t1 = RT::Date->new($self->CurrentUser);
892 $t1->Set(Format => 'ISO', Value => $self->NewValue);
893 my $t2 = RT::Date->new($self->CurrentUser);
894 $t2->Set(Format => 'ISO', Value => $self->OldValue);
895 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
897 elsif ( $self->Field eq 'Owner' ) {
898 my $Old = RT::User->new( $self->CurrentUser );
899 $Old->Load( $self->OldValue );
900 my $New = RT::User->new( $self->CurrentUser );
901 $New->Load( $self->NewValue );
903 if ( $Old->id == RT->Nobody->id ) {
904 if ( $New->id == $self->Creator ) {
905 return $self->loc("Taken");
908 return $self->loc( "Given to [_1]", $New->Name );
912 if ( $New->id == $self->Creator ) {
913 return $self->loc("Stolen from [_1]", $Old->Name);
915 elsif ( $Old->id == $self->Creator ) {
916 if ( $New->id == RT->Nobody->id ) {
917 return $self->loc("Untaken");
920 return $self->loc( "Given to [_1]", $New->Name );
925 "Owner forcibly changed from [_1] to [_2]",
926 $Old->Name, $New->Name );
931 return $self->loc( "[_1] changed from [_2] to [_3]",
932 $self->loc($self->Field),
933 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
936 PurgeTransaction => sub {
938 return $self->loc("Transaction [_1] purged", $self->Data);
942 my $ticket = RT::Ticket->new($self->CurrentUser);
943 $ticket->Load($self->NewValue);
944 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
946 OpenReminder => sub {
948 my $ticket = RT::Ticket->new($self->CurrentUser);
949 $ticket->Load($self->NewValue);
950 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
953 ResolveReminder => sub {
955 my $ticket = RT::Ticket->new($self->CurrentUser);
956 $ticket->Load($self->NewValue);
957 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
968 Returns true if the creator of the transaction is a requestor of the ticket.
969 Returns false otherwise
975 $self->ObjectType eq 'RT::Ticket' or return undef;
976 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
981 sub _OverlayAccessible {
984 ObjectType => { public => 1},
985 ObjectId => { public => 1},
995 return ( 0, $self->loc('Transactions are immutable') );
1002 Takes the name of a table column.
1003 Returns its value as a string, if the user passes an ACL check
1011 #if the field is public, return it.
1012 if ( $self->_Accessible( $field, 'public' ) ) {
1013 return $self->SUPER::_Value( $field );
1016 unless ( $self->CurrentUserCanSee ) {
1020 return $self->SUPER::_Value( $field );
1025 =head2 CurrentUserHasRight RIGHT
1027 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1032 sub CurrentUserHasRight {
1035 return $self->CurrentUser->HasRight(
1037 Object => $self->Object
1041 =head2 CurrentUserCanSee
1043 Returns true if current user has rights to see this particular transaction.
1045 This fact depends on type of the transaction, type of an object the transaction
1046 is attached to and may be other conditions, so this method is prefered over
1047 custom implementations.
1051 sub CurrentUserCanSee {
1054 # If it's a comment, we need to be extra special careful
1055 my $type = $self->__Value('Type');
1056 if ( $type eq 'Comment' ) {
1057 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1061 elsif ( $type eq 'CommentEmailRecord' ) {
1062 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1063 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1067 elsif ( $type eq 'EmailRecord' ) {
1068 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1072 # Make sure the user can see the custom field before showing that it changed
1073 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1074 my $cf = RT::CustomField->new( $self->CurrentUser );
1075 $cf->SetContextObject( $self->Object );
1076 $cf->Load( $cf_id );
1077 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1080 # Transactions that might have changed the ->Object's visibility to
1081 # the current user are marked readable
1082 return 1 if $self->{ _object_is_readable };
1084 # Defer to the object in question
1085 return $self->Object->CurrentUserCanSee("Transaction");
1091 return $self->ObjectId;
1096 return $self->Object;
1101 if ( my $type = $self->__Value('ReferenceType')
1102 and my $id = $self->__Value('OldReference') )
1104 my $Object = $type->new($self->CurrentUser);
1105 $Object->Load( $id );
1106 return $Object->Content;
1109 return $self->_Value('OldValue');
1115 if ( my $type = $self->__Value('ReferenceType')
1116 and my $id = $self->__Value('NewReference') )
1118 my $Object = $type->new($self->CurrentUser);
1119 $Object->Load( $id );
1120 return $Object->Content;
1123 return $self->_Value('NewValue');
1129 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1130 $Object->Load($self->__Value('ObjectId'));
1134 sub FriendlyObjectType {
1136 my $type = $self->ObjectType or return undef;
1138 return $self->loc($type);
1141 =head2 UpdateCustomFields
1145 CustomField-<<Id>> => Value
1148 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1149 this transaction's custom fields
1153 sub UpdateCustomFields {
1157 # This method used to have an API that took a hash of a single
1158 # value "ARGSRef", which was a reference to a hash of arguments.
1159 # This was insane. The next few lines of code preserve that API
1160 # while giving us something saner.
1162 # TODO: 3.6: DEPRECATE OLD API
1166 if ($args{'ARGSRef'}) {
1167 $args = $args{ARGSRef};
1172 foreach my $arg ( keys %$args ) {
1175 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1176 next if $arg =~ /-Magic$/;
1178 my $values = $args->{$arg};
1180 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1182 next unless (defined($value) && length($value));
1183 $self->_AddCustomFieldValue(
1186 RecordTransaction => 0,
1192 =head2 LoadCustomFieldByIdentifier
1194 Finds and returns the custom field of the given name for the
1195 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1196 look for queue-specific CFs before global ones.
1200 sub LoadCustomFieldByIdentifier {
1204 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1205 if ref $field or $field =~ /^\d+$/;
1207 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1208 unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1210 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1211 $CFs->SetContextObject( $self->Object );
1212 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1213 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1214 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1215 return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1218 =head2 CustomFieldLookupType
1220 Returns the RT::Transaction lookup type, which can
1221 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1226 sub CustomFieldLookupType {
1227 "RT::Queue-RT::Ticket-RT::Transaction";
1231 =head2 SquelchMailTo
1233 Similar to Ticket class SquelchMailTo method - returns a list of
1234 transaction's squelched addresses. As transactions are immutable, the
1235 list of squelched recipients cannot be modified after creation.
1241 return () unless $self->CurrentUserCanSee;
1242 return $self->Attributes->Named('SquelchMailTo');
1247 Returns the list of email addresses (as L<Email::Address> objects)
1248 that this transaction would send mail to. There may be duplicates.
1255 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1256 my $action = $scrip->ActionObj->Action;
1257 next unless $action->isa('RT::Action::SendEmail');
1259 foreach my $type (qw(To Cc Bcc)) {
1260 push @recipients, $action->$type();
1264 if ( $self->Rules ) {
1265 for my $rule (@{$self->Rules}) {
1266 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1267 my $data = $rule->{hints}{recipients};
1268 foreach my $type (qw(To Cc Bcc)) {
1269 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1276 =head2 DeferredRecipients($freq, $include_sent )
1278 Takes the following arguments:
1282 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1284 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1288 Returns an array of users who should now receive the notification that
1289 was recorded in this transaction. Returns an empty array if there were
1290 no deferred users, or if $include_sent was not specified and the deferred
1291 notifications have been sent.
1295 sub DeferredRecipients {
1298 my $include_sent = @_? shift : 0;
1300 my $attr = $self->FirstAttribute('DeferredRecipients');
1302 return () unless ($attr);
1304 my $deferred = $attr->Content;
1306 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1310 for my $user (keys %{$deferred->{$freq}}) {
1311 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1312 delete $deferred->{$freq}->{$user}
1315 # Now get our users. Easy.
1317 return keys %{ $deferred->{$freq} };
1322 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1326 'fast_update_p' => 1,
1327 'cache_for_sec' => 6000,
1332 =head2 ACLEquivalenceObjects
1334 This method returns a list of objects for which a user's rights also apply
1335 to this Transaction.
1337 This currently only applies to Transaction Custom Fields on Tickets, so we return
1338 the Ticket's Queue and the Ticket.
1340 This method is called from L<RT::Principal/HasRight>.
1344 sub ACLEquivalenceObjects {
1347 return unless $self->ObjectType eq 'RT::Ticket';
1348 my $object = $self->Object;
1349 return $object,$object->QueueObj;
1359 Returns the current value of id.
1360 (In the database, id is stored as int(11).)
1368 Returns the current value of ObjectType.
1369 (In the database, ObjectType is stored as varchar(64).)
1373 =head2 SetObjectType VALUE
1376 Set ObjectType to VALUE.
1377 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1378 (In the database, ObjectType will be stored as a varchar(64).)
1386 Returns the current value of ObjectId.
1387 (In the database, ObjectId is stored as int(11).)
1391 =head2 SetObjectId VALUE
1394 Set ObjectId to VALUE.
1395 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1396 (In the database, ObjectId will be stored as a int(11).)
1404 Returns the current value of TimeTaken.
1405 (In the database, TimeTaken is stored as int(11).)
1409 =head2 SetTimeTaken VALUE
1412 Set TimeTaken to VALUE.
1413 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1414 (In the database, TimeTaken will be stored as a int(11).)
1422 Returns the current value of Type.
1423 (In the database, Type is stored as varchar(20).)
1427 =head2 SetType VALUE
1431 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1432 (In the database, Type will be stored as a varchar(20).)
1440 Returns the current value of Field.
1441 (In the database, Field is stored as varchar(40).)
1445 =head2 SetField VALUE
1449 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1450 (In the database, Field will be stored as a varchar(40).)
1458 Returns the current value of OldValue.
1459 (In the database, OldValue is stored as varchar(255).)
1463 =head2 SetOldValue VALUE
1466 Set OldValue to VALUE.
1467 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1468 (In the database, OldValue will be stored as a varchar(255).)
1476 Returns the current value of NewValue.
1477 (In the database, NewValue is stored as varchar(255).)
1481 =head2 SetNewValue VALUE
1484 Set NewValue to VALUE.
1485 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1486 (In the database, NewValue will be stored as a varchar(255).)
1492 =head2 ReferenceType
1494 Returns the current value of ReferenceType.
1495 (In the database, ReferenceType is stored as varchar(255).)
1499 =head2 SetReferenceType VALUE
1502 Set ReferenceType to VALUE.
1503 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1504 (In the database, ReferenceType will be stored as a varchar(255).)
1512 Returns the current value of OldReference.
1513 (In the database, OldReference is stored as int(11).)
1517 =head2 SetOldReference VALUE
1520 Set OldReference to VALUE.
1521 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1522 (In the database, OldReference will be stored as a int(11).)
1530 Returns the current value of NewReference.
1531 (In the database, NewReference is stored as int(11).)
1535 =head2 SetNewReference VALUE
1538 Set NewReference to VALUE.
1539 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1540 (In the database, NewReference will be stored as a int(11).)
1548 Returns the current value of Data.
1549 (In the database, Data is stored as varchar(255).)
1553 =head2 SetData VALUE
1557 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1558 (In the database, Data will be stored as a varchar(255).)
1566 Returns the current value of Creator.
1567 (In the database, Creator is stored as int(11).)
1575 Returns the current value of Created.
1576 (In the database, Created is stored as datetime.)
1583 sub _CoreAccessible {
1587 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1589 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1595 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1597 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', 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 => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', 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 => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1609 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1611 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1613 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1618 RT::Base->_ImportOverlays();