3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
26 RT::Transaction - RT\'s transaction object
36 Each RT::Transaction describes an atomic change to a ticket object
37 or an update to an RT::Ticket object.
38 It can have arbitrary MIME attachments.
45 ok(require RT::Transaction);
52 no warnings qw(redefine);
54 use vars qw( %_BriefDescriptions );
62 Create a new transaction.
64 This routine should _never_ be called anything other Than RT::Ticket. It should not be called
65 from client code. Ever. Not ever. If you do this, we will hunt you down. and break your kneecaps.
66 Then the unpleasant stuff will start.
68 TODO: Document what gets passed to this
88 #if we didn't specify a ticket, we need to bail
89 unless ( $args{'Ticket'} ) {
90 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify a ticket id"));
95 #lets create our transaction
96 my %params = (Ticket => $args{'Ticket'},
97 Type => $args{'Type'},
98 Data => $args{'Data'},
99 Field => $args{'Field'},
100 OldValue => $args{'OldValue'},
101 NewValue => $args{'NewValue'},
102 Created => $args{'Created'}
105 # Parameters passed in during an import that we probably don't want to touch, otherwise
106 foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
107 $params{$attr} = $args{$attr} if ($args{$attr});
110 my $id = $self->SUPER::Create(%params);
112 $self->_Attach( $args{'MIMEObj'} )
113 if defined $args{'MIMEObj'};
115 #Provide a way to turn off scrips if we need to
116 if ( $args{'ActivateScrips'} ) {
118 RT::Scrips->new($RT::SystemUser)->Apply(
119 Stage => 'TransactionCreate',
120 Type => $args{'Type'},
121 Ticket => $args{'Ticket'},
122 Transaction => $self->id,
126 return ( $id, $self->loc("Transaction Created") );
136 $self->loc('Deleting this object could break referential integrity') );
141 # {{{ Routines dealing with Attachments
147 Returns the RT::Attachments Object which contains the "top-level"object
148 attachment for this transaction
156 if ( !defined( $self->{'message'} ) ) {
158 $self->{'message'} = new RT::Attachments( $self->CurrentUser );
159 $self->{'message'}->Limit(
160 FIELD => 'TransactionId',
164 $self->{'message'}->ChildrenOf(0);
166 return ( $self->{'message'} );
173 =head2 Content PARAMHASH
175 If this transaction has attached mime objects, returns the first text/plain part.
176 Otherwise, returns undef.
178 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
179 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
193 my $content_obj = $self->ContentObj;
195 $content = $content_obj->Content;
198 # If all else fails, return a message that we couldn't find any content
200 $content = $self->loc('This transaction appears to have no content');
203 if ( $args{'Quote'} ) {
205 # Remove quoted signature.
206 $content =~ s/\n-- \n(.*)$//s;
208 # What's the longest line like?
210 foreach ( split ( /\n/, $content ) ) {
211 $max = length if ( length > $max );
215 require Text::Wrapper;
216 my $wrapper = new Text::Wrapper(
217 columns => $args{'Wrap'},
218 body_start => ( $max > 70 * 3 ? ' ' : '' ),
221 $content = $wrapper->wrap($content);
225 . $self->CreatorObj->Name() . ' - '
226 . $self->CreatedAsString() . "]:\n\n" . $content . "\n\n";
227 $content =~ s/^/> /gm;
240 Returns the RT::Attachment object which contains the content for this Transaction
250 # If we don\'t have any content, return undef now.
251 unless ( $self->Attachments->First ) {
255 # Get the set of toplevel attachments to this transaction.
256 my $Attachment = $self->Attachments->First();
258 # If it's a message or a plain part, just return the
260 if ( $Attachment->ContentType() =~ '^(text/plain$|message/)' ) {
261 return ($Attachment);
264 # If it's a multipart object, first try returning the first
267 elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
268 my $plain_parts = $Attachment->Children();
269 $plain_parts->ContentType( VALUE => 'text/plain' );
271 # If we actully found a part, return its content
272 if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
273 return ( $plain_parts->First );
276 # If that fails, return the first text/plain or message/ part
277 # which has some content.
280 my $all_parts = $Attachment->Children();
281 while ( my $part = $all_parts->Next ) {
282 if (( $part->ContentType() =~ '^(text/plain$|message/)' ) && $part->Content() ) {
290 # We found no content. suck
300 If this transaction has attached mime objects, returns the first one's subject
301 Otherwise, returns null
307 if ( $self->Attachments->First ) {
308 return ( $self->Attachments->First->Subject );
317 # {{{ sub Attachments
321 Returns all the RT::Attachment objects which are attached
322 to this transaction. Takes an optional parameter, which is
323 a ContentType that Attachments should be restricted to.
330 unless ( $self->{'attachments'} ) {
331 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
333 #If it's a comment, return an empty object if they don't have the right to see it
334 if ( $self->Type eq 'Comment' ) {
335 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
336 return ( $self->{'attachments'} );
340 #if they ain't got rights to see, return an empty object
342 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
343 return ( $self->{'attachments'} );
347 $self->{'attachments'}->Limit( FIELD => 'TransactionId',
348 VALUE => $self->Id );
350 # Get the self->{'attachments'} in the order they're put into
351 # the database. Arguably, we should be returning a tree
352 # of self->{'attachments'}, not a set...but no current app seems to need
355 $self->{'attachments'}->OrderBy( ALIAS => 'main',
360 return ( $self->{'attachments'} );
370 A private method used to attach a mime object to this transaction.
376 my $MIMEObject = shift;
378 if ( !defined($MIMEObject) ) {
380 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
382 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
385 my $Attachment = new RT::Attachment( $self->CurrentUser );
387 TransactionId => $self->Id,
388 Attachment => $MIMEObject
390 return ( $Attachment, $self->loc("Attachment created") );
398 # {{{ Routines dealing with Transaction Attributes
400 # {{{ sub Description
404 Returns a text string which describes this transaction
412 #If it's a comment, we need to be extra special careful
413 if ( $self->__Value('Type') eq 'Comment' ) {
414 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
415 return ( $self->loc("Permission Denied") );
419 #if they ain't got rights to see, don't let em
421 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
422 return ($self->loc("Permission Denied") );
426 if ( !defined( $self->Type ) ) {
427 return ( $self->loc("No transaction type specified"));
430 return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
435 # {{{ sub BriefDescription
437 =head2 BriefDescription
439 Returns a text string which briefly describes this transaction
443 sub BriefDescription {
448 #If it's a comment, we need to be extra special careful
449 if ( $self->__Value('Type') eq 'Comment' ) {
450 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
451 return ( $self->loc("Permission Denied") );
455 #if they ain't got rights to see, don't let em
457 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
458 return ( $self->loc("Permission Denied") );
462 my $type = $self->Type; #cache this, rather than calling it 30 times
464 if ( !defined( $type ) ) {
465 return $self->loc("No transaction type specified");
468 if ( $type eq 'Create' ) {
469 return ($self->loc("Ticket created"));
471 elsif ( $type =~ /Status/ ) {
472 if ( $self->Field eq 'Status' ) {
473 if ( $self->NewValue eq 'deleted' ) {
474 return ($self->loc("Ticket deleted"));
477 return ( $self->loc("Status changed from [_1] to [_2]", $self->loc($self->OldValue), $self->loc($self->NewValue) ));
483 my $no_value = $self->loc("(no value)");
484 return ( $self->loc( "[_1] changed from [_2] to [_3]", $self->Field , ( $self->OldValue || $no_value ) , $self->NewValue ));
487 if (my $code = $_BriefDescriptions{$type}) {
488 return $code->($self);
491 return $self->loc( "Default: [_1]/[_2] changed from [_3] to [_4]", $type, $self->Field, $self->OldValue, $self->NewValue );
494 %_BriefDescriptions = (
497 return $self->loc("Correspondence added");
501 return $self->loc("Comments added");
505 my $field = $self->loc('CustomField');
507 if ( $self->Field ) {
508 my $cf = RT::CustomField->new( $self->CurrentUser );
509 $cf->Load( $self->Field );
510 $field = $cf->Name();
513 if ( $self->OldValue eq '' ) {
514 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
516 elsif ( $self->NewValue eq '' ) {
517 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
521 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
526 return $self->loc("Untaken");
530 return $self->loc("Taken");
534 my $Old = RT::User->new( $self->CurrentUser );
535 $Old->Load( $self->OldValue );
536 my $New = RT::User->new( $self->CurrentUser );
537 $New->Load( $self->NewValue );
539 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
543 my $Old = RT::User->new( $self->CurrentUser );
544 $Old->Load( $self->OldValue );
545 return $self->loc("Stolen from [_1] ", $Old->Name);
549 my $New = RT::User->new( $self->CurrentUser );
550 $New->Load( $self->NewValue );
551 return $self->loc( "Given to [_1]", $New->Name );
555 my $principal = RT::Principal->new($self->CurrentUser);
556 $principal->Load($self->NewValue);
557 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
561 my $principal = RT::Principal->new($self->CurrentUser);
562 $principal->Load($self->OldValue);
563 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
567 return $self->loc( "Subject changed to [_1]", $self->Data );
572 if ( $self->NewValue ) {
573 my $URI = RT::URI->new( $self->CurrentUser );
574 $URI->FromURI( $self->NewValue );
575 if ( $URI->Resolver ) {
576 $value = $URI->Resolver->AsString;
579 $value = $self->NewValue;
581 if ( $self->Field eq 'DependsOn' ) {
582 return $self->loc( "Dependency on [_1] added", $value );
584 elsif ( $self->Field eq 'DependedOnBy' ) {
585 return $self->loc( "Dependency by [_1] added", $value );
588 elsif ( $self->Field eq 'RefersTo' ) {
589 return $self->loc( "Reference to [_1] added", $value );
591 elsif ( $self->Field eq 'ReferredToBy' ) {
592 return $self->loc( "Reference by [_1] added", $value );
594 elsif ( $self->Field eq 'MemberOf' ) {
595 return $self->loc( "Membership in [_1] added", $value );
597 elsif ( $self->Field eq 'HasMember' ) {
598 return $self->loc( "Member [_1] added", $value );
602 return ( $self->Data );
608 if ( $self->OldValue ) {
609 my $URI = RT::URI->new( $self->CurrentUser );
610 $URI->FromURI( $self->OldValue );
611 if ( $URI->Resolver ) {
612 $value = $URI->Resolver->AsString;
615 $value = $self->OldValue;
618 if ( $self->Field eq 'DependsOn' ) {
619 return $self->loc( "Dependency on [_1] deleted", $value );
621 elsif ( $self->Field eq 'DependedOnBy' ) {
622 return $self->loc( "Dependency by [_1] deleted", $value );
625 elsif ( $self->Field eq 'RefersTo' ) {
626 return $self->loc( "Reference to [_1] deleted", $value );
628 elsif ( $self->Field eq 'ReferredToBy' ) {
629 return $self->loc( "Reference by [_1] deleted", $value );
631 elsif ( $self->Field eq 'MemberOf' ) {
632 return $self->loc( "Membership in [_1] deleted", $value );
634 elsif ( $self->Field eq 'HasMember' ) {
635 return $self->loc( "Member [_1] deleted", $value );
639 return ( $self->Data );
644 if ( $self->Field eq 'Queue' ) {
645 my $q1 = new RT::Queue( $self->CurrentUser );
646 $q1->Load( $self->OldValue );
647 my $q2 = new RT::Queue( $self->CurrentUser );
648 $q2->Load( $self->NewValue );
649 return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
652 # Write the date/time change at local time:
653 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
654 my $t1 = new RT::Date($self->CurrentUser);
655 $t1->Set(Format => 'ISO', Value => $self->NewValue);
656 my $t2 = new RT::Date($self->CurrentUser);
657 $t2->Set(Format => 'ISO', Value => $self->OldValue);
658 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
661 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $self->OldValue, $self->NewValue );
664 PurgeTransaction => sub {
666 return $self->loc("Transaction [_1] purged", $self->Data);
672 # {{{ Utility methods
678 Returns true if the creator of the transaction is a requestor of the ticket.
679 Returns false otherwise
685 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
692 sub _ClassAccessible {
695 id => { read => 1, type => 'int(11)', default => '' },
697 { read => 1, write => 1, type => 'int(11)', default => '' },
699 { read => 1, public => 1, type => 'int(11)', default => '' },
700 TimeTaken => { read => 1, type => 'int(11)', default => '' },
701 Type => { read => 1, type => 'varchar(20)', default => '' },
702 Field => { read => 1, type => 'varchar(40)', default => '' },
703 OldValue => { read => 1, type => 'varchar(255)', default => '' },
704 NewValue => { read => 1, type => 'varchar(255)', default => '' },
705 Data => { read => 1, type => 'varchar(100)', default => '' },
706 Creator => { read => 1, auto => 1, type => 'int(11)', default => '' },
708 { read => 1, auto => 1, type => 'datetime', default => '' },
721 return ( 0, $self->loc('Transactions are immutable') );
730 Takes the name of a table column.
731 Returns its value as a string, if the user passes an ACL check
740 #if the field is public, return it.
741 if ( $self->_Accessible( $field, 'public' ) ) {
742 return ( $self->__Value($field) );
746 #If it's a comment, we need to be extra special careful
747 if ( $self->__Value('Type') eq 'Comment' ) {
748 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
753 #if they ain't got rights to see, don't let em
755 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
760 return ( $self->__Value($field) );
766 # {{{ sub CurrentUserHasRight
768 =head2 CurrentUserHasRight RIGHT
770 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
775 sub CurrentUserHasRight {
779 $self->CurrentUser->HasRight(
781 Object => $self->TicketObj
788 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
792 'fast_update_p' => 1,
793 'cache_for_sec' => 180,