1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # }}} END BPS TAGGED BLOCK
48 RT::Transaction - RT\'s transaction object
58 Each RT::Transaction describes an atomic change to a ticket object
59 or an update to an RT::Ticket object.
60 It can have arbitrary MIME attachments.
67 ok(require RT::Transaction);
74 no warnings qw(redefine);
76 use vars qw( %_BriefDescriptions );
85 Create a new transaction.
87 This routine should _never_ be called anything other Than RT::Ticket. It should not be called
88 from client code. Ever. Not ever. If you do this, we will hunt you down. and break your kneecaps.
89 Then the unpleasant stuff will start.
91 TODO: Document what gets passed to this
112 #if we didn't specify a ticket, we need to bail
113 unless ( $args{'Ticket'} ) {
114 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify a ticket id"));
119 #lets create our transaction
120 my %params = (Ticket => $args{'Ticket'},
121 Type => $args{'Type'},
122 Data => $args{'Data'},
123 Field => $args{'Field'},
124 OldValue => $args{'OldValue'},
125 NewValue => $args{'NewValue'},
126 Created => $args{'Created'}
129 # Parameters passed in during an import that we probably don't want to touch, otherwise
130 foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
131 $params{$attr} = $args{$attr} if ($args{$attr});
134 my $id = $self->SUPER::Create(%params);
136 $self->_Attach( $args{'MIMEObj'} ) if defined $args{'MIMEObj'};
139 #Provide a way to turn off scrips if we need to
140 $RT::Logger->debug('About to think about scrips for transaction' .$self->Id);
141 if ( $args{'ActivateScrips'} ) {
142 $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
144 $RT::Logger->debug('About to prepare scrips for transaction' .$self->Id);
146 $self->{'scrips'}->Prepare(
147 Stage => 'TransactionCreate',
148 Type => $args{'Type'},
149 Ticket => $args{'Ticket'},
150 Transaction => $self->id,
152 if ($args{'CommitScrips'} ) {
153 $RT::Logger->debug('About to commit scrips for transaction' .$self->Id);
154 $self->{'scrips'}->Commit();
158 return ( $id, $self->loc("Transaction Created") );
165 Returns the Scrips object for this transaction.
166 This routine is only useful on a freshly created transaction object.
167 Scrips do not get persisted to the database with transactions.
175 return($self->{'scrips'});
184 $self->loc('Deleting this object could break referential integrity') );
189 # {{{ Routines dealing with Attachments
195 Returns the RT::Attachments Object which contains the "top-level"object
196 attachment for this transaction
204 if ( !defined( $self->{'message'} ) ) {
206 $self->{'message'} = new RT::Attachments( $self->CurrentUser );
207 $self->{'message'}->Limit(
208 FIELD => 'TransactionId',
212 $self->{'message'}->ChildrenOf(0);
214 return ( $self->{'message'} );
221 =head2 Content PARAMHASH
223 If this transaction has attached mime objects, returns the first text/plain part.
224 Otherwise, returns undef.
226 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
227 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
241 my $content_obj = $self->ContentObj;
243 $content = $content_obj->Content;
246 # If all else fails, return a message that we couldn't find any content
248 $content = $self->loc('This transaction appears to have no content');
251 if ( $args{'Quote'} ) {
253 # Remove quoted signature.
254 $content =~ s/\n-- \n(.*?)$//s;
256 # What's the longest line like?
258 foreach ( split ( /\n/, $content ) ) {
259 $max = length if ( length > $max );
263 require Text::Wrapper;
264 my $wrapper = new Text::Wrapper(
265 columns => $args{'Wrap'},
266 body_start => ( $max > 70 * 3 ? ' ' : '' ),
269 $content = $wrapper->wrap($content);
273 . $self->CreatorObj->Name() . ' - '
274 . $self->CreatedAsString() . "]:\n\n" . $content . "\n\n";
275 $content =~ s/^/> /gm;
288 Returns the RT::Attachment object which contains the content for this Transaction
298 # If we don\'t have any content, return undef now.
299 unless ( $self->Attachments->First ) {
303 # Get the set of toplevel attachments to this transaction.
304 my $Attachment = $self->Attachments->First();
306 # If it's a message or a plain part, just return the
308 if ( $Attachment->ContentType() =~ '^(text/plain$|message/)' ) {
309 return ($Attachment);
312 # If it's a multipart object, first try returning the first
315 elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
316 my $plain_parts = $Attachment->Children();
317 $plain_parts->ContentType( VALUE => 'text/plain' );
319 # If we actully found a part, return its content
320 if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
321 return ( $plain_parts->First );
324 # If that fails, return the first text/plain or message/ part
325 # which has some content.
328 my $all_parts = $Attachment->Children();
329 while ( my $part = $all_parts->Next ) {
330 if (( $part->ContentType() =~ '^(text/plain$|message/)' ) && $part->Content() ) {
338 # We found no content. suck
348 If this transaction has attached mime objects, returns the first one's subject
349 Otherwise, returns null
355 if ( $self->Attachments->First ) {
356 return ( $self->Attachments->First->Subject );
365 # {{{ sub Attachments
369 Returns all the RT::Attachment objects which are attached
370 to this transaction. Takes an optional parameter, which is
371 a ContentType that Attachments should be restricted to.
378 unless ( $self->{'attachments'} ) {
379 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
381 #If it's a comment, return an empty object if they don't have the right to see it
382 if ( $self->Type eq 'Comment' ) {
383 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
384 return ( $self->{'attachments'} );
388 #if they ain't got rights to see, return an empty object
390 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
391 return ( $self->{'attachments'} );
395 $self->{'attachments'}->Limit( FIELD => 'TransactionId',
396 VALUE => $self->Id );
398 # Get the self->{'attachments'} in the order they're put into
399 # the database. Arguably, we should be returning a tree
400 # of self->{'attachments'}, not a set...but no current app seems to need
403 $self->{'attachments'}->OrderBy( ALIAS => 'main',
408 return ( $self->{'attachments'} );
418 A private method used to attach a mime object to this transaction.
424 my $MIMEObject = shift;
426 if ( !defined($MIMEObject) ) {
428 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
430 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
433 my $Attachment = new RT::Attachment( $self->CurrentUser );
435 TransactionId => $self->Id,
436 Attachment => $MIMEObject
438 return ( $Attachment, $self->loc("Attachment created") );
446 # {{{ Routines dealing with Transaction Attributes
448 # {{{ sub Description
452 Returns a text string which describes this transaction
460 #If it's a comment or a comment email record,
461 # we need to be extra special careful
463 if ( $self->__Value('Type') =~ /^Comment/ ) {
464 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
465 return ( $self->loc("Permission Denied") );
469 #if they ain't got rights to see, don't let em
471 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
472 return ($self->loc("Permission Denied") );
476 if ( !defined( $self->Type ) ) {
477 return ( $self->loc("No transaction type specified"));
480 return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
485 # {{{ sub BriefDescription
487 =head2 BriefDescription
489 Returns a text string which briefly describes this transaction
493 sub BriefDescription {
497 #If it's a comment or a comment email record,
498 # we need to be extra special careful
499 if ( $self->__Value('Type') =~ /^Comment/ ) {
500 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
501 return ( $self->loc("Permission Denied") );
505 #if they ain't got rights to see, don't let em
507 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
508 return ( $self->loc("Permission Denied") );
512 my $type = $self->Type; #cache this, rather than calling it 30 times
514 if ( !defined( $type ) ) {
515 return $self->loc("No transaction type specified");
518 if ( $type eq 'Create' ) {
519 return ($self->loc("Ticket created"));
521 elsif ( $type =~ /Status/ ) {
522 if ( $self->Field eq 'Status' ) {
523 if ( $self->NewValue eq 'deleted' ) {
524 return ($self->loc("Ticket deleted"));
527 return ( $self->loc("Status changed from [_1] to [_2]", $self->loc($self->OldValue), $self->loc($self->NewValue) ));
533 my $no_value = $self->loc("(no value)");
534 return ( $self->loc( "[_1] changed from [_2] to [_3]", $self->Field , ( $self->OldValue || $no_value ) , $self->NewValue ));
537 if (my $code = $_BriefDescriptions{$type}) {
538 return $code->($self);
541 return $self->loc( "Default: [_1]/[_2] changed from [_3] to [_4]", $type, $self->Field, $self->OldValue, $self->NewValue );
544 %_BriefDescriptions = (
545 CommentEmailRecord => sub {
547 return $self->loc("Outgoing email about a comment recorded");
551 return $self->loc("Outgoing email recorded");
555 return $self->loc("Correspondence added");
559 return $self->loc("Comments added");
563 my $field = $self->loc('CustomField');
565 if ( $self->Field ) {
566 my $cf = RT::CustomField->new( $self->CurrentUser );
567 $cf->Load( $self->Field );
568 $field = $cf->Name();
571 if ( $self->OldValue eq '' ) {
572 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
574 elsif ( $self->NewValue eq '' ) {
575 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
579 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
584 return $self->loc("Untaken");
588 return $self->loc("Taken");
592 my $Old = RT::User->new( $self->CurrentUser );
593 $Old->Load( $self->OldValue );
594 my $New = RT::User->new( $self->CurrentUser );
595 $New->Load( $self->NewValue );
597 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
601 my $Old = RT::User->new( $self->CurrentUser );
602 $Old->Load( $self->OldValue );
603 return $self->loc("Stolen from [_1] ", $Old->Name);
607 my $New = RT::User->new( $self->CurrentUser );
608 $New->Load( $self->NewValue );
609 return $self->loc( "Given to [_1]", $New->Name );
613 my $principal = RT::Principal->new($self->CurrentUser);
614 $principal->Load($self->NewValue);
615 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
619 my $principal = RT::Principal->new($self->CurrentUser);
620 $principal->Load($self->OldValue);
621 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
625 return $self->loc( "Subject changed to [_1]", $self->Data );
630 if ( $self->NewValue ) {
631 my $URI = RT::URI->new( $self->CurrentUser );
632 $URI->FromURI( $self->NewValue );
633 if ( $URI->Resolver ) {
634 $value = $URI->Resolver->AsString;
637 $value = $self->NewValue;
639 if ( $self->Field eq 'DependsOn' ) {
640 return $self->loc( "Dependency on [_1] added", $value );
642 elsif ( $self->Field eq 'DependedOnBy' ) {
643 return $self->loc( "Dependency by [_1] added", $value );
646 elsif ( $self->Field eq 'RefersTo' ) {
647 return $self->loc( "Reference to [_1] added", $value );
649 elsif ( $self->Field eq 'ReferredToBy' ) {
650 return $self->loc( "Reference by [_1] added", $value );
652 elsif ( $self->Field eq 'MemberOf' ) {
653 return $self->loc( "Membership in [_1] added", $value );
655 elsif ( $self->Field eq 'HasMember' ) {
656 return $self->loc( "Member [_1] added", $value );
658 elsif ( $self->Field eq 'MergedInto' ) {
659 return $self->loc( "Merged into [_1]", $value );
663 return ( $self->Data );
669 if ( $self->OldValue ) {
670 my $URI = RT::URI->new( $self->CurrentUser );
671 $URI->FromURI( $self->OldValue );
672 if ( $URI->Resolver ) {
673 $value = $URI->Resolver->AsString;
676 $value = $self->OldValue;
679 if ( $self->Field eq 'DependsOn' ) {
680 return $self->loc( "Dependency on [_1] deleted", $value );
682 elsif ( $self->Field eq 'DependedOnBy' ) {
683 return $self->loc( "Dependency by [_1] deleted", $value );
686 elsif ( $self->Field eq 'RefersTo' ) {
687 return $self->loc( "Reference to [_1] deleted", $value );
689 elsif ( $self->Field eq 'ReferredToBy' ) {
690 return $self->loc( "Reference by [_1] deleted", $value );
692 elsif ( $self->Field eq 'MemberOf' ) {
693 return $self->loc( "Membership in [_1] deleted", $value );
695 elsif ( $self->Field eq 'HasMember' ) {
696 return $self->loc( "Member [_1] deleted", $value );
700 return ( $self->Data );
705 if ( $self->Field eq 'Queue' ) {
706 my $q1 = new RT::Queue( $self->CurrentUser );
707 $q1->Load( $self->OldValue );
708 my $q2 = new RT::Queue( $self->CurrentUser );
709 $q2->Load( $self->NewValue );
710 return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
713 # Write the date/time change at local time:
714 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
715 my $t1 = new RT::Date($self->CurrentUser);
716 $t1->Set(Format => 'ISO', Value => $self->NewValue);
717 my $t2 = new RT::Date($self->CurrentUser);
718 $t2->Set(Format => 'ISO', Value => $self->OldValue);
719 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
722 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $self->OldValue, $self->NewValue );
725 PurgeTransaction => sub {
727 return $self->loc("Transaction [_1] purged", $self->Data);
733 # {{{ Utility methods
739 Returns true if the creator of the transaction is a requestor of the ticket.
740 Returns false otherwise
746 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
753 sub _ClassAccessible {
756 id => { read => 1, type => 'int(11)', default => '' },
758 { read => 1, write => 1, type => 'int(11)', default => '' },
760 { read => 1, public => 1, type => 'int(11)', default => '' },
761 TimeTaken => { read => 1, type => 'int(11)', default => '' },
762 Type => { read => 1, type => 'varchar(20)', default => '' },
763 Field => { read => 1, type => 'varchar(40)', default => '' },
764 OldValue => { read => 1, type => 'varchar(255)', default => '' },
765 NewValue => { read => 1, type => 'varchar(255)', default => '' },
766 Data => { read => 1, type => 'varchar(100)', default => '' },
767 Creator => { read => 1, auto => 1, type => 'int(11)', default => '' },
769 { read => 1, auto => 1, type => 'datetime', default => '' },
782 return ( 0, $self->loc('Transactions are immutable') );
791 Takes the name of a table column.
792 Returns its value as a string, if the user passes an ACL check
801 #if the field is public, return it.
802 if ( $self->_Accessible( $field, 'public' ) ) {
803 return ( $self->__Value($field) );
807 #If it's a comment, we need to be extra special careful
808 if ( $self->__Value('Type') eq 'Comment' ) {
809 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
813 elsif ( $self->__Value('Type') eq 'CommentEmailRecord' ) {
814 unless ( $self->CurrentUserHasRight('ShowTicketComments')
815 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
820 elsif ( $self->__Value('Type') eq 'EmailRecord' ) {
821 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail')) {
827 #if they ain't got rights to see, don't let em
829 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
834 return ( $self->__Value($field) );
840 # {{{ sub CurrentUserHasRight
842 =head2 CurrentUserHasRight RIGHT
844 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
849 sub CurrentUserHasRight {
853 $self->CurrentUser->HasRight(
855 Object => $self->TicketObj
862 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
866 'fast_update_p' => 1,
867 'cache_for_sec' => 6000,